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

Authentication and Authorisation #227

Merged
merged 1 commit into from
Nov 10, 2021
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
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:
john-dupuy marked this conversation as resolved.
Show resolved Hide resolved
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