Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#1879 Experimenting with user management UI #8

Merged
merged 16 commits into from
Nov 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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