Skip to content

Commit

Permalink
Merge pull request #8 from ministryofjustice/investigate-user-permiss…
Browse files Browse the repository at this point in the history
…ions

#1879 Experimenting with user management UI
  • Loading branch information
julialawrence authored Nov 15, 2023
2 parents 4564f3f + 579cb36 commit 440605a
Show file tree
Hide file tree
Showing 6 changed files with 113 additions and 20 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ cython_debug/
#.idea/

# aad secrets
secrets.json
secrets*.json

# IDE
**/*.vscode
Expand Down
48 changes: 38 additions & 10 deletions app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,20 @@
import json
from models import init_app, db, DataSource, UserDataSourcePermission, User
from forms import DataSourceForm
from azure_active_directory import create_aad_group, add_users_to_aad_group
from azure_active_directory import (
create_aad_group,
add_users_to_aad_group,
create_team_from_group,
)
from cluster_manager import launch_vscode_for_user, sanitize_username
from requests.exceptions import RequestException
from gevent.pywsgi import WSGIServer
from geventwebsocket.handler import WebSocketHandler
from geventwebsocket import WebSocketError
import websocket
import functools

print = functools.partial(print, flush=True) # redefine to flush the buffer always

# Load secrets from a JSON file
with open("secrets.json") as f:
Expand All @@ -36,7 +43,7 @@

app.config["SECRET_KEY"] = secrets["session_secret"]
app.config["SESSION_TYPE"] = "filesystem"
app.config['LOGGING_LEVEL'] = 10
app.config["LOGGING_LEVEL"] = 10

# Initialize database
init_app(app)
Expand Down Expand Up @@ -197,6 +204,30 @@ def create_data_source():
flash(
"Data source and associated AAD group created successfully!", "success"
)

team_creation_response = create_team_from_group(
group_id=aad_group_id,
access_token=session.get("token").get("access_token"),
)
if team_creation_response:
print(f"Team Creation Response: %s", team_creation_response)
team_info = team_creation_response.json()
print(f"Team Info: %s", team_info)
team_id = team_info.get("id")
team_web_url = team_info.get("webUrl")
if team_id and team_web_url:
data_source.team_id = team_id
data_source.team_web_url = team_web_url

db.session.commit()
flash(
"Team created and associated with data source successfully!",
"success",
)
else:
flash("Failed to extract team ID or web url from the response.", "error")
else: # If there was an error
flash("Failed to create the team.", "error")
else:
flash("Failed to create AAD group.", "error")

Expand Down Expand Up @@ -306,8 +337,9 @@ def data_source_details(id):
creator=creator,
assigned_users=assigned_users,
user=creator,
is_admin=is_user_aad_group_admin, # Here we use the AAD group check instead of the local one
is_admin=is_user_aad_group_admin,
user_has_access=user_has_access,
team_id=data_source.team_id,
)


Expand Down Expand Up @@ -438,13 +470,11 @@ def start_vscode(id):
# Redirect to a waiting page or directly embed the VS Code interface if it's ready
# The implementation of this part can vary based on how you handle the VS Code UI embedding
# return render_template("vscode.html")
vscode_url = url_for('vscode_proxy')
vscode_url = url_for("vscode_proxy")
return redirect(vscode_url)


@app.route(
"/vscode_proxy", methods=["GET", "POST", "PUT", "DELETE", "PATCH"]
)
@app.route("/vscode_proxy", methods=["GET", "POST", "PUT", "DELETE", "PATCH"])
def vscode_proxy():
"""
This route acts as a proxy for the VS Code server, forwarding requests and responses.
Expand All @@ -464,9 +494,7 @@ def vscode_proxy():
app.logger.info(f"Service name: {service_name}")

# Construct the URL of the VS Code server for this user.
vscode_url = (
f"http://vscode-service-dbe0354c6b5f4bdc8a356af8d4ec68ed.dataaccessmanager.svc.cluster.local/"
)
vscode_url = f"http://vscode-service-dbe0354c6b5f4bdc8a356af8d4ec68ed.dataaccessmanager.svc.cluster.local/"
app.logger.info(f"VSCode URL: {vscode_url}")

# Check if it's a WebSocket request
Expand Down
56 changes: 53 additions & 3 deletions app/azure_active_directory.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import requests
from flask import flash
import functools
import time

print = functools.partial(print, flush=True) # redefine to flush the buffer always


def create_aad_group(group_name, description, user_id, access_token, dry_run=False):
Expand All @@ -12,13 +16,15 @@ def create_aad_group(group_name, description, user_id, access_token, dry_run=Fal
"Content-Type": "application/json",
}

# The payload for the request
# The payload for the request, setting 'visibility' to 'Private' to make a private group
group_data = {
"displayName": group_name,
"description": description,
"mailEnabled": False,
"groupTypes": ["Unified"],
"mailEnabled": True,
"mailNickname": group_name.replace(" ", "").lower(),
"securityEnabled": True,
"securityEnabled": False,
"visibility": "Private", # Setting the group as a private group
}

# If dry_run is enabled, we skip the actual creation process
Expand All @@ -45,6 +51,7 @@ def create_aad_group(group_name, description, user_id, access_token, dry_run=Fal
flash("User added as an admin to the group successfully!", "success")
else:
flash("Failed to add the user as an admin to the group.", "error")
print("Failed to add the user as an admin to the group.", "error")
else:
flash(
"Group was created but user could not be added as an admin.", "warning"
Expand All @@ -64,6 +71,49 @@ def create_aad_group(group_name, description, user_id, access_token, dry_run=Fal
return None


def create_team_from_group(group_id, access_token):
url = f"https://graph.microsoft.com/v1.0/groups/{group_id}/team"

headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
}

team_data = {
"memberSettings": {"allowCreateUpdateChannels": True},
"messagingSettings": {
"allowUserEditMessages": True,
"allowUserDeleteMessages": True,
},
"funSettings": {"allowGiphy": True, "giphyContentRating": "Moderate"},
}

retry_count = 0
max_retries = 5
backoff_time = 10 # seconds

while retry_count < max_retries:

try:
response = requests.put(
url, headers=headers, json=team_data
) # Using PUT as per Graph API documentation for creating team from group
response.raise_for_status()
# If the request was successful, get the JSON response
return response
except requests.exceptions.HTTPError as err:
print(f"HTTP Error {err} encountered...")
if response.status_code == 404 and retry_count < max_retries - 1:
print(f"404 error encountered, retrying in {backoff_time} seconds...")
time.sleep(backoff_time)
retry_count += 1
continue
except Exception as e:
print(f"An unexpected error occurred: {e}")
flash("An unexpected error occurred while creating the team.", "error")
return None


def add_user_as_group_admin(group_id, user_id, access_token):
# Microsoft Graph API endpoint to add a member to the group
url = f"https://graph.microsoft.com/v1.0/groups/{group_id}/members/$ref"
Expand Down
17 changes: 13 additions & 4 deletions app/cluster_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ def create_service_account(user_id):
else:
print(f"Failed to check service account {name}: {e}", flush=True)


def deploy_vscode_server(user_id):
namespace = "dataaccessmanager"
sanitized_user_id = sanitize_username(user_id)
Expand All @@ -77,12 +78,15 @@ def deploy_vscode_server(user_id):
],
)
metadata = client.V1ObjectMeta(name=name, labels={"app": name})
pod = client.V1Pod(api_version="v1", kind="Pod", metadata=metadata, spec=pod_spec)
pod = client.V1Pod(
api_version="v1", kind="Pod", metadata=metadata, spec=pod_spec
)
api_instance.create_namespaced_pod(namespace, pod)
print(f"Deployed pod {name}.", flush=True)
else:
print(f"Failed to check pod {name}: {e}", flush=True)


def create_service_for_vscode(user_id):
namespace = "dataaccessmanager"
sanitized_user_id = sanitize_username(user_id)
Expand Down Expand Up @@ -111,6 +115,7 @@ def create_service_for_vscode(user_id):
else:
print(f"Failed to check service {name}: {e}", flush=True)


def wait_for_pod_ready(namespace, pod_name):
# Create a watch object for Pod events
w = watch.Watch()
Expand All @@ -126,6 +131,7 @@ def wait_for_pod_ready(namespace, pod_name):

return False # Default case, though your logic might differ based on how you want to handle timeouts


def wait_for_service_ready(service_url, timeout=300):
"""
Wait for the service to become ready by sending requests to the service URL.
Expand All @@ -138,16 +144,17 @@ def wait_for_service_ready(service_url, timeout=300):
try:
response = requests.get(service_url, timeout=5)
if response.status_code == 200:
print("VSCODE Service is ready",flush=True)
print("VSCODE Service is ready", flush=True)
# Service is ready
return True
except requests.RequestException as e:
print(f"Request failed: {e}")
# Wait for a while before retrying
time.sleep(5)
print("VSCODE Service is NOT ready",flush=True)
print("VSCODE Service is NOT ready", flush=True)
return False # Timeout reached


def launch_vscode_for_user(user_id):
# Step 1: Create a service account for the user
create_service_account(user_id)
Expand All @@ -164,7 +171,9 @@ def launch_vscode_for_user(user_id):
namespace = "dataaccessmanager"

# if wait_for_pod_ready(namespace, pod_name):
if wait_for_service_ready(service_url="http://vscode-service-dbe0354c6b5f4bdc8a356af8d4ec68ed.dataaccessmanager.svc.cluster.local/"):
if wait_for_service_ready(
service_url="http://vscode-service-dbe0354c6b5f4bdc8a356af8d4ec68ed.dataaccessmanager.svc.cluster.local/"
):
print("VS Code server is ready for use.")
else:
print("There was a problem starting the VS Code server.")
Expand Down
2 changes: 2 additions & 0 deletions app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ class DataSource(db.Model):
)
created_by = db.Column(db.String(255), db.ForeignKey("users.id"))
aad_group_id = db.Column(db.String(255), unique=True)
team_id = db.Column(db.String(255), unique=True)
team_web_url = db.Column(db.String(255), unique=True)

# Relationships
permissions = db.relationship(
Expand Down
8 changes: 6 additions & 2 deletions app/templates/data_source_details.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,18 @@ <h3>Assigned Users:</h3>
<!-- Link or form for managing users. This part assumes you have a 'manage_users' route set up. -->
<div>
{% if is_admin %}
<a href="{{ url_for('manage_users', id=data_source.id) }}">Manage Users</a>
{% endif %}
<a href="{{ url_for('manage_users', id=data_source.id) }}">Manage Users</a>
{% endif %}
</div>

<!-- Check if the user has the necessary permissions to launch VS Code -->
{% if is_admin or user_has_access %}
<!-- Button to launch VS Code -->
<button onclick="launchVSCode()">Launch VS Code</button>
<a href="{{ data_source.team_web_url }}" target="_blank">Access MS Teams Team</a>
{% else %}
<!-- Link to MS Teams team for users who are not admins and don't have access -->
<a href="{{ data_source.team_web_url }}" target="_blank">Access MS Teams Team</a>
{% endif %}

<script type="text/javascript">
Expand Down

0 comments on commit 440605a

Please sign in to comment.