From f8d7d01fcbcecfb6ff0db5ab2be263660373f1e5 Mon Sep 17 00:00:00 2001 From: julialawrence Date: Tue, 17 Oct 2023 13:18:06 +0100 Subject: [PATCH 1/7] Authenticate with AzureAD devl and retrieve groups. --- .gitignore | 8 ++++ app.py | 81 ++++++++++++++++++++++++++++++++++++++++ templates/dashboard.html | 19 ++++++++++ templates/homepage.html | 43 +++++++++++++++++++++ 4 files changed, 151 insertions(+) create mode 100644 app.py create mode 100644 templates/dashboard.html create mode 100644 templates/homepage.html diff --git a/.gitignore b/.gitignore index 68bc17f..fb04179 100644 --- a/.gitignore +++ b/.gitignore @@ -124,6 +124,7 @@ celerybeat.pid .venv env/ venv/ +myenv/ ENV/ env.bak/ venv.bak/ @@ -158,3 +159,10 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ + +# aad secrets +secrets.json + +# IDE + +**.*vscode \ No newline at end of file diff --git a/app.py b/app.py new file mode 100644 index 0000000..d6e0513 --- /dev/null +++ b/app.py @@ -0,0 +1,81 @@ +from flask import Flask, redirect, url_for, session, request, jsonify, render_template +from authlib.integrations.flask_client import OAuth +import os +import requests +import json + +# Load secrets from a JSON file +with open('secrets.json') as f: + secrets = json.load(f) + +# Initialize the Flask application +app = Flask(__name__) +app.secret_key = 'something-secret' # Change this to a random secret key + +# Azure AD OAuth configuration +oauth = OAuth(app) +azure = oauth.register( + 'azure', + client_id=secrets['client_id'], + client_secret=secrets['client_secret'], + server_metadata_url=f'https://login.microsoftonline.com/{secrets["tenant_id"]}/v2.0/.well-known/openid-configuration', + client_kwargs={ + 'scope': 'openid email profile User.ReadWrite.All Group.ReadWrite.All offline_access', + }, +) + +@app.route('/') +def homepage(): + user_info = session.get('user') + groups = session.get('groups') + if user_info: + # User is logged in, render the dashboard + return render_template('dashboard.html', user_info=user_info, groups=groups) + # For logged-out users, render the homepage + return render_template('homepage.html') + +@app.route('/login') +def login(): + redirect_uri = url_for('authorized', _external=True) + return azure.authorize_redirect(redirect_uri) + +@app.route('/login/authorized') +def authorized(): + token = azure.authorize_access_token() + user_resp = azure.get('https://graph.microsoft.com/v1.0/me', token=token) + user_info = user_resp.json() + + # Debug: Print the whole response to see all available fields + print("\nUser response:") + print(json.dumps(user_info, indent=4)) # Pretty-print the JSON response + + session['user'] = user_info + + # Attempt to get the user's group memberships + try: + groups_resp = azure.get('https://graph.microsoft.com/v1.0/me/memberOf', token=token) + groups_info = groups_resp.json() + + # Debug: Print the groups response + print("\nGroups response:") + print(json.dumps(groups_info, indent=4)) # Pretty-print the JSON response + + # Assuming the response contains an array of group objects + session['groups'] = groups_info.get('value', []) + + except Exception as e: + # If the group request fails, print out why + print("\nFailed to fetch groups:") + print(e) + + return redirect('/') # Redirect to the homepage + +@app.route('/logout') +def logout(): + session.pop('user', None) + session.pop('groups', None) + return redirect('/') + +# Run the Flask application +if __name__ == "__main__": + app.run(host='127.0.0.1', port=5000) diff --git a/templates/dashboard.html b/templates/dashboard.html new file mode 100644 index 0000000..c9c5186 --- /dev/null +++ b/templates/dashboard.html @@ -0,0 +1,19 @@ + + + + + + Dashboard + + +

Dashboard

+

Welcome, {{ user_info.displayName }}!

+

Your Groups

+ + Logout + + diff --git a/templates/homepage.html b/templates/homepage.html new file mode 100644 index 0000000..a48f997 --- /dev/null +++ b/templates/homepage.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + DataAccessManager + + + + + + +
+

Welcome to DataAccessManager

+

Simplifying your data access management.

+ Login with Azure AD +
+ + + + + + + + + From 3beedde6c8f1b5a7acc91c3edf809ffc2cd620e3 Mon Sep 17 00:00:00 2001 From: julialawrence Date: Tue, 17 Oct 2023 18:04:56 +0100 Subject: [PATCH 2/7] Create DataSource and store in local database. --- app.py | 106 +++++++++++++++++++++++++++++- forms.py | 9 +++ models.py | 56 ++++++++++++++++ templates/base.html | 10 +++ templates/create_data_source.html | 32 +++++++++ templates/dashboard.html | 13 +++- templates/data_sources.html | 37 +++++++++++ 7 files changed, 259 insertions(+), 4 deletions(-) create mode 100644 forms.py create mode 100644 models.py create mode 100644 templates/base.html create mode 100644 templates/create_data_source.html create mode 100644 templates/data_sources.html diff --git a/app.py b/app.py index d6e0513..6c191ff 100644 --- a/app.py +++ b/app.py @@ -1,8 +1,11 @@ -from flask import Flask, redirect, url_for, session, request, jsonify, render_template +from flask import Flask, redirect, url_for, session, render_template, flash from authlib.integrations.flask_client import OAuth import os import requests import json +from models import init_app, db, DataSource, UserDataSourcePermission, User +from forms import DataSourceForm # Import the form you created + # Load secrets from a JSON file with open('secrets.json') as f: @@ -10,7 +13,10 @@ # Initialize the Flask application app = Flask(__name__) -app.secret_key = 'something-secret' # Change this to a random secret key +app.secret_key = 'development' # Change this to a random secret key + +# Initialize database +init_app(app) # Azure AD OAuth configuration oauth = OAuth(app) @@ -28,9 +34,10 @@ def homepage(): user_info = session.get('user') groups = session.get('groups') + user_role = session.get('user_role', 'user') if user_info: # User is logged in, render the dashboard - return render_template('dashboard.html', user_info=user_info, groups=groups) + return render_template('dashboard.html', user_info=user_info, groups=groups, user_role=user_role) # For logged-out users, render the homepage return render_template('homepage.html') @@ -49,6 +56,26 @@ def authorized(): print("\nUser response:") print(json.dumps(user_info, indent=4)) # Pretty-print the JSON response + # Extract user information from the response + user_id = user_info.get('id') # Adjust if the ID is under a different key in the response + user_name = user_info.get('displayName') # Or appropriate field for the user's name + user_email = user_info.get('userPrincipalName') # Or appropriate field for the user's email + + # Check if the user exists in the database + user = User.query.get(user_id) + if not user: + # User not found in the database, so let's create a new one + user = User( + id=user_id, + name=user_name, + email=user_email, + # ... any other fields you want to populate ... + ) + db.session.add(user) + db.session.commit() + print(f"User {user_name} added to the database.") + + # Store user info in session session['user'] = user_info # Attempt to get the user's group memberships @@ -68,14 +95,87 @@ def authorized(): print("\nFailed to fetch groups:") print(e) + # Determine if the user is an admin + is_admin = check_if_user_is_admin(session.get('groups',[])) + + # Store role in session + session['user_role'] = 'admin' if is_admin else 'user' # It seems there was a typo here, setting 'user' for non-admins + return redirect('/') # Redirect to the homepage + +@app.route('/datasource/create', methods=['GET', 'POST']) +def create_data_source(): + form = DataSourceForm() + if form.validate_on_submit(): + current_user_id = session['user']['id'] + # Create a new data source instance + data_source = DataSource( + name=form.name.data, + description=form.description.data, + aws_resource_arn=form.aws_resource_arn.data, # This field is for the AWS ARN + created_by=current_user_id + + ) + + # Add to the database session and commit + db.session.add(data_source) + db.session.commit() + + # Here, you should trigger the AAD group creation and store the returned group ID + # For now, we'll simulate this with a placeholder + # data_source.aad_group_id = "dummy-aad-group-id" # Placeholder for the AAD group ID + # db.session.commit() + + flash('Data source created successfully!', 'success') + return redirect(url_for('list_data_sources')) # Redirect to the homepage or list of data sources + + # If the form is not submitted or not valid, render the form page + return render_template('create_data_source.html', form=form) + +@app.route('/datasources') +def list_data_sources(): + # Query all data sources from the database + data_sources = DataSource.query.all() + + # Create a list of dictionaries containing the data you want to display + data_sources_info = [] + for data_source in data_sources: + info = { + 'name': data_source.name, + 'description': data_source.description, + 'created_by': User.query.get(data_source.created_by).name if data_source.created_by else 'N/A', # Assuming 'created_by' is a field in your DataSource model + 'created_at': data_source.created_at.strftime('%Y-%m-%d %H:%M:%S') # Format the date as you prefer + } + data_sources_info.append(info) + + # Pass the list of dictionaries to the template + return render_template('data_sources.html', data_sources=data_sources_info) + + + @app.route('/logout') def logout(): session.pop('user', None) session.pop('groups', None) return redirect('/') +def check_if_user_is_admin(group_ids): + admin_group_name = "data-platform-single-ui-group" # This should be the actual ID of your admin group in Azure AD + + # Debugging: Print the group information to the console for verification + print("\nDebugging Info: Groups associated with the user:") + for group in group_ids: + print(group.get('displayName')) + + # Extracting the 'displayName' from each group and checking if 'admin_group_name' is one of them + is_user_admin = any(group.get('displayName') == admin_group_name for group in group_ids) + + # Debugging: Print whether the user is an admin + print(f"Is the user an admin: {is_user_admin}") + + return is_user_admin + # Run the Flask application if __name__ == "__main__": app.run(host='127.0.0.1', port=5000) diff --git a/forms.py b/forms.py new file mode 100644 index 0000000..f8e2132 --- /dev/null +++ b/forms.py @@ -0,0 +1,9 @@ +from flask_wtf import FlaskForm +from wtforms import StringField, TextAreaField, SubmitField +from wtforms.validators import DataRequired + +class DataSourceForm(FlaskForm): + name = StringField('Data Source Name', validators=[DataRequired()]) + description = TextAreaField('Description') # Not mandatory + aws_resource_arn = StringField('AWS Resource ARN', validators=[DataRequired()]) + submit = SubmitField('Create') diff --git a/models.py b/models.py new file mode 100644 index 0000000..868c9d1 --- /dev/null +++ b/models.py @@ -0,0 +1,56 @@ +# models.py +from flask_sqlalchemy import SQLAlchemy +from datetime import datetime + +db = SQLAlchemy() + +def init_app(app): + app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///data_access_manager.db' # Ensure this is the correct path + app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False # Optional: Disable event system if not needed + db.init_app(app) + + with app.app_context(): + db.create_all() # This command should create the database file + +class DataSource(db.Model): + __tablename__ = 'data_sources' + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(100), nullable=False) + description = db.Column(db.String(255)) + aws_resource_arn = db.Column(db.String(255)) # If applicable, ARN for the corresponding AWS resource + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + created_by = db.Column(db.String(255), db.ForeignKey('users.id')) + + # Relationships + permissions = db.relationship('UserDataSourcePermission', backref='data_source', lazy=True) + user = db.relationship('User', backref='created_data_sources', lazy=True) + + def __repr__(self): + return f'' + +class UserDataSourcePermission(db.Model): + __tablename__ = 'user_data_source_permissions' + id = db.Column(db.Integer, primary_key=True) # Auto-incrementing primary key + user_id = db.Column(db.String(255), db.ForeignKey('users.id'), nullable=False) + data_source_id = db.Column(db.Integer, db.ForeignKey('data_sources.id'), nullable=False) + permission_type = db.Column(db.String(50)) # Could denote read, write, admin, etc. + assigned_at = db.Column(db.DateTime, default=datetime.utcnow) + + # Relationships + user = db.relationship('User', backref='user_permissions_backref', lazy=True) + + def __repr__(self): + return f'' + +# Assuming you will have a User model that might look something like this: +class User(db.Model): + __tablename__ = 'users' + id = db.Column(db.String(255), primary_key=True) # Azure AD User Object ID + name = db.Column(db.String(100), nullable=False) + email = db.Column(db.String(100), nullable=False) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + + permissions = db.relationship('UserDataSourcePermission', backref='user_info_backref', lazy=True) + def __repr__(self): + return f'' diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..6d1f230 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,10 @@ + + + + + {% block title %}{% endblock %} + + + {% block content %}{% endblock %} + + diff --git a/templates/create_data_source.html b/templates/create_data_source.html new file mode 100644 index 0000000..68d4ef6 --- /dev/null +++ b/templates/create_data_source.html @@ -0,0 +1,32 @@ + + +{% extends "base.html" %} + +{% block content %} +

Create Data Source

+
+ {{ form.hidden_tag() }} +

+ {{ form.name.label }}
+ {{ form.name(size=20) }}
+ {% for error in form.name.errors %} + [{{ error }}] + {% endfor %} +

+

+ {{ form.description.label }}
+ {{ form.description(rows=3, cols=40) }}
+ {% for error in form.description.errors %} + [{{ error }}] + {% endfor %} +

+

+ {{ form.aws_resource_arn.label }}
+ {{ form.aws_resource_arn(size=40) }}
+ {% for error in form.aws_resource_arn.errors %} + [{{ error }}] + {% endfor %} +

+

{{ form.submit() }}

+
+{% endblock %} diff --git a/templates/dashboard.html b/templates/dashboard.html index c9c5186..2f0cd90 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -4,16 +4,27 @@ Dashboard +

Dashboard

-

Welcome, {{ user_info.displayName }}!

+

Welcome, {{ user_info.displayName }}:{{ user_role }}!

+ + + {% if user_role == 'admin' %} + + {% endif %} +

Your Groups

    {% for group in groups %}
  • {{ group.displayName }}
  • {% endfor %}
+ View Data Sources Logout diff --git a/templates/data_sources.html b/templates/data_sources.html new file mode 100644 index 0000000..31556b7 --- /dev/null +++ b/templates/data_sources.html @@ -0,0 +1,37 @@ + + + + + Data Sources + + + + +

Data Sources

+ + + + + + + + + + + + {% for data_source in data_sources %} + + + + + + + {% endfor %} + +
NameDescriptionCreated ByTime Created
{{ data_source.name }}{{ data_source.description }}{{ data_source.created_by }}{{ data_source.created_at }}
+ + +Back to Dashboard + + + From 41b81aaf12b3391135e5945a349e31a6f29fb371 Mon Sep 17 00:00:00 2001 From: julialawrence Date: Wed, 18 Oct 2023 08:24:49 +0100 Subject: [PATCH 3/7] Create the AAD group on datasource creation and assign admin --- .gitignore | 6 +- app.py | 150 ++++++++++++++++++++++++-------------- azure_active_directory.py | 93 +++++++++++++++++++++++ forms.py | 9 ++- models.py | 56 +++++++++----- secrets.json.tmpl | 6 ++ 6 files changed, 243 insertions(+), 77 deletions(-) create mode 100644 azure_active_directory.py create mode 100644 secrets.json.tmpl diff --git a/.gitignore b/.gitignore index fb04179..f38abb9 100644 --- a/.gitignore +++ b/.gitignore @@ -150,6 +150,9 @@ dmypy.json # pytype static type analyzer .pytype/ +# Flask +**/**flask_session + # Cython debug symbols cython_debug/ @@ -164,5 +167,4 @@ cython_debug/ secrets.json # IDE - -**.*vscode \ No newline at end of file +**/*.vscode \ No newline at end of file diff --git a/app.py b/app.py index 6c191ff..3469e17 100644 --- a/app.py +++ b/app.py @@ -1,55 +1,72 @@ from flask import Flask, redirect, url_for, session, render_template, flash +from flask_session import Session from authlib.integrations.flask_client import OAuth import os import requests import json from models import init_app, db, DataSource, UserDataSourcePermission, User -from forms import DataSourceForm # Import the form you created +from forms import DataSourceForm +from azure_active_directory import create_aad_group # Load secrets from a JSON file -with open('secrets.json') as f: +with open("secrets.json") as f: secrets = json.load(f) + + # Initialize the Flask application app = Flask(__name__) -app.secret_key = 'development' # Change this to a random secret key +app.config['SECRET_KEY'] = secrets["session_secret"] +app.config['SESSION_TYPE'] = 'filesystem' # Initialize database init_app(app) +# Initialize the session extension +Session(app) + + # Azure AD OAuth configuration oauth = OAuth(app) azure = oauth.register( - 'azure', - client_id=secrets['client_id'], - client_secret=secrets['client_secret'], + "azure", + client_id=secrets["client_id"], + client_secret=secrets["client_secret"], server_metadata_url=f'https://login.microsoftonline.com/{secrets["tenant_id"]}/v2.0/.well-known/openid-configuration', client_kwargs={ - 'scope': 'openid email profile User.ReadWrite.All Group.ReadWrite.All offline_access', + "scope": "openid email profile User.ReadWrite.All Group.ReadWrite.All offline_access", }, ) -@app.route('/') + +@app.route("/") def homepage(): - user_info = session.get('user') - groups = session.get('groups') - user_role = session.get('user_role', 'user') + user_info = session.get("user") + groups = session.get("groups") + user_role = session.get("user_role", "user") if user_info: # User is logged in, render the dashboard - return render_template('dashboard.html', user_info=user_info, groups=groups, user_role=user_role) + return render_template( + "dashboard.html", user_info=user_info, groups=groups, user_role=user_role + ) # For logged-out users, render the homepage - return render_template('homepage.html') + return render_template("homepage.html") + -@app.route('/login') +@app.route("/login") def login(): - redirect_uri = url_for('authorized', _external=True) + redirect_uri = url_for("authorized", _external=True) return azure.authorize_redirect(redirect_uri) -@app.route('/login/authorized') + +@app.route("/login/authorized") def authorized(): token = azure.authorize_access_token() - user_resp = azure.get('https://graph.microsoft.com/v1.0/me', token=token) + access_token = token.get('access_token') + if access_token: + session['access_token'] = access_token + user_resp = azure.get("https://graph.microsoft.com/v1.0/me", token=token) user_info = user_resp.json() # Debug: Print the whole response to see all available fields @@ -57,9 +74,13 @@ def authorized(): print(json.dumps(user_info, indent=4)) # Pretty-print the JSON response # Extract user information from the response - user_id = user_info.get('id') # Adjust if the ID is under a different key in the response - user_name = user_info.get('displayName') # Or appropriate field for the user's name - user_email = user_info.get('userPrincipalName') # Or appropriate field for the user's email + user_id = user_info.get( + "id" + ) # Adjust if the ID is under a different key in the response + user_name = user_info.get("displayName") # Or appropriate field for the user's name + user_email = user_info.get( + "userPrincipalName" + ) # Or appropriate field for the user's email # Check if the user exists in the database user = User.query.get(user_id) @@ -76,11 +97,14 @@ def authorized(): print(f"User {user_name} added to the database.") # Store user info in session - session['user'] = user_info + session["user"] = user_info + session["token"] = token # Attempt to get the user's group memberships try: - groups_resp = azure.get('https://graph.microsoft.com/v1.0/me/memberOf', token=token) + groups_resp = azure.get( + "https://graph.microsoft.com/v1.0/me/memberOf", token=token + ) groups_info = groups_resp.json() # Debug: Print the groups response @@ -88,7 +112,7 @@ def authorized(): print(json.dumps(groups_info, indent=4)) # Pretty-print the JSON response # Assuming the response contains an array of group objects - session['groups'] = groups_info.get('value', []) + session["groups"] = groups_info.get("value", []) except Exception as e: # If the group request fails, print out why @@ -96,44 +120,57 @@ def authorized(): print(e) # Determine if the user is an admin - is_admin = check_if_user_is_admin(session.get('groups',[])) + is_admin = check_if_user_is_admin(session.get("groups", [])) # Store role in session - session['user_role'] = 'admin' if is_admin else 'user' # It seems there was a typo here, setting 'user' for non-admins + session["user_role"] = ( + "admin" if is_admin else "user" + ) # It seems there was a typo here, setting 'user' for non-admins - return redirect('/') # Redirect to the homepage + return redirect("/") # Redirect to the homepage -@app.route('/datasource/create', methods=['GET', 'POST']) +@app.route("/datasource/create", methods=["GET", "POST"]) def create_data_source(): form = DataSourceForm() + # token = azure.authorize_access_token() if form.validate_on_submit(): - current_user_id = session['user']['id'] + current_user_id = session["user"]["id"] # Create a new data source instance data_source = DataSource( name=form.name.data, description=form.description.data, - aws_resource_arn=form.aws_resource_arn.data, # This field is for the AWS ARN - created_by=current_user_id - + aws_resource_arn=form.aws_resource_arn.data, + created_by=current_user_id, ) # Add to the database session and commit db.session.add(data_source) db.session.commit() - # Here, you should trigger the AAD group creation and store the returned group ID - # For now, we'll simulate this with a placeholder - # data_source.aad_group_id = "dummy-aad-group-id" # Placeholder for the AAD group ID - # db.session.commit() + group_name = f"data_platform_datasource_{form.name.data}_{data_source.id}" + + # Create the group in Azure AD + aad_group_id = create_aad_group(group_name=group_name, description=form.name.data, access_token=session.get("token").get("access_token"), user_id=current_user_id, dry_run=False) + + if aad_group_id: + data_source.aad_group_id = aad_group_id # Save the new AAD group ID + db.session.commit() + + flash('Data source and associated AAD group created successfully!', 'success') + else: + flash('Failed to create AAD group.', 'error') - flash('Data source created successfully!', 'success') - return redirect(url_for('list_data_sources')) # Redirect to the homepage or list of data sources + flash("Data source created successfully!", "success") + return redirect( + url_for("list_data_sources") + ) # Redirect to the homepage or list of data sources # If the form is not submitted or not valid, render the form page - return render_template('create_data_source.html', form=form) + return render_template("create_data_source.html", form=form) -@app.route('/datasources') + +@app.route("/datasources") def list_data_sources(): # Query all data sources from the database data_sources = DataSource.query.all() @@ -142,23 +179,27 @@ def list_data_sources(): data_sources_info = [] for data_source in data_sources: info = { - 'name': data_source.name, - 'description': data_source.description, - 'created_by': User.query.get(data_source.created_by).name if data_source.created_by else 'N/A', # Assuming 'created_by' is a field in your DataSource model - 'created_at': data_source.created_at.strftime('%Y-%m-%d %H:%M:%S') # Format the date as you prefer + "name": data_source.name, + "description": data_source.description, + "created_by": User.query.get(data_source.created_by).name + if data_source.created_by + else "N/A", # Assuming 'created_by' is a field in your DataSource model + "created_at": data_source.created_at.strftime( + "%Y-%m-%d %H:%M:%S" + ), # Format the date as you prefer } data_sources_info.append(info) # Pass the list of dictionaries to the template - return render_template('data_sources.html', data_sources=data_sources_info) - + return render_template("data_sources.html", data_sources=data_sources_info) -@app.route('/logout') +@app.route("/logout") def logout(): - session.pop('user', None) - session.pop('groups', None) - return redirect('/') + session.pop("user", None) + session.pop("groups", None) + return redirect("/") + def check_if_user_is_admin(group_ids): admin_group_name = "data-platform-single-ui-group" # This should be the actual ID of your admin group in Azure AD @@ -166,16 +207,19 @@ def check_if_user_is_admin(group_ids): # Debugging: Print the group information to the console for verification print("\nDebugging Info: Groups associated with the user:") for group in group_ids: - print(group.get('displayName')) + print(group.get("displayName")) - # Extracting the 'displayName' from each group and checking if 'admin_group_name' is one of them - is_user_admin = any(group.get('displayName') == admin_group_name for group in group_ids) + # Extracting the 'displayName' from each group and checking if 'admin_group_name' is one of them + is_user_admin = any( + group.get("displayName") == admin_group_name for group in group_ids + ) # Debugging: Print whether the user is an admin print(f"Is the user an admin: {is_user_admin}") return is_user_admin + # Run the Flask application if __name__ == "__main__": - app.run(host='127.0.0.1', port=5000) + app.run(host="127.0.0.1", port=5000) diff --git a/azure_active_directory.py b/azure_active_directory.py new file mode 100644 index 0000000..9b96d12 --- /dev/null +++ b/azure_active_directory.py @@ -0,0 +1,93 @@ +import requests +from flask import session, flash + +def create_aad_group(group_name, description, user_id, access_token, dry_run=False): + # Microsoft Graph API endpoint to create a new group + url = "https://graph.microsoft.com/v1.0/groups" + + # The headers for the request + headers = { + 'Authorization': f'Bearer {access_token}', + 'Content-Type': 'application/json' + } + + # The payload for the request + group_data = { + "displayName": group_name, + "description": description, + "mailEnabled": False, + "mailNickname": group_name.replace(" ", "").lower(), + "securityEnabled": True + } + + # If dry_run is enabled, we skip the actual creation process + if dry_run: + print("Dry run enabled. No group was actually created.") + print("Group data:", group_data) + return "mock_group_id" + + try: + # Make the request to create the group + response = requests.post(url, headers=headers, json=group_data) + response.raise_for_status() # Will raise an HTTPError if the HTTP request returned an unsuccessful status code + + # If the request was successful, get the JSON response + group_info = response.json() + + # Extract the id of the created group + group_id = group_info.get('id') + + # Now, add the user as an admin of the group + if group_id and user_id: + add_admin_status = add_user_as_group_admin(group_id, user_id, access_token) + if add_admin_status: + 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') + else: + flash('Group was created but user could not be added as an admin.', 'warning') + + return group_id + + except requests.exceptions.HTTPError as err: + # Handle errors (print them to console or log file, display message to user, etc.) + print(f"An HTTP error occurred: {err}") + flash('An error occurred while creating the group.', 'error') + except Exception as e: + # Handle any other exceptions + print(f"An unexpected error occurred: {e}") + flash('An unexpected error occurred while creating the group.', '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" + + # The headers for the request + headers = { + 'Authorization': f'Bearer {access_token}', + 'Content-Type': 'application/json' + } + + # The payload for the request + member_data = { + "@odata.id": f"https://graph.microsoft.com/v1.0/directoryObjects/{user_id}" + } + + try: + # Make the request to add the user to the group + response = requests.post(url, headers=headers, json=member_data) + response.raise_for_status() # Will raise an HTTPError if the HTTP request returned an unsuccessful status code + + # If the request was successful, return True + if response.status_code == 204: # 204 No Content response means success + return True + except requests.exceptions.HTTPError as err: + # Handle errors (print them to console or log file, display message to user, etc.) + print(f"An HTTP error occurred: {err}") + except Exception as e: + # Handle any other exceptions + print(f"An unexpected error occurred: {e}") + + return False diff --git a/forms.py b/forms.py index f8e2132..486273e 100644 --- a/forms.py +++ b/forms.py @@ -2,8 +2,9 @@ from wtforms import StringField, TextAreaField, SubmitField from wtforms.validators import DataRequired + class DataSourceForm(FlaskForm): - name = StringField('Data Source Name', validators=[DataRequired()]) - description = TextAreaField('Description') # Not mandatory - aws_resource_arn = StringField('AWS Resource ARN', validators=[DataRequired()]) - submit = SubmitField('Create') + name = StringField("Data Source Name", validators=[DataRequired()]) + description = TextAreaField("Description") # Not mandatory + aws_resource_arn = StringField("AWS Resource ARN", validators=[DataRequired()]) + submit = SubmitField("Create") diff --git a/models.py b/models.py index 868c9d1..1f60a9d 100644 --- a/models.py +++ b/models.py @@ -4,53 +4,73 @@ db = SQLAlchemy() + def init_app(app): - app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///data_access_manager.db' # Ensure this is the correct path - app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False # Optional: Disable event system if not needed + app.config[ + "SQLALCHEMY_DATABASE_URI" + ] = "sqlite:///data_access_manager.db" + app.config[ + "SQLALCHEMY_TRACK_MODIFICATIONS" + ] = False # Optional: Disable event system if not needed db.init_app(app) with app.app_context(): - db.create_all() # This command should create the database file + db.create_all() + class DataSource(db.Model): - __tablename__ = 'data_sources' + __tablename__ = "data_sources" id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(100), nullable=False) description = db.Column(db.String(255)) - aws_resource_arn = db.Column(db.String(255)) # If applicable, ARN for the corresponding AWS resource + aws_resource_arn = db.Column( + db.String(255), unique=True + ) # If applicable, ARN for the corresponding AWS resource created_at = db.Column(db.DateTime, default=datetime.utcnow) - updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) - created_by = db.Column(db.String(255), db.ForeignKey('users.id')) + updated_at = db.Column( + db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow + ) + created_by = db.Column(db.String(255), db.ForeignKey("users.id")) + aad_group_id = db.Column(db.String(255), unique=True) # Relationships - permissions = db.relationship('UserDataSourcePermission', backref='data_source', lazy=True) - user = db.relationship('User', backref='created_data_sources', lazy=True) + permissions = db.relationship( + "UserDataSourcePermission", backref="data_source", lazy=True + ) + user = db.relationship("User", backref="created_data_sources", lazy=True) def __repr__(self): - return f'' + return f"" + class UserDataSourcePermission(db.Model): - __tablename__ = 'user_data_source_permissions' + __tablename__ = "user_data_source_permissions" id = db.Column(db.Integer, primary_key=True) # Auto-incrementing primary key - user_id = db.Column(db.String(255), db.ForeignKey('users.id'), nullable=False) - data_source_id = db.Column(db.Integer, db.ForeignKey('data_sources.id'), nullable=False) + user_id = db.Column(db.String(255), db.ForeignKey("users.id"), nullable=False) + data_source_id = db.Column( + db.Integer, db.ForeignKey("data_sources.id"), nullable=False + ) permission_type = db.Column(db.String(50)) # Could denote read, write, admin, etc. assigned_at = db.Column(db.DateTime, default=datetime.utcnow) # Relationships - user = db.relationship('User', backref='user_permissions_backref', lazy=True) + user = db.relationship("User", backref="user_permissions_backref", lazy=True) def __repr__(self): - return f'' + return f"" + # Assuming you will have a User model that might look something like this: class User(db.Model): - __tablename__ = 'users' + __tablename__ = "users" id = db.Column(db.String(255), primary_key=True) # Azure AD User Object ID name = db.Column(db.String(100), nullable=False) email = db.Column(db.String(100), nullable=False) created_at = db.Column(db.DateTime, default=datetime.utcnow) - permissions = db.relationship('UserDataSourcePermission', backref='user_info_backref', lazy=True) + permissions = db.relationship( + "UserDataSourcePermission", backref="user_info_backref", lazy=True + ) + def __repr__(self): - return f'' + return f"" diff --git a/secrets.json.tmpl b/secrets.json.tmpl new file mode 100644 index 0000000..1bb6dac --- /dev/null +++ b/secrets.json.tmpl @@ -0,0 +1,6 @@ +{ + "client_id": "", + "client_secret": "", + "tenant_id": "", + "session_secret": "" +} From bf0029d416c3ccf626b93d1d0b99cc2ed5305796 Mon Sep 17 00:00:00 2001 From: julialawrence Date: Wed, 18 Oct 2023 08:25:22 +0100 Subject: [PATCH 4/7] Formatting. --- app.py | 23 +++++++++++++++-------- azure_active_directory.py | 26 +++++++++++++++----------- models.py | 4 +--- 3 files changed, 31 insertions(+), 22 deletions(-) diff --git a/app.py b/app.py index 3469e17..77de54b 100644 --- a/app.py +++ b/app.py @@ -14,11 +14,10 @@ secrets = json.load(f) - # Initialize the Flask application app = Flask(__name__) -app.config['SECRET_KEY'] = secrets["session_secret"] -app.config['SESSION_TYPE'] = 'filesystem' +app.config["SECRET_KEY"] = secrets["session_secret"] +app.config["SESSION_TYPE"] = "filesystem" # Initialize database init_app(app) @@ -63,9 +62,9 @@ def login(): @app.route("/login/authorized") def authorized(): token = azure.authorize_access_token() - access_token = token.get('access_token') + access_token = token.get("access_token") if access_token: - session['access_token'] = access_token + session["access_token"] = access_token user_resp = azure.get("https://graph.microsoft.com/v1.0/me", token=token) user_info = user_resp.json() @@ -151,15 +150,23 @@ def create_data_source(): group_name = f"data_platform_datasource_{form.name.data}_{data_source.id}" # Create the group in Azure AD - aad_group_id = create_aad_group(group_name=group_name, description=form.name.data, access_token=session.get("token").get("access_token"), user_id=current_user_id, dry_run=False) + aad_group_id = create_aad_group( + group_name=group_name, + description=form.name.data, + access_token=session.get("token").get("access_token"), + user_id=current_user_id, + dry_run=False, + ) if aad_group_id: data_source.aad_group_id = aad_group_id # Save the new AAD group ID db.session.commit() - flash('Data source and associated AAD group created successfully!', 'success') + flash( + "Data source and associated AAD group created successfully!", "success" + ) else: - flash('Failed to create AAD group.', 'error') + flash("Failed to create AAD group.", "error") flash("Data source created successfully!", "success") return redirect( diff --git a/azure_active_directory.py b/azure_active_directory.py index 9b96d12..431328f 100644 --- a/azure_active_directory.py +++ b/azure_active_directory.py @@ -1,14 +1,15 @@ import requests from flask import session, flash + def create_aad_group(group_name, description, user_id, access_token, dry_run=False): # Microsoft Graph API endpoint to create a new group url = "https://graph.microsoft.com/v1.0/groups" # The headers for the request headers = { - 'Authorization': f'Bearer {access_token}', - 'Content-Type': 'application/json' + "Authorization": f"Bearer {access_token}", + "Content-Type": "application/json", } # The payload for the request @@ -17,7 +18,7 @@ def create_aad_group(group_name, description, user_id, access_token, dry_run=Fal "description": description, "mailEnabled": False, "mailNickname": group_name.replace(" ", "").lower(), - "securityEnabled": True + "securityEnabled": True, } # If dry_run is enabled, we skip the actual creation process @@ -35,39 +36,42 @@ def create_aad_group(group_name, description, user_id, access_token, dry_run=Fal group_info = response.json() # Extract the id of the created group - group_id = group_info.get('id') + group_id = group_info.get("id") # Now, add the user as an admin of the group if group_id and user_id: add_admin_status = add_user_as_group_admin(group_id, user_id, access_token) if add_admin_status: - flash('User added as an admin to the group successfully!', 'success') + 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') + flash("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') + flash( + "Group was created but user could not be added as an admin.", "warning" + ) return group_id except requests.exceptions.HTTPError as err: # Handle errors (print them to console or log file, display message to user, etc.) print(f"An HTTP error occurred: {err}") - flash('An error occurred while creating the group.', 'error') + flash("An error occurred while creating the group.", "error") except Exception as e: # Handle any other exceptions print(f"An unexpected error occurred: {e}") - flash('An unexpected error occurred while creating the group.', 'error') + flash("An unexpected error occurred while creating the group.", "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" # The headers for the request headers = { - 'Authorization': f'Bearer {access_token}', - 'Content-Type': 'application/json' + "Authorization": f"Bearer {access_token}", + "Content-Type": "application/json", } # The payload for the request diff --git a/models.py b/models.py index 1f60a9d..ea268dd 100644 --- a/models.py +++ b/models.py @@ -6,9 +6,7 @@ def init_app(app): - app.config[ - "SQLALCHEMY_DATABASE_URI" - ] = "sqlite:///data_access_manager.db" + app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///data_access_manager.db" app.config[ "SQLALCHEMY_TRACK_MODIFICATIONS" ] = False # Optional: Disable event system if not needed From d85ef4842482619d8bc5bae02d6eb25ecafa5dbe Mon Sep 17 00:00:00 2001 From: julialawrence Date: Thu, 19 Oct 2023 07:34:38 +0100 Subject: [PATCH 5/7] Implement user management and change structure to make dockerizing easier. --- app/README.md | 1 + app.py => app/app.py | 140 +++++++++++++++++- .../azure_active_directory.py | 53 ++++++- forms.py => app/forms.py | 0 models.py => app/models.py | 6 +- {templates => app/templates}/base.html | 0 .../templates}/create_data_source.html | 0 {templates => app/templates}/dashboard.html | 0 app/templates/data_source_details.html | 45 ++++++ .../templates}/data_sources.html | 10 +- {templates => app/templates}/homepage.html | 0 app/templates/manage_users.html | 18 +++ 12 files changed, 261 insertions(+), 12 deletions(-) create mode 100644 app/README.md rename app.py => app/app.py (58%) rename azure_active_directory.py => app/azure_active_directory.py (66%) rename forms.py => app/forms.py (100%) rename models.py => app/models.py (92%) rename {templates => app/templates}/base.html (100%) rename {templates => app/templates}/create_data_source.html (100%) rename {templates => app/templates}/dashboard.html (100%) create mode 100644 app/templates/data_source_details.html rename {templates => app/templates}/data_sources.html (64%) rename {templates => app/templates}/homepage.html (100%) create mode 100644 app/templates/manage_users.html diff --git a/app/README.md b/app/README.md new file mode 100644 index 0000000..471a226 --- /dev/null +++ b/app/README.md @@ -0,0 +1 @@ +# data-platform-django-proof-of-concept \ No newline at end of file diff --git a/app.py b/app/app.py similarity index 58% rename from app.py rename to app/app.py index 77de54b..68c3033 100644 --- a/app.py +++ b/app/app.py @@ -1,4 +1,4 @@ -from flask import Flask, redirect, url_for, session, render_template, flash +from flask import Flask, redirect, request, url_for, session, render_template, flash from flask_session import Session from authlib.integrations.flask_client import OAuth import os @@ -6,7 +6,7 @@ import json from models import init_app, db, DataSource, UserDataSourcePermission, User from forms import DataSourceForm -from azure_active_directory import create_aad_group +from azure_active_directory import create_aad_group, add_users_to_aad_group # Load secrets from a JSON file @@ -42,12 +42,25 @@ @app.route("/") def homepage(): user_info = session.get("user") - groups = session.get("groups") user_role = session.get("user_role", "user") if user_info: + try: + # Fetch the latest group memberships from Azure AD + groups_resp = azure.get( + "https://graph.microsoft.com/v1.0/me/memberOf", + token=session.get("token"), + ) + groups_info = groups_resp.json() + session["groups"] = groups_info.get("value", []) + except Exception as e: + # Handle exceptions from the API call + print(f"Failed to refresh group memberships: {e}") # User is logged in, render the dashboard return render_template( - "dashboard.html", user_info=user_info, groups=groups, user_role=user_role + "dashboard.html", + user_info=user_info, + groups=session.get("groups", []), + user_role=user_role, ) # For logged-out users, render the homepage return render_template("homepage.html") @@ -181,11 +194,15 @@ def create_data_source(): def list_data_sources(): # Query all data sources from the database data_sources = DataSource.query.all() - + print("\nDebugging Info: Contents of data_sources:") + for ds in data_sources: + print(vars(ds)) + print("\n") # Create a list of dictionaries containing the data you want to display data_sources_info = [] for data_source in data_sources: info = { + "id": data_source.id, "name": data_source.name, "description": data_source.description, "created_by": User.query.get(data_source.created_by).name @@ -201,6 +218,119 @@ def list_data_sources(): return render_template("data_sources.html", data_sources=data_sources_info) +@app.route("/datasource/") +def data_source_details(id): + # Fetch the data source from the database + data_source = DataSource.query.get_or_404(id) + + # Fetch the creator of the data source + creator = User.query.get(data_source.created_by) + + # Fetch the users assigned to this data source through permissions + # This assumes that your UserDataSourcePermission model links back to the User model + permissions = UserDataSourcePermission.query.filter_by( + data_source_id=data_source.id + ).all() + assigned_users = [permission.user for permission in permissions] + + # Check if the current user is the admin of the data source + current_user_id = session.get("user")["id"] + is_admin = current_user_id == data_source.created_by + # Render the template with the necessary information + return render_template( + "data_source_details.html", + data_source=data_source, + creator=creator, + assigned_users=assigned_users, + user=creator, + is_admin=is_admin, + ) + + +@app.route("/datasource//manage_users", methods=["GET", "POST"]) +def manage_users(id): + data_source = DataSource.query.get_or_404(id) + + current_user_id = session.get("user")["id"] + current_user = User.query.get(current_user_id) + + if not current_user: # Adjust as necessary for your permissions model + flash("You do not have permission to manage this data source.", "error") + return redirect(url_for("homepage")) # or wherever you'd like to redirect + + if request.method == "POST": + user_ids = request.form.getlist("users") + + # Assign users to the data source with appropriate permissions + for user_id in user_ids: + # Prevent adding the admin as a member again + if user_id == data_source.created_by: + continue # Skip the admin user + + # Check if the user already has access + existing_permission = UserDataSourcePermission.query.filter_by( + user_id=user_id, data_source_id=data_source.id + ).first() + if existing_permission: + continue + user = User.query.get(user_id) + if user: + permission = UserDataSourcePermission( + user_id=user.id, + data_source_id=data_source.id, + permission_type="read", + ) + db.session.add(permission) + + db.session.commit() + + # After assigning users to the data source, add them to the corresponding AAD group + aad_group_id = ( + data_source.aad_group_id + ) # The ID of the AAD group associated with the data source + if aad_group_id: + successful_additions, failed_additions = add_users_to_aad_group( + user_ids, aad_group_id, session.get("access_token") + ) + + if failed_additions: + flash( + f"Failed to add users {', '.join(failed_additions)} to the AAD group.", + "error", + ) + if successful_additions: + flash( + f"Successfully added users {', '.join(successful_additions)} to the AAD group.", + "success", + ) + else: + flash("No associated AAD group found for this data source.", "error") + + flash("Users successfully assigned!", "success") + return redirect(url_for("data_source_details", id=id)) # or appropriate route + + else: + all_users = User.query.all() + + # Get the IDs of users who already have permissions for this data source + existing_permissions = UserDataSourcePermission.query.filter_by( + data_source_id=data_source.id + ).all() + users_with_permissions = {perm.user_id for perm in existing_permissions} + + # Exclude the admin and users with existing permissions from the list + selectable_users = [ + user + for user in all_users + if user.id != data_source.created_by + and user.id not in users_with_permissions + ] + + return render_template( + "manage_users.html", data_source=data_source, users=selectable_users + ) + + @app.route("/logout") def logout(): session.pop("user", None) diff --git a/azure_active_directory.py b/app/azure_active_directory.py similarity index 66% rename from azure_active_directory.py rename to app/azure_active_directory.py index 431328f..cdef77d 100644 --- a/azure_active_directory.py +++ b/app/azure_active_directory.py @@ -1,5 +1,5 @@ import requests -from flask import session, flash +from flask import flash def create_aad_group(group_name, description, user_id, access_token, dry_run=False): @@ -79,6 +79,38 @@ def add_user_as_group_admin(group_id, user_id, access_token): "@odata.id": f"https://graph.microsoft.com/v1.0/directoryObjects/{user_id}" } + try: + # Make the request to add the user to the group + response = requests.post(url, headers=headers, json=member_data) + response.raise_for_status() # Will raise an HTTPError if the HTTP request returned an unsuccessful status code + + # If the request was successful, return True + if response.status_code == 204: # 204 No Content response means success + return True + except requests.exceptions.HTTPError as err: + print(f"An HTTP error occurred: {err}") + except Exception as e: + # Handle any other exceptions + print(f"An unexpected error occurred: {e}") + + return False + + +def add_user_to_aad_group(user_id, group_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" + + # The headers for the request + headers = { + "Authorization": f"Bearer {access_token}", + "Content-Type": "application/json", + } + + # The payload for the request + member_data = { + "@odata.id": f"https://graph.microsoft.com/v1.0/directoryObjects/{user_id}" + } + try: # Make the request to add the user to the group response = requests.post(url, headers=headers, json=member_data) @@ -90,8 +122,27 @@ def add_user_as_group_admin(group_id, user_id, access_token): except requests.exceptions.HTTPError as err: # Handle errors (print them to console or log file, display message to user, etc.) print(f"An HTTP error occurred: {err}") + print("Response body:", err.response.text) + flash("An error occurred while adding the user to the group.", "error") except Exception as e: # Handle any other exceptions print(f"An unexpected error occurred: {e}") + flash( + "An unexpected error occurred while adding the user to the group.", "error" + ) return False + + +def add_users_to_aad_group(user_ids, group_id, access_token): + successful_additions = [] + failed_additions = [] + + for user_id in user_ids: + success = add_user_to_aad_group(user_id, group_id, access_token) + if success: + successful_additions.append(user_id) + else: + failed_additions.append(user_id) + + return successful_additions, failed_additions diff --git a/forms.py b/app/forms.py similarity index 100% rename from forms.py rename to app/forms.py diff --git a/models.py b/app/models.py similarity index 92% rename from models.py rename to app/models.py index ea268dd..533acfd 100644 --- a/models.py +++ b/app/models.py @@ -52,7 +52,7 @@ class UserDataSourcePermission(db.Model): assigned_at = db.Column(db.DateTime, default=datetime.utcnow) # Relationships - user = db.relationship("User", backref="user_permissions_backref", lazy=True) + user = db.relationship("User", back_populates="permissions") def __repr__(self): return f"" @@ -66,9 +66,7 @@ class User(db.Model): email = db.Column(db.String(100), nullable=False) created_at = db.Column(db.DateTime, default=datetime.utcnow) - permissions = db.relationship( - "UserDataSourcePermission", backref="user_info_backref", lazy=True - ) + permissions = db.relationship("UserDataSourcePermission", back_populates="user") def __repr__(self): return f"" diff --git a/templates/base.html b/app/templates/base.html similarity index 100% rename from templates/base.html rename to app/templates/base.html diff --git a/templates/create_data_source.html b/app/templates/create_data_source.html similarity index 100% rename from templates/create_data_source.html rename to app/templates/create_data_source.html diff --git a/templates/dashboard.html b/app/templates/dashboard.html similarity index 100% rename from templates/dashboard.html rename to app/templates/dashboard.html diff --git a/app/templates/data_source_details.html b/app/templates/data_source_details.html new file mode 100644 index 0000000..beae3f7 --- /dev/null +++ b/app/templates/data_source_details.html @@ -0,0 +1,45 @@ + + + + + Data Source Details + + + + +

Data Source Details

+ + +
+

{{ data_source.name }}

+

Description: {{ data_source.description }}

+

Created At: {{ data_source.created_at.strftime('%Y-%m-%d %H:%M:%S') }}

+

Created By: {{ creator.name }}

+
+ + +
+

Assigned Users:

+ {% if assigned_users %} +
    + {% for user in assigned_users %} +
  • {{ user.name }} ({{ user.email }})
  • + {% endfor %} +
+ {% else %} +

No users assigned to this data source.

+ {% endif %} +
+ + +
+ {% if is_admin %} + Manage Users +{% endif %} +
+ + +Back to Data Sources List + + + diff --git a/templates/data_sources.html b/app/templates/data_sources.html similarity index 64% rename from templates/data_sources.html rename to app/templates/data_sources.html index 31556b7..5cdabc6 100644 --- a/templates/data_sources.html +++ b/app/templates/data_sources.html @@ -3,12 +3,13 @@ Data Sources - +

Data Sources

+ @@ -16,6 +17,7 @@

Data Sources

+ @@ -25,12 +27,16 @@

Data Sources

+ {% endfor %}
Description Created By Time CreatedActions
{{ data_source.description }} {{ data_source.created_by }} {{ data_source.created_at }} + + View Details +
- + Back to Dashboard diff --git a/templates/homepage.html b/app/templates/homepage.html similarity index 100% rename from templates/homepage.html rename to app/templates/homepage.html diff --git a/app/templates/manage_users.html b/app/templates/manage_users.html new file mode 100644 index 0000000..7d50028 --- /dev/null +++ b/app/templates/manage_users.html @@ -0,0 +1,18 @@ + +{% extends "base.html" %} + +{% block content %} +

Manage Users for {{ data_source.name }}

+ +
+
+ + +
+ +
+{% endblock %} From 9f27490004f96ea08f6c191c3aeb8f5431e9e156 Mon Sep 17 00:00:00 2001 From: julialawrence Date: Thu, 19 Oct 2023 07:35:18 +0100 Subject: [PATCH 6/7] Adding requirements.txt --- requirements.txt | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..6d405de --- /dev/null +++ b/requirements.txt @@ -0,0 +1,26 @@ +Authlib==1.2.1 +blinker==1.6.3 +cachelib==0.10.2 +certifi==2023.7.22 +cffi==1.16.0 +charset-normalizer==3.3.0 +click==8.1.7 +cryptography==41.0.4 +Flask==3.0.0 +Flask-Session==0.5.0 +Flask-SQLAlchemy==3.1.1 +Flask-WTF==1.2.1 +greenlet==3.0.0 +idna==3.4 +itsdangerous==2.1.2 +Jinja2==3.1.2 +MarkupSafe==2.1.3 +oauthlib==2.1.0 +pycparser==2.21 +requests==2.31.0 +requests-oauthlib==1.1.0 +SQLAlchemy==2.0.22 +typing_extensions==4.8.0 +urllib3==2.0.6 +Werkzeug==3.0.0 +WTForms==3.1.0 From edb96bd1eafcb3539e1f400f3a3c810d8289ad8a Mon Sep 17 00:00:00 2001 From: julialawrence Date: Thu, 19 Oct 2023 07:44:55 +0100 Subject: [PATCH 7/7] Moving requirements file and adding build and push workflow. --- .dockerignore | 8 ++++++ .gthub/workflows/build-and-push.yml | 36 ++++++++++++++++++++++++ Dockerfile | 20 +++++++++++++ requirements.txt => app/requirements.txt | 0 4 files changed, 64 insertions(+) create mode 100644 .dockerignore create mode 100644 .gthub/workflows/build-and-push.yml create mode 100644 Dockerfile rename requirements.txt => app/requirements.txt (100%) diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..cee9a65 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +__pycache__ +*.pyc +*.pyo +*.pyd +.Python +env +pip-log.txt +pip-delete-this-directory.txt diff --git a/.gthub/workflows/build-and-push.yml b/.gthub/workflows/build-and-push.yml new file mode 100644 index 0000000..e148f4b --- /dev/null +++ b/.gthub/workflows/build-and-push.yml @@ -0,0 +1,36 @@ +name: Build and Push Docker Image + +on: + push: + tags: + - '*' + +jobs: + build-and-push: + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Login to GitHub Container Registry + run: echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin + + - name: Build and Push Docker Image + run: | + docker buildx create --use + docker buildx build \ + --platform linux/amd64,linux/arm64 \ + -t ghcr.io/${{ github.repository }}:${{ github.ref }} \ + -t ghcr.io/${{ github.repository }}:latest \ + -f Dockerfile . + docker buildx stop ${GITHUB_RUN_ID} + env: + DOCKER_CLI_AGGREGATE: 1 + + - name: Clean up + run: | + docker buildx rm --all diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ffe7002 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +# Use an official Python runtime as the base image +FROM python:3.8-slim + +# Set the working directory in the container +WORKDIR /app + +# Copy the current directory contents into the container at /app +COPY ./app /app + +# Install any needed packages specified in requirements.txt +RUN pip install --trusted-host pypi.python.org -r requirements.txt + +# Make port 80 available to the world outside this container +EXPOSE 80 + +# Define environment variable +ENV NAME DataAccessManager + +# Run app.py when the container launches +CMD ["python", "app.py"] diff --git a/requirements.txt b/app/requirements.txt similarity index 100% rename from requirements.txt rename to app/requirements.txt