Skip to content

Commit

Permalink
Merge pull request #227 from rsnyman/registration-page
Browse files Browse the repository at this point in the history
Authentication and Authorisation
  • Loading branch information
rsnyman committed Nov 10, 2021
2 parents 5c50400 + 589e5b8 commit 0724f95
Show file tree
Hide file tree
Showing 82 changed files with 9,156 additions and 5,239 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ frontend/public/version.json

# Settings
backend/settings.yaml
backend/settings.local.yaml

# pytest-ibutsu stuff
*.tar.gz
Expand Down
7 changes: 4 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ repos:
- id: reorder-python-imports
language_version: python3
- repo: https://github.com/ambv/black
rev: 21.9b0
rev: 21.10b0
hooks:
- id: black
args: [--safe, --quiet, --line-length, "100"]
Expand Down Expand Up @@ -34,11 +34,11 @@ repos:
- id: pyupgrade
language_version: python3
- repo: https://github.com/pre-commit/mirrors-eslint
rev: v8.0.0-1
rev: v7.32.0
hooks:
- id: eslint
additional_dependencies:
- eslint
- eslint@7
- eslint-plugin-react
- eslint-plugin-import
- "@babel/eslint-parser"
Expand All @@ -47,3 +47,4 @@ repos:
- "@babel/plugin-syntax-jsx"
- "@babel/preset-flow"
- "@babel/core"
- "react"
13 changes: 9 additions & 4 deletions backend/ibutsu_server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from flask import redirect
from flask import request
from flask_cors import CORS
from flask_mail import Mail
from ibutsu_server.auth import bcrypt
from ibutsu_server.db import upgrades
from ibutsu_server.db.base import db
Expand Down Expand Up @@ -41,10 +42,13 @@ def get_app(**extra_config):
config = app.app.config
config.setdefault("BCRYPT_LOG_ROUNDS", 20)
config.setdefault("SQLALCHEMY_TRACK_MODIFICATIONS", True)
settings_path = Path("./settings.yaml").resolve()
if settings_path.exists():
# If there's a config file, load it
config.from_mapping(yaml_load(settings_path.open()))
if hasattr(config, "from_file"):
config.from_file(str(Path("./settings.yaml").resolve()), yaml_load, silent=True)
else:
settings_path = Path("./settings.yaml").resolve()
if settings_path.exists():
# If there's a config file, load it
config.from_mapping(yaml_load(settings_path.open()))
# Now load config from environment variables
config.from_mapping(os.environ)
if config.get("POSTGRESQL_HOST") and config.get("POSTGRESQL_DATABASE"):
Expand All @@ -71,6 +75,7 @@ def get_app(**extra_config):
CORS(app.app)
db.init_app(app.app)
bcrypt.init_app(app.app)
Mail(app.app)

with app.app.app_context():
db.create_all()
Expand Down
26 changes: 25 additions & 1 deletion backend/ibutsu_server/__main__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,29 @@
#!/usr/bin/env python3
import sys
from pathlib import Path

from ibutsu_server import get_app

SSL_CERT = Path("../certs/dev.ibutsu.org+2.pem")
SSL_KEY = Path("../certs/dev.ibutsu.org+2-key.pem")


if __name__ == "__main__":
get_app().run(port=8080, debug=True)
kwargs = {}
if "--ssl" in sys.argv:
if not SSL_CERT.exists() or not SSL_CERT.exists():
print(
"SSL certificate and/or key files not found. Please download and install mkcert "
"and run the following command to generate these files:"
)
print("")
print(" mkcert dev.ibutsu.org devapi.ibutsu.org localhost")
print("")
print(
"Then place these two files in a `certs` directory in the top level directory of "
"the project (the same directory containing `backend` and `frontend`)."
)
sys.exit(1)
else:
kwargs["ssl_context"] = (SSL_CERT, SSL_KEY)
get_app().run(port=8080, debug=True, **kwargs)
2 changes: 2 additions & 0 deletions backend/ibutsu_server/auth.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from flask_bcrypt import Bcrypt

bcrypt = Bcrypt()

__all__ = ["bcrypt"]
27 changes: 27 additions & 0 deletions backend/ibutsu_server/constants.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,30 @@
OAUTH_CONFIG = {
"google": {
"scope": ["https://www.googleapis.com/auth/userinfo.profile"],
"auth_url": "https://accounts.google.com/o/oauth2/v2/auth",
"token_url": "https://oauth2.googleapis.com/token",
"user_url": "https://www.googleapis.com/oauth2/v1/userinfo?alt=json",
},
"github": {
"scope": ["user"],
"user_url": "https://api.github.com/user",
"auth_url": "https://github.com/login/oauth/authorize",
"token_url": "https://github.com/login/oauth/access_token",
},
"facebook": {
"scope": ["email", "public_profile"],
"auth_url": "https://www.facebook.com/v11.0/dialog/oauth",
"user_url": "",
"token_url": "https://graph.facebook.com/v11.0/oauth/access_token",
},
"gitlab": {
"scope": ["read_user"],
"sep": "+",
"user_url": "/api/v4/user",
"auth_url": "/oauth/authorize",
"token_url": "/oauth/token",
},
}
ALLOWED_TRUE_BOOLEANS = ["y", "t", "1"]
ARRAY_FIELDS = ["metadata.tags", "metadata.markers", "metadata.annotations"]
NUMERIC_FIELDS = [
Expand Down
44 changes: 33 additions & 11 deletions backend/ibutsu_server/controllers/artifact_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
from flask import make_response
from ibutsu_server.db.base import session
from ibutsu_server.db.models import Artifact
from ibutsu_server.db.models import Result
from ibutsu_server.db.models import User
from ibutsu_server.util.projects import add_user_filter
from ibutsu_server.util.projects import project_has_user
from ibutsu_server.util.uuid import validate_uuid


Expand All @@ -22,18 +26,21 @@ def _build_artifact_response(id_):
return artifact, response


def view_artifact(id_):
def view_artifact(id_, token_info=None, user=None):
"""Stream an artifact directly to the client/browser
:param id: ID of the artifact to download
:type id: str
:rtype: file
"""
return _build_artifact_response(id_)[1]
artifact, response = _build_artifact_response(id_)
if not project_has_user(artifact.result.project, user):
return "Forbidden", 403
return response


def download_artifact(id_):
def download_artifact(id_, token_info=None, user=None):
"""Download an artifact
:param id: ID of artifact to download
Expand All @@ -42,12 +49,14 @@ def download_artifact(id_):
:rtype: file
"""
artifact, response = _build_artifact_response(id_)
if not project_has_user(artifact.result.project, user):
return "Forbidden", 403
response.headers["Content-Disposition"] = "attachment; filename={}".format(artifact.filename)
return response


@validate_uuid
def get_artifact(id_):
def get_artifact(id_, token_info=None, user=None):
"""Return a single artifact
:param id: ID of the artifact
Expand All @@ -58,10 +67,14 @@ def get_artifact(id_):
artifact = Artifact.query.get(id_)
if not artifact:
return "Not Found", 404
if not project_has_user(artifact.result.project, user):
return "Forbidden", 403
return artifact.to_dict()


def get_artifact_list(result_id=None, run_id=None, page_size=25, page=1):
def get_artifact_list(
result_id=None, run_id=None, page_size=25, page=1, token_info=None, user=None
):
"""Get a list of artifact files for result
:param id: ID of test result
Expand All @@ -70,10 +83,15 @@ def get_artifact_list(result_id=None, run_id=None, page_size=25, page=1):
:rtype: List[Artifact]
"""
query = Artifact.query
user = User.query.get(user)
if "result_id" in connexion.request.args:
result_id = connexion.request.args["result_id"]
if result_id:
query = query.filter(Artifact.result_id == result_id)
if run_id:
query = query.filter(Artifact.run_id == run_id)
if user:
query = add_user_filter(query, user)
total_items = query.count()
offset = (page * page_size) - page_size
total_pages = (total_items // page_size) + (1 if total_items % page_size > 0 else 0)
Expand All @@ -89,7 +107,7 @@ def get_artifact_list(result_id=None, run_id=None, page_size=25, page=1):
}


def upload_artifact(body):
def upload_artifact(body, token_info=None, user=None):
"""Uploads a artifact artifact
:param result_id: ID of result to attach artifact to
Expand All @@ -107,6 +125,9 @@ def upload_artifact(body):
"""
result_id = body.get("result_id")
run_id = body.get("run_id")
result = Result.query.get(result_id)
if result and not project_has_user(result.project, user):
return "Forbidden", 403
filename = body.get("filename")
additional_metadata = body.get("additional_metadata", {})
file_ = connexion.request.files["file"]
Expand Down Expand Up @@ -150,7 +171,7 @@ def upload_artifact(body):
return artifact.to_dict(), 201


def delete_artifact(id_):
def delete_artifact(id_, token_info=None, user=None):
"""Deletes an artifact
:param id: ID of the artifact to delete
Expand All @@ -161,7 +182,8 @@ def delete_artifact(id_):
artifact = Artifact.query.get(id_)
if not artifact:
return "Not Found", 404
else:
session.delete(artifact)
session.commit()
return "OK", 200
if not project_has_user(artifact.result.project, user):
return "Forbidden", 403
session.delete(artifact)
session.commit()
return "OK", 200
51 changes: 33 additions & 18 deletions backend/ibutsu_server/controllers/dashboard_controller.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import connexion
from ibutsu_server.db.base import session
from ibutsu_server.db.models import Dashboard
from ibutsu_server.db.models import Project
from ibutsu_server.db.models import WidgetConfig
from ibutsu_server.util.projects import project_has_user
from ibutsu_server.util.uuid import validate_uuid


def add_dashboard(dashboard=None):
def add_dashboard(dashboard=None, token_info=None, user=None):
"""Create a dashboard
:param body: Dashboard
Expand All @@ -16,29 +18,31 @@ def add_dashboard(dashboard=None):
if not connexion.request.is_json:
return "Bad request, JSON required", 400
dashboard = Dashboard.from_dict(**connexion.request.get_json())
if dashboard.project_id and not project_has_user(dashboard.project_id, user):
return "Forbidden", 403
session.add(dashboard)
session.commit()
return dashboard.to_dict(), 201


@validate_uuid
def get_dashboard(id_):
def get_dashboard(id_, token_info=None, user=None):
"""Get a single dashboard by ID
:param id: ID of test dashboard
:type id: str
:rtype: Dashboard
"""
dashboard = Dashboard.query.filter(Dashboard.id == id_).first()
if not dashboard:
dashboard = Dashboard.query.get(id_)
dashboard = Dashboard.query.get(id_)
if not dashboard:
return "Dashboard not found", 404
if dashboard and dashboard.project and not project_has_user(dashboard.project, user):
return "Forbidden", 403
return dashboard.to_dict()


def get_dashboard_list(project_id=None, user_id=None, page=1, page_size=25):
def get_dashboard_list(project_id=None, page=1, page_size=25, token_info=None, user=None):
"""Get a list of dashboards
:param project_id: Filter dashboards by project ID
Expand All @@ -53,10 +57,13 @@ def get_dashboard_list(project_id=None, user_id=None, page=1, page_size=25):
:rtype: DashboardList
"""
query = Dashboard.query
if project_id:
project = None
if "project_id" in connexion.request.args:
project = Project.query.get(connexion.request.args["project_id"])
if project:
if not project_has_user(project, user):
return "Forbidden", 403
query = query.filter(Dashboard.project_id == project_id)
if user_id:
query = query.filter(Dashboard.user_id == user_id)
offset = (page * page_size) - page_size
total_items = query.count()
total_pages = (total_items // page_size) + (1 if total_items % page_size > 0 else 0)
Expand All @@ -72,7 +79,7 @@ def get_dashboard_list(project_id=None, user_id=None, page=1, page_size=25):
}


def update_dashboard(id_, dashboard=None):
def update_dashboard(id_, dashboard=None, token_info=None, user=None):
"""Update a dashboard
:param id: ID of test dashboard
Expand All @@ -84,16 +91,23 @@ def update_dashboard(id_, dashboard=None):
"""
if not connexion.request.is_json:
return "Bad request, JSON required", 400
dashboard_dict = connexion.request.get_json()
if dashboard_dict.get("metadata", {}).get("project") and not project_has_user(
dashboard_dict["metadata"]["project"], user
):
return "Forbidden", 403
dashboard = Dashboard.query.get(id_)
if not dashboard:
return "Dashboard not found", 404
if project_has_user(dashboard.project, user):
return "Forbidden", 403
dashboard.update(connexion.request.get_json())
session.add(dashboard)
session.commit()
return dashboard.to_dict()


def delete_dashboard(id_):
def delete_dashboard(id_, token_info=None, user=None):
"""Deletes a dashboard
:param id: ID of the dashboard to delete
Expand All @@ -104,10 +118,11 @@ def delete_dashboard(id_):
dashboard = Dashboard.query.get(id_)
if not dashboard:
return "Not Found", 404
else:
widget_configs = WidgetConfig.query.filter(WidgetConfig.dashboard_id == dashboard.id).all()
for widget_config in widget_configs:
session.delete(widget_config)
session.delete(dashboard)
session.commit()
return "OK", 200
if not project_has_user(dashboard.project, user):
return "Forbidden", 403
widget_configs = WidgetConfig.query.filter(WidgetConfig.dashboard_id == dashboard.id).all()
for widget_config in widget_configs:
session.delete(widget_config)
session.delete(dashboard)
session.commit()
return "OK", 200
Loading

0 comments on commit 0724f95

Please sign in to comment.