From f0c472b1792779e63fdeb97a470b46300de00050 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Tue, 26 Oct 2021 16:19:55 +0200 Subject: feat(web/api): Initial API implementation in Kotlin This change adds the initial implementation of the new API server in Kotlin, replacing the old API written in Python. The implementation uses Quarkus, RESTEasy, and Hibernate to implement the new API endpoints. The reason for replacing the old API server is unifying the build and deployment toolchains, reducing the number of technologies necessary to work with OpenDC. Furthermore, we envision bundling the entire OpenDC project into a single distributions, allowing users to launch their own deployment trivially. --- opendc-web/opendc-web-api/opendc/__init__.py | 0 opendc-web/opendc-web-api/opendc/api/__init__.py | 0 opendc-web/opendc-web-api/opendc/api/jobs.py | 105 --------- opendc-web/opendc-web-api/opendc/api/portfolios.py | 153 ------------- opendc-web/opendc-web-api/opendc/api/prefabs.py | 123 ----------- opendc-web/opendc-web-api/opendc/api/projects.py | 224 ------------------- opendc-web/opendc-web-api/opendc/api/scenarios.py | 86 -------- opendc-web/opendc-web-api/opendc/api/schedulers.py | 46 ---- opendc-web/opendc-web-api/opendc/api/topologies.py | 97 --------- opendc-web/opendc-web-api/opendc/api/traces.py | 51 ----- opendc-web/opendc-web-api/opendc/auth.py | 236 --------------------- opendc-web/opendc-web-api/opendc/database.py | 97 --------- opendc-web/opendc-web-api/opendc/exts.py | 91 -------- .../opendc-web-api/opendc/models/__init__.py | 0 opendc-web/opendc-web-api/opendc/models/model.py | 61 ------ .../opendc-web-api/opendc/models/portfolio.py | 47 ---- opendc-web/opendc-web-api/opendc/models/prefab.py | 31 --- opendc-web/opendc-web-api/opendc/models/project.py | 48 ----- .../opendc-web-api/opendc/models/scenario.py | 93 -------- .../opendc-web-api/opendc/models/topology.py | 108 ---------- opendc-web/opendc-web-api/opendc/models/trace.py | 16 -- opendc-web/opendc-web-api/opendc/util.py | 32 --- 22 files changed, 1745 deletions(-) delete mode 100644 opendc-web/opendc-web-api/opendc/__init__.py delete mode 100644 opendc-web/opendc-web-api/opendc/api/__init__.py delete mode 100644 opendc-web/opendc-web-api/opendc/api/jobs.py delete mode 100644 opendc-web/opendc-web-api/opendc/api/portfolios.py delete mode 100644 opendc-web/opendc-web-api/opendc/api/prefabs.py delete mode 100644 opendc-web/opendc-web-api/opendc/api/projects.py delete mode 100644 opendc-web/opendc-web-api/opendc/api/scenarios.py delete mode 100644 opendc-web/opendc-web-api/opendc/api/schedulers.py delete mode 100644 opendc-web/opendc-web-api/opendc/api/topologies.py delete mode 100644 opendc-web/opendc-web-api/opendc/api/traces.py delete mode 100644 opendc-web/opendc-web-api/opendc/auth.py delete mode 100644 opendc-web/opendc-web-api/opendc/database.py delete mode 100644 opendc-web/opendc-web-api/opendc/exts.py delete mode 100644 opendc-web/opendc-web-api/opendc/models/__init__.py delete mode 100644 opendc-web/opendc-web-api/opendc/models/model.py delete mode 100644 opendc-web/opendc-web-api/opendc/models/portfolio.py delete mode 100644 opendc-web/opendc-web-api/opendc/models/prefab.py delete mode 100644 opendc-web/opendc-web-api/opendc/models/project.py delete mode 100644 opendc-web/opendc-web-api/opendc/models/scenario.py delete mode 100644 opendc-web/opendc-web-api/opendc/models/topology.py delete mode 100644 opendc-web/opendc-web-api/opendc/models/trace.py delete mode 100644 opendc-web/opendc-web-api/opendc/util.py (limited to 'opendc-web/opendc-web-api/opendc') diff --git a/opendc-web/opendc-web-api/opendc/__init__.py b/opendc-web/opendc-web-api/opendc/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/opendc-web/opendc-web-api/opendc/api/__init__.py b/opendc-web/opendc-web-api/opendc/api/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/opendc-web/opendc-web-api/opendc/api/jobs.py b/opendc-web/opendc-web-api/opendc/api/jobs.py deleted file mode 100644 index 6fb0522b..00000000 --- a/opendc-web/opendc-web-api/opendc/api/jobs.py +++ /dev/null @@ -1,105 +0,0 @@ -# Copyright (c) 2021 AtLarge Research -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. -from flask import request -from flask_restful import Resource -from marshmallow import fields, Schema, validate -from werkzeug.exceptions import BadRequest, Conflict - -from opendc.exts import requires_auth, requires_scope -from opendc.models.scenario import Scenario - - -def convert_to_job(scenario): - """Convert a scenario to a job. - """ - return JobSchema().dump({ - '_id': scenario['_id'], - 'scenarioId': scenario['_id'], - 'state': scenario['simulation']['state'], - 'heartbeat': scenario['simulation'].get('heartbeat', None), - 'results': scenario.get('results', {}) - }) - - -class JobSchema(Schema): - """ - Schema representing a simulation job. - """ - _id = fields.String(dump_only=True) - scenarioId = fields.String(dump_only=True) - state = fields.String(required=True, - validate=validate.OneOf(["QUEUED", "CLAIMED", "RUNNING", "FINISHED", "FAILED"])) - heartbeat = fields.DateTime() - results = fields.Dict() - - -class JobList(Resource): - """ - Resource representing the list of available jobs. - """ - method_decorators = [requires_auth, requires_scope('runner')] - - def get(self): - """Get all available jobs.""" - jobs = Scenario.get_jobs() - data = list(map(convert_to_job, jobs.obj)) - return {'data': data} - - -class Job(Resource): - """ - Resource representing a single job. - """ - method_decorators = [requires_auth, requires_scope('runner')] - - def get(self, job_id): - """Get the details of a single job.""" - job = Scenario.from_id(job_id) - job.check_exists() - data = convert_to_job(job.obj) - return {'data': data} - - def post(self, job_id): - """Update the details of a single job.""" - action = JobSchema(only=('state', 'results')).load(request.json) - - job = Scenario.from_id(job_id) - job.check_exists() - - old_state = job.obj['simulation']['state'] - new_state = action['state'] - - if old_state == new_state: - data = job.update_state(new_state) - elif (old_state, new_state) == ('QUEUED', 'CLAIMED'): - data = job.update_state('CLAIMED') - elif (old_state, new_state) == ('CLAIMED', 'RUNNING'): - data = job.update_state('RUNNING') - elif (old_state, new_state) == ('RUNNING', 'FINISHED'): - data = job.update_state('FINISHED', results=action.get('results', None)) - elif old_state in ('CLAIMED', 'RUNNING') and new_state == 'FAILED': - data = job.update_state('FAILED') - else: - raise BadRequest('Invalid state transition') - - if not data: - raise Conflict('State conflict') - - return {'data': convert_to_job(data)} diff --git a/opendc-web/opendc-web-api/opendc/api/portfolios.py b/opendc-web/opendc-web-api/opendc/api/portfolios.py deleted file mode 100644 index 4d8f54fd..00000000 --- a/opendc-web/opendc-web-api/opendc/api/portfolios.py +++ /dev/null @@ -1,153 +0,0 @@ -# Copyright (c) 2021 AtLarge Research -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -from flask import request -from flask_restful import Resource -from marshmallow import Schema, fields - -from opendc.exts import requires_auth, current_user, has_scope -from opendc.models.portfolio import Portfolio as PortfolioModel, PortfolioSchema -from opendc.models.project import Project -from opendc.models.scenario import ScenarioSchema, Scenario -from opendc.models.topology import Topology - - -class Portfolio(Resource): - """ - Resource representing a portfolio. - """ - method_decorators = [requires_auth] - - def get(self, portfolio_id): - """ - Get a portfolio by identifier. - """ - portfolio = PortfolioModel.from_id(portfolio_id) - - portfolio.check_exists() - - # Users with scope runner can access all portfolios - if not has_scope('runner'): - portfolio.check_user_access(current_user['sub'], False) - - data = PortfolioSchema().dump(portfolio.obj) - return {'data': data} - - def put(self, portfolio_id): - """ - Replace the portfolio. - """ - schema = Portfolio.PutSchema() - result = schema.load(request.json) - - portfolio = PortfolioModel.from_id(portfolio_id) - portfolio.check_exists() - portfolio.check_user_access(current_user['sub'], True) - - portfolio.set_property('name', result['portfolio']['name']) - portfolio.set_property('targets.enabledMetrics', result['portfolio']['targets']['enabledMetrics']) - portfolio.set_property('targets.repeatsPerScenario', result['portfolio']['targets']['repeatsPerScenario']) - - portfolio.update() - data = PortfolioSchema().dump(portfolio.obj) - return {'data': data} - - def delete(self, portfolio_id): - """ - Delete a portfolio. - """ - portfolio = PortfolioModel.from_id(portfolio_id) - - portfolio.check_exists() - portfolio.check_user_access(current_user['sub'], True) - - portfolio_id = portfolio.get_id() - - project = Project.from_id(portfolio.obj['projectId']) - project.check_exists() - if portfolio_id in project.obj['portfolioIds']: - project.obj['portfolioIds'].remove(portfolio_id) - project.update() - - old_object = portfolio.delete() - data = PortfolioSchema().dump(old_object) - return {'data': data} - - class PutSchema(Schema): - """ - Schema for the PUT operation on a portfolio. - """ - portfolio = fields.Nested(PortfolioSchema, required=True) - - -class PortfolioScenarios(Resource): - """ - Resource representing the scenarios of a portfolio. - """ - method_decorators = [requires_auth] - - def get(self, portfolio_id): - """ - Get all scenarios belonging to a portfolio. - """ - portfolio = PortfolioModel.from_id(portfolio_id) - - portfolio.check_exists() - portfolio.check_user_access(current_user['sub'], True) - - scenarios = Scenario.get_for_portfolio(portfolio_id) - - data = ScenarioSchema().dump(scenarios, many=True) - return {'data': data} - - def post(self, portfolio_id): - """ - Add a new scenario to this portfolio - """ - schema = PortfolioScenarios.PostSchema() - result = schema.load(request.json) - - portfolio = PortfolioModel.from_id(portfolio_id) - - portfolio.check_exists() - portfolio.check_user_access(current_user['sub'], True) - - scenario = Scenario(result['scenario']) - - topology = Topology.from_id(scenario.obj['topology']['topologyId']) - topology.check_exists() - topology.check_user_access(current_user['sub'], True) - - scenario.set_property('portfolioId', portfolio.get_id()) - scenario.set_property('simulation', {'state': 'QUEUED'}) - scenario.set_property('topology.topologyId', topology.get_id()) - - scenario.insert() - - portfolio.obj['scenarioIds'].append(scenario.get_id()) - portfolio.update() - data = ScenarioSchema().dump(scenario.obj) - return {'data': data} - - class PostSchema(Schema): - """ - Schema for the POST operation on a portfolio's scenarios. - """ - scenario = fields.Nested(ScenarioSchema, required=True) diff --git a/opendc-web/opendc-web-api/opendc/api/prefabs.py b/opendc-web/opendc-web-api/opendc/api/prefabs.py deleted file mode 100644 index 730546ba..00000000 --- a/opendc-web/opendc-web-api/opendc/api/prefabs.py +++ /dev/null @@ -1,123 +0,0 @@ -# Copyright (c) 2021 AtLarge Research -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -from datetime import datetime -from flask import request -from flask_restful import Resource -from marshmallow import Schema, fields - -from opendc.models.prefab import Prefab as PrefabModel, PrefabSchema -from opendc.exts import current_user, requires_auth, db - - -class PrefabList(Resource): - """ - Resource for the list of prefabs available to the user. - """ - method_decorators = [requires_auth] - - def get(self): - """ - Get the available prefabs for a user. - """ - user_id = current_user['sub'] - - own_prefabs = db.fetch_all({'authorId': user_id}, PrefabModel.collection_name) - public_prefabs = db.fetch_all({'visibility': 'public'}, PrefabModel.collection_name) - - authorizations = {"authorizations": []} - authorizations["authorizations"].append(own_prefabs) - authorizations["authorizations"].append(public_prefabs) - return {'data': authorizations} - - def post(self): - """ - Create a new prefab. - """ - schema = PrefabList.PostSchema() - result = schema.load(request.json) - - prefab = PrefabModel(result['prefab']) - prefab.set_property('datetimeCreated', datetime.now()) - prefab.set_property('datetimeLastEdited', datetime.now()) - - user_id = current_user['sub'] - prefab.set_property('authorId', user_id) - - prefab.insert() - data = PrefabSchema().dump(prefab.obj) - return {'data': data} - - class PostSchema(Schema): - """ - Schema for the POST operation on the prefab list. - """ - prefab = fields.Nested(PrefabSchema, required=True) - - -class Prefab(Resource): - """ - Resource representing a single prefab. - """ - method_decorators = [requires_auth] - - def get(self, prefab_id): - """Get this Prefab.""" - prefab = PrefabModel.from_id(prefab_id) - prefab.check_exists() - prefab.check_user_access(current_user['sub']) - data = PrefabSchema().dump(prefab.obj) - return {'data': data} - - def put(self, prefab_id): - """Update a prefab's name and/or contents.""" - - schema = Prefab.PutSchema() - result = schema.load(request.json) - - prefab = PrefabModel.from_id(prefab_id) - prefab.check_exists() - prefab.check_user_access(current_user['sub']) - - prefab.set_property('name', result['prefab']['name']) - prefab.set_property('rack', result['prefab']['rack']) - prefab.set_property('datetimeLastEdited', datetime.now()) - prefab.update() - - data = PrefabSchema().dump(prefab.obj) - return {'data': data} - - def delete(self, prefab_id): - """Delete this Prefab.""" - prefab = PrefabModel.from_id(prefab_id) - - prefab.check_exists() - prefab.check_user_access(current_user['sub']) - - old_object = prefab.delete() - - data = PrefabSchema().dump(old_object) - return {'data': data} - - class PutSchema(Schema): - """ - Schema for the PUT operation on a prefab. - """ - prefab = fields.Nested(PrefabSchema, required=True) diff --git a/opendc-web/opendc-web-api/opendc/api/projects.py b/opendc-web/opendc-web-api/opendc/api/projects.py deleted file mode 100644 index 2b47c12e..00000000 --- a/opendc-web/opendc-web-api/opendc/api/projects.py +++ /dev/null @@ -1,224 +0,0 @@ -# Copyright (c) 2021 AtLarge Research -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -from datetime import datetime -from flask import request -from flask_restful import Resource -from marshmallow import Schema, fields - -from opendc.models.portfolio import Portfolio, PortfolioSchema -from opendc.models.topology import Topology, TopologySchema -from opendc.models.project import Project as ProjectModel, ProjectSchema -from opendc.exts import current_user, requires_auth - - -class ProjectList(Resource): - """ - Resource representing the list of projects available to a user. - """ - method_decorators = [requires_auth] - - def get(self): - """Get the authorized projects of the user""" - user_id = current_user['sub'] - projects = ProjectModel.get_for_user(user_id) - data = ProjectSchema().dump(projects, many=True) - return {'data': data} - - def post(self): - """Create a new project, and return that new project.""" - user_id = current_user['sub'] - - schema = Project.PutSchema() - result = schema.load(request.json) - - topology = Topology({'name': 'Default topology', 'rooms': []}) - topology.insert() - - project = ProjectModel(result['project']) - project.set_property('datetimeCreated', datetime.now()) - project.set_property('datetimeLastEdited', datetime.now()) - project.set_property('topologyIds', [topology.get_id()]) - project.set_property('portfolioIds', []) - project.set_property('authorizations', [{'userId': user_id, 'level': 'OWN'}]) - project.insert() - - topology.set_property('projectId', project.get_id()) - topology.update() - - data = ProjectSchema().dump(project.obj) - return {'data': data} - - -class Project(Resource): - """ - Resource representing a single project. - """ - method_decorators = [requires_auth] - - def get(self, project_id): - """Get this Project.""" - project = ProjectModel.from_id(project_id) - - project.check_exists() - project.check_user_access(current_user['sub'], False) - - data = ProjectSchema().dump(project.obj) - return {'data': data} - - def put(self, project_id): - """Update a project's name.""" - schema = Project.PutSchema() - result = schema.load(request.json) - - project = ProjectModel.from_id(project_id) - - project.check_exists() - project.check_user_access(current_user['sub'], True) - - project.set_property('name', result['project']['name']) - project.set_property('datetimeLastEdited', datetime.now()) - project.update() - - data = ProjectSchema().dump(project.obj) - return {'data': data} - - def delete(self, project_id): - """Delete this Project.""" - project = ProjectModel.from_id(project_id) - - project.check_exists() - project.check_user_access(current_user['sub'], True) - - for topology_id in project.obj['topologyIds']: - topology = Topology.from_id(topology_id) - topology.delete() - - for portfolio_id in project.obj['portfolioIds']: - portfolio = Portfolio.from_id(portfolio_id) - portfolio.delete() - - old_object = project.delete() - data = ProjectSchema().dump(old_object) - return {'data': data} - - class PutSchema(Schema): - """ - Schema for the PUT operation on a project. - """ - project = fields.Nested(ProjectSchema, required=True) - - -class ProjectTopologies(Resource): - """ - Resource representing the topologies of a project. - """ - method_decorators = [requires_auth] - - def get(self, project_id): - """Get all topologies belonging to the project.""" - project = ProjectModel.from_id(project_id) - - project.check_exists() - project.check_user_access(current_user['sub'], True) - - topologies = Topology.get_for_project(project_id) - data = TopologySchema().dump(topologies, many=True) - - return {'data': data} - - def post(self, project_id): - """Add a new Topology to the specified project and return it""" - schema = ProjectTopologies.PutSchema() - result = schema.load(request.json) - - project = ProjectModel.from_id(project_id) - - project.check_exists() - project.check_user_access(current_user['sub'], True) - - topology = Topology({ - 'projectId': project.get_id(), - 'name': result['topology']['name'], - 'rooms': result['topology']['rooms'], - }) - - topology.insert() - - project.obj['topologyIds'].append(topology.get_id()) - project.set_property('datetimeLastEdited', datetime.now()) - project.update() - - data = TopologySchema().dump(topology.obj) - return {'data': data} - - class PutSchema(Schema): - """ - Schema for the PUT operation on a project topology. - """ - topology = fields.Nested(TopologySchema, required=True) - - -class ProjectPortfolios(Resource): - """ - Resource representing the portfolios of a project. - """ - method_decorators = [requires_auth] - - def get(self, project_id): - """Get all portfolios belonging to the project.""" - project = ProjectModel.from_id(project_id) - - project.check_exists() - project.check_user_access(current_user['sub'], True) - - portfolios = Portfolio.get_for_project(project_id) - data = PortfolioSchema().dump(portfolios, many=True) - - return {'data': data} - - def post(self, project_id): - """Add a new Portfolio for this Project.""" - schema = ProjectPortfolios.PutSchema() - result = schema.load(request.json) - - project = ProjectModel.from_id(project_id) - - project.check_exists() - project.check_user_access(current_user['sub'], True) - - portfolio = Portfolio(result['portfolio']) - - portfolio.set_property('projectId', project.get_id()) - portfolio.set_property('scenarioIds', []) - - portfolio.insert() - - project.obj['portfolioIds'].append(portfolio.get_id()) - project.update() - - data = PortfolioSchema().dump(portfolio.obj) - return {'data': data} - - class PutSchema(Schema): - """ - Schema for the PUT operation on a project portfolio. - """ - portfolio = fields.Nested(PortfolioSchema, required=True) diff --git a/opendc-web/opendc-web-api/opendc/api/scenarios.py b/opendc-web/opendc-web-api/opendc/api/scenarios.py deleted file mode 100644 index eacb0b49..00000000 --- a/opendc-web/opendc-web-api/opendc/api/scenarios.py +++ /dev/null @@ -1,86 +0,0 @@ -# Copyright (c) 2021 AtLarge Research -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -from flask import request -from flask_restful import Resource -from marshmallow import Schema, fields - -from opendc.models.scenario import Scenario as ScenarioModel, ScenarioSchema -from opendc.models.portfolio import Portfolio -from opendc.exts import current_user, requires_auth, has_scope - - -class Scenario(Resource): - """ - A Scenario resource. - """ - method_decorators = [requires_auth] - - def get(self, scenario_id): - """Get scenario by identifier.""" - scenario = ScenarioModel.from_id(scenario_id) - scenario.check_exists() - - # Users with scope runner can access all scenarios - if not has_scope('runner'): - scenario.check_user_access(current_user['sub'], False) - - data = ScenarioSchema().dump(scenario.obj) - return {'data': data} - - def put(self, scenario_id): - """Update this Scenarios name.""" - schema = Scenario.PutSchema() - result = schema.load(request.json) - - scenario = ScenarioModel.from_id(scenario_id) - - scenario.check_exists() - scenario.check_user_access(current_user['sub'], True) - - scenario.set_property('name', result['scenario']['name']) - - scenario.update() - data = ScenarioSchema().dump(scenario.obj) - return {'data': data} - - def delete(self, scenario_id): - """Delete this Scenario.""" - scenario = ScenarioModel.from_id(scenario_id) - scenario.check_exists() - scenario.check_user_access(current_user['sub'], True) - - scenario_id = scenario.get_id() - - portfolio = Portfolio.from_id(scenario.obj['portfolioId']) - portfolio.check_exists() - if scenario_id in portfolio.obj['scenarioIds']: - portfolio.obj['scenarioIds'].remove(scenario_id) - portfolio.update() - - old_object = scenario.delete() - data = ScenarioSchema().dump(old_object) - return {'data': data} - - class PutSchema(Schema): - """ - Schema for the put operation. - """ - scenario = fields.Nested(ScenarioSchema, required=True) diff --git a/opendc-web/opendc-web-api/opendc/api/schedulers.py b/opendc-web/opendc-web-api/opendc/api/schedulers.py deleted file mode 100644 index b00d8c31..00000000 --- a/opendc-web/opendc-web-api/opendc/api/schedulers.py +++ /dev/null @@ -1,46 +0,0 @@ -# Copyright (c) 2021 AtLarge Research -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - - -from flask_restful import Resource -from opendc.exts import requires_auth - -SCHEDULERS = [ - 'mem', - 'mem-inv', - 'core-mem', - 'core-mem-inv', - 'active-servers', - 'active-servers-inv', - 'provisioned-cores', - 'provisioned-cores-inv', - 'random' -] - - -class SchedulerList(Resource): - """ - Resource for the list of schedulers to pick from. - """ - method_decorators = [requires_auth] - - def get(self): - """Get all available Traces.""" - return {'data': [{'name': name} for name in SCHEDULERS]} diff --git a/opendc-web/opendc-web-api/opendc/api/topologies.py b/opendc-web/opendc-web-api/opendc/api/topologies.py deleted file mode 100644 index c0b2e7ee..00000000 --- a/opendc-web/opendc-web-api/opendc/api/topologies.py +++ /dev/null @@ -1,97 +0,0 @@ -# Copyright (c) 2021 AtLarge Research -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -from datetime import datetime - -from flask import request -from flask_restful import Resource -from marshmallow import Schema, fields - -from opendc.models.project import Project -from opendc.models.topology import Topology as TopologyModel, TopologySchema -from opendc.exts import current_user, requires_auth, has_scope - - -class Topology(Resource): - """ - Resource representing a single topology. - """ - method_decorators = [requires_auth] - - def get(self, topology_id): - """ - Get a single topology. - """ - topology = TopologyModel.from_id(topology_id) - topology.check_exists() - - # Users with scope runner can access all topologies - if not has_scope('runner'): - topology.check_user_access(current_user['sub'], False) - - data = TopologySchema().dump(topology.obj) - return {'data': data} - - def put(self, topology_id): - """ - Replace the topology. - """ - topology = TopologyModel.from_id(topology_id) - - schema = Topology.PutSchema() - result = schema.load(request.json) - - topology.check_exists() - topology.check_user_access(current_user['sub'], True) - - topology.set_property('name', result['topology']['name']) - topology.set_property('rooms', result['topology']['rooms']) - topology.set_property('datetimeLastEdited', datetime.now()) - - topology.update() - data = TopologySchema().dump(topology.obj) - return {'data': data} - - def delete(self, topology_id): - """ - Delete a topology. - """ - topology = TopologyModel.from_id(topology_id) - - topology.check_exists() - topology.check_user_access(current_user['sub'], True) - - topology_id = topology.get_id() - - project = Project.from_id(topology.obj['projectId']) - project.check_exists() - if topology_id in project.obj['topologyIds']: - project.obj['topologyIds'].remove(topology_id) - project.update() - - old_object = topology.delete() - data = TopologySchema().dump(old_object) - return {'data': data} - - class PutSchema(Schema): - """ - Schema for the PUT operation on a topology. - """ - topology = fields.Nested(TopologySchema, required=True) diff --git a/opendc-web/opendc-web-api/opendc/api/traces.py b/opendc-web/opendc-web-api/opendc/api/traces.py deleted file mode 100644 index 6be8c5e5..00000000 --- a/opendc-web/opendc-web-api/opendc/api/traces.py +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright (c) 2021 AtLarge Research -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -from flask_restful import Resource - -from opendc.exts import requires_auth -from opendc.models.trace import Trace as TraceModel, TraceSchema - - -class TraceList(Resource): - """ - Resource for the list of traces to pick from. - """ - method_decorators = [requires_auth] - - def get(self): - """Get all available Traces.""" - traces = TraceModel.get_all() - data = TraceSchema().dump(traces.obj, many=True) - return {'data': data} - - -class Trace(Resource): - """ - Resource representing a single trace. - """ - method_decorators = [requires_auth] - - def get(self, trace_id): - """Get trace information by identifier.""" - trace = TraceModel.from_id(trace_id) - trace.check_exists() - data = TraceSchema().dump(trace.obj) - return {'data': data} diff --git a/opendc-web/opendc-web-api/opendc/auth.py b/opendc-web/opendc-web-api/opendc/auth.py deleted file mode 100644 index d5da6ee5..00000000 --- a/opendc-web/opendc-web-api/opendc/auth.py +++ /dev/null @@ -1,236 +0,0 @@ -# Copyright (c) 2021 AtLarge Research -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -import json -import time - -import urllib3 -from flask import request -from jose import jwt, JWTError - - -def get_token(): - """ - Obtain the Access Token from the Authorization Header - """ - auth = request.headers.get("Authorization", None) - if not auth: - raise AuthError({ - "code": "authorization_header_missing", - "description": "Authorization header is expected" - }, 401) - - parts = auth.split() - - if parts[0].lower() != "bearer": - raise AuthError({"code": "invalid_header", "description": "Authorization header must start with Bearer"}, 401) - if len(parts) == 1: - raise AuthError({"code": "invalid_header", "description": "Token not found"}, 401) - if len(parts) > 2: - raise AuthError({"code": "invalid_header", "description": "Authorization header must be" " Bearer token"}, 401) - - token = parts[1] - return token - - -class AuthError(Exception): - """ - This error is thrown when the request failed to authorize. - """ - def __init__(self, error, status_code): - Exception.__init__(self, error) - self.error = error - self.status_code = status_code - - -class AuthContext: - """ - This class handles the authorization of requests. - """ - def __init__(self, alg, issuer, audience): - self._alg = alg - self._issuer = issuer - self._audience = audience - - def validate(self, token): - """ - Validate the specified JWT token. - :param token: The authorization token specified by the user. - :return: The token payload on success, otherwise `AuthError`. - """ - try: - header = jwt.get_unverified_header(token) - except JWTError as e: - raise AuthError({"code": "invalid_token", "message": str(e)}, 401) - - alg = header.get('alg', None) - if alg != self._alg.algorithm: - raise AuthError( - { - "code": - "invalid_header", - "message": - f"Signature algorithm of {alg} is not supported. Expected the ID token " - f"to be signed with {self._alg.algorithm}" - }, 401) - - kid = header.get('kid', None) - try: - secret_or_certificate = self._alg.get_key(key_id=kid) - except TokenValidationError as e: - raise AuthError({"code": "invalid_header", "message": str(e)}, 401) - try: - payload = jwt.decode(token, - key=secret_or_certificate, - algorithms=[self._alg.algorithm], - audience=self._audience, - issuer=self._issuer) - return payload - except jwt.ExpiredSignatureError: - raise AuthError({"code": "token_expired", "message": "Token is expired"}, 401) - except jwt.JWTClaimsError: - raise AuthError( - { - "code": "invalid_claims", - "message": "Incorrect claims, please check the audience and issuer" - }, 401) - except Exception as e: - print(e) - raise AuthError({"code": "invalid_header", "message": "Unable to parse authentication token."}, 401) - - -class SymmetricJwtAlgorithm: - """Verifier for HMAC signatures, which rely on shared secrets. - Args: - shared_secret (str): The shared secret used to decode the token. - algorithm (str, optional): The expected signing algorithm. Defaults to "HS256". - """ - def __init__(self, shared_secret, algorithm="HS256"): - self.algorithm = algorithm - self._shared_secret = shared_secret - - # pylint: disable=W0613 - def get_key(self, key_id=None): - """ - Obtain the key for this algorithm. - :param key_id: The identifier of the key. - :return: The JWK key. - """ - return self._shared_secret - - -class AsymmetricJwtAlgorithm: - """Verifier for RSA signatures, which rely on public key certificates. - Args: - jwks_url (str): The url where the JWK set is located. - algorithm (str, optional): The expected signing algorithm. Defaults to "RS256". - """ - def __init__(self, jwks_url, algorithm="RS256"): - self.algorithm = algorithm - self._fetcher = JwksFetcher(jwks_url) - - def get_key(self, key_id=None): - """ - Obtain the key for this algorithm. - :param key_id: The identifier of the key. - :return: The JWK key. - """ - return self._fetcher.get_key(key_id) - - -class TokenValidationError(Exception): - """ - Error thrown when the token cannot be validated - """ - - -class JwksFetcher: - """Class that fetches and holds a JSON web key set. - This class makes use of an in-memory cache. For it to work properly, define this instance once and re-use it. - Args: - jwks_url (str): The url where the JWK set is located. - cache_ttl (str, optional): The lifetime of the JWK set cache in seconds. Defaults to 600 seconds. - """ - CACHE_TTL = 600 # 10 min cache lifetime - - def __init__(self, jwks_url, cache_ttl=CACHE_TTL): - self._jwks_url = jwks_url - self._http = urllib3.PoolManager() - self._cache_value = {} - self._cache_date = 0 - self._cache_ttl = cache_ttl - self._cache_is_fresh = False - - def _fetch_jwks(self, force=False): - """Attempts to obtain the JWK set from the cache, as long as it's still valid. - When not, it will perform a network request to the jwks_url to obtain a fresh result - and update the cache value with it. - Args: - force (bool, optional): whether to ignore the cache and force a network request or not. Defaults to False. - """ - has_expired = self._cache_date + self._cache_ttl < time.time() - - if not force and not has_expired: - # Return from cache - self._cache_is_fresh = False - return self._cache_value - - # Invalidate cache and fetch fresh data - self._cache_value = {} - response = self._http.request('GET', self._jwks_url) - - if response.status == 200: - # Update cache - jwks = json.loads(response.data.decode('utf-8')) - self._cache_value = self._parse_jwks(jwks) - self._cache_is_fresh = True - self._cache_date = time.time() - return self._cache_value - - @staticmethod - def _parse_jwks(jwks): - """Converts a JWK string representation into a binary certificate in PEM format. - """ - keys = {} - - for key in jwks['keys']: - keys[key["kid"]] = key - return keys - - def get_key(self, key_id): - """Obtains the JWK associated with the given key id. - Args: - key_id (str): The id of the key to fetch. - Returns: - the JWK associated with the given key id. - - Raises: - TokenValidationError: when a key with that id cannot be found - """ - keys = self._fetch_jwks() - - if keys and key_id in keys: - return keys[key_id] - - if not self._cache_is_fresh: - keys = self._fetch_jwks(force=True) - if keys and key_id in keys: - return keys[key_id] - raise TokenValidationError(f"RSA Public Key with ID {key_id} was not found.") diff --git a/opendc-web/opendc-web-api/opendc/database.py b/opendc-web/opendc-web-api/opendc/database.py deleted file mode 100644 index dd6367f2..00000000 --- a/opendc-web/opendc-web-api/opendc/database.py +++ /dev/null @@ -1,97 +0,0 @@ -# Copyright (c) 2021 AtLarge Research -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -import urllib.parse - -from pymongo import MongoClient, ReturnDocument - -DATETIME_STRING_FORMAT = '%Y-%m-%dT%H:%M:%S' -CONNECTION_POOL = None - - -class Database: - """Object holding functionality for database access.""" - def __init__(self, db=None): - """Initializes the database connection.""" - self.opendc_db = db - - @classmethod - def from_credentials(cls, user, password, database, host): - """ - Construct a database instance from the specified credentials. - :param user: The username to connect with. - :param password: The password to connect with. - :param database: The database name to connect to. - :param host: The host to connect to. - :return: The database instance. - """ - user = urllib.parse.quote_plus(user) - password = urllib.parse.quote_plus(password) - database = urllib.parse.quote_plus(database) - host = urllib.parse.quote_plus(host) - - client = MongoClient('mongodb://%s:%s@%s/default_db?authSource=%s' % (user, password, host, database)) - return cls(client.opendc) - - def fetch_one(self, query, collection): - """Uses existing mongo connection to return a single (the first) document in a collection matching the given - query as a JSON object. - - The query needs to be in json format, i.e.: `{'name': prefab_name}`. - """ - return getattr(self.opendc_db, collection).find_one(query) - - def fetch_all(self, query, collection): - """Uses existing mongo connection to return all documents matching a given query, as a list of JSON objects. - - The query needs to be in json format, i.e.: `{'name': prefab_name}`. - """ - cursor = getattr(self.opendc_db, collection).find(query) - return list(cursor) - - def insert(self, obj, collection): - """Updates an existing object.""" - bson = getattr(self.opendc_db, collection).insert(obj) - - return bson - - def update(self, _id, obj, collection): - """Updates an existing object.""" - return getattr(self.opendc_db, collection).update({'_id': _id}, obj) - - def fetch_and_update(self, query, update, collection): - """Updates an existing object.""" - return getattr(self.opendc_db, collection).find_one_and_update(query, - update, - return_document=ReturnDocument.AFTER) - - def delete_one(self, query, collection): - """Deletes one object matching the given query. - - The query needs to be in json format, i.e.: `{'name': prefab_name}`. - """ - getattr(self.opendc_db, collection).delete_one(query) - - def delete_all(self, query, collection): - """Deletes all objects matching the given query. - - The query needs to be in json format, i.e.: `{'name': prefab_name}`. - """ - getattr(self.opendc_db, collection).delete_many(query) diff --git a/opendc-web/opendc-web-api/opendc/exts.py b/opendc-web/opendc-web-api/opendc/exts.py deleted file mode 100644 index 3ee8babb..00000000 --- a/opendc-web/opendc-web-api/opendc/exts.py +++ /dev/null @@ -1,91 +0,0 @@ -import os -from functools import wraps - -from flask import g, _request_ctx_stack -from jose import jwt -from werkzeug.local import LocalProxy - -from opendc.database import Database -from opendc.auth import AuthContext, AsymmetricJwtAlgorithm, get_token, AuthError - - -def get_db(): - """ - Return the configured database instance for the application. - """ - _db = getattr(g, 'db', None) - if _db is None: - _db = Database.from_credentials(user=os.environ['OPENDC_DB_USERNAME'], - password=os.environ['OPENDC_DB_PASSWORD'], - database=os.environ['OPENDC_DB'], - host=os.environ.get('OPENDC_DB_HOST', 'localhost')) - g.db = _db - return _db - - -db = LocalProxy(get_db) - - -def get_auth_context(): - """ - Return the configured auth context for the application. - """ - _auth_context = getattr(g, 'auth_context', None) - if _auth_context is None: - _auth_context = AuthContext( - alg=AsymmetricJwtAlgorithm(jwks_url=f"https://{os.environ['AUTH0_DOMAIN']}/.well-known/jwks.json"), - issuer=f"https://{os.environ['AUTH0_DOMAIN']}/", - audience=os.environ['AUTH0_AUDIENCE']) - g.auth_context = _auth_context - return _auth_context - - -auth_context = LocalProxy(get_auth_context) - - -def requires_auth(f): - """Decorator to determine if the Access Token is valid. - """ - @wraps(f) - def decorated(*args, **kwargs): - token = get_token() - payload = auth_context.validate(token) - _request_ctx_stack.top.current_user = payload - return f(*args, **kwargs) - - return decorated - - -current_user = LocalProxy(lambda: getattr(_request_ctx_stack.top, 'current_user', None)) - - -def has_scope(required_scope): - """Determines if the required scope is present in the Access Token - Args: - required_scope (str): The scope required to access the resource - """ - token = get_token() - unverified_claims = jwt.get_unverified_claims(token) - if unverified_claims.get("scope"): - token_scopes = unverified_claims["scope"].split() - for token_scope in token_scopes: - if token_scope == required_scope: - return True - return False - - -def requires_scope(required_scope): - """Determines if the required scope is present in the Access Token - Args: - required_scope (str): The scope required to access the resource - """ - def decorator(f): - @wraps(f) - def decorated(*args, **kwargs): - if not has_scope(required_scope): - raise AuthError({"code": "Unauthorized", "description": "You don't have access to this resource"}, 403) - return f(*args, **kwargs) - - return decorated - - return decorator diff --git a/opendc-web/opendc-web-api/opendc/models/__init__.py b/opendc-web/opendc-web-api/opendc/models/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/opendc-web/opendc-web-api/opendc/models/model.py b/opendc-web/opendc-web-api/opendc/models/model.py deleted file mode 100644 index 28299453..00000000 --- a/opendc-web/opendc-web-api/opendc/models/model.py +++ /dev/null @@ -1,61 +0,0 @@ -from bson.objectid import ObjectId -from werkzeug.exceptions import NotFound - -from opendc.exts import db - - -class Model: - """Base class for all models.""" - - collection_name = '' - - @classmethod - def from_id(cls, _id): - """Fetches the document with given ID from the collection.""" - if isinstance(_id, str) and len(_id) == 24: - _id = ObjectId(_id) - - return cls(db.fetch_one({'_id': _id}, cls.collection_name)) - - @classmethod - def get_all(cls): - """Fetches all documents from the collection.""" - return cls(db.fetch_all({}, cls.collection_name)) - - def __init__(self, obj): - self.obj = obj - - def get_id(self): - """Returns the ID of the enclosed object.""" - return self.obj['_id'] - - def check_exists(self): - """Raises an error if the enclosed object does not exist.""" - if self.obj is None: - raise NotFound('Entity not found.') - - def set_property(self, key, value): - """Sets the given property on the enclosed object, with support for simple nested access.""" - if '.' in key: - keys = key.split('.') - self.obj[keys[0]][keys[1]] = value - else: - self.obj[key] = value - - def insert(self): - """Inserts the enclosed object and generates a UUID for it.""" - self.obj['_id'] = ObjectId() - db.insert(self.obj, self.collection_name) - - def update(self): - """Updates the enclosed object and updates the internal reference to the newly inserted object.""" - db.update(self.get_id(), self.obj, self.collection_name) - - def delete(self): - """Deletes the enclosed object in the database, if it existed.""" - if self.obj is None: - return None - - old_object = self.obj.copy() - db.delete_one({'_id': self.get_id()}, self.collection_name) - return old_object diff --git a/opendc-web/opendc-web-api/opendc/models/portfolio.py b/opendc-web/opendc-web-api/opendc/models/portfolio.py deleted file mode 100644 index eb016947..00000000 --- a/opendc-web/opendc-web-api/opendc/models/portfolio.py +++ /dev/null @@ -1,47 +0,0 @@ -from bson import ObjectId -from marshmallow import Schema, fields - -from opendc.exts import db -from opendc.models.project import Project -from opendc.models.model import Model - - -class TargetSchema(Schema): - """ - Schema representing a target. - """ - enabledMetrics = fields.List(fields.String()) - repeatsPerScenario = fields.Integer(required=True) - - -class PortfolioSchema(Schema): - """ - Schema representing a portfolio. - """ - _id = fields.String(dump_only=True) - projectId = fields.String() - name = fields.String(required=True) - scenarioIds = fields.List(fields.String()) - targets = fields.Nested(TargetSchema) - - -class Portfolio(Model): - """Model representing a Portfolio.""" - - collection_name = 'portfolios' - - def check_user_access(self, user_id, edit_access): - """Raises an error if the user with given [user_id] has insufficient access. - - Checks access on the parent project. - - :param user_id: The User ID of the user. - :param edit_access: True when edit access should be checked, otherwise view access. - """ - project = Project.from_id(self.obj['projectId']) - project.check_user_access(user_id, edit_access) - - @classmethod - def get_for_project(cls, project_id): - """Get all portfolios for the specified project id.""" - return db.fetch_all({'projectId': ObjectId(project_id)}, cls.collection_name) diff --git a/opendc-web/opendc-web-api/opendc/models/prefab.py b/opendc-web/opendc-web-api/opendc/models/prefab.py deleted file mode 100644 index 5e4b81dc..00000000 --- a/opendc-web/opendc-web-api/opendc/models/prefab.py +++ /dev/null @@ -1,31 +0,0 @@ -from marshmallow import Schema, fields -from werkzeug.exceptions import Forbidden - -from opendc.models.topology import ObjectSchema -from opendc.models.model import Model - - -class PrefabSchema(Schema): - """ - Schema for a Prefab. - """ - _id = fields.String(dump_only=True) - authorId = fields.String(dump_only=True) - name = fields.String(required=True) - datetimeCreated = fields.DateTime() - datetimeLastEdited = fields.DateTime() - rack = fields.Nested(ObjectSchema) - - -class Prefab(Model): - """Model representing a Prefab.""" - - collection_name = 'prefabs' - - def check_user_access(self, user_id): - """Raises an error if the user with given [user_id] has insufficient access to view this prefab. - - :param user_id: The user ID of the user. - """ - if self.obj['authorId'] != user_id and self.obj['visibility'] == "private": - raise Forbidden("Forbidden from retrieving prefab.") diff --git a/opendc-web/opendc-web-api/opendc/models/project.py b/opendc-web/opendc-web-api/opendc/models/project.py deleted file mode 100644 index f2b3b564..00000000 --- a/opendc-web/opendc-web-api/opendc/models/project.py +++ /dev/null @@ -1,48 +0,0 @@ -from marshmallow import Schema, fields, validate -from werkzeug.exceptions import Forbidden - -from opendc.models.model import Model -from opendc.exts import db - - -class ProjectAuthorizations(Schema): - """ - Schema representing a project authorization. - """ - userId = fields.String(required=True) - level = fields.String(required=True, validate=validate.OneOf(["VIEW", "EDIT", "OWN"])) - - -class ProjectSchema(Schema): - """ - Schema representing a Project. - """ - _id = fields.String(dump_only=True) - name = fields.String(required=True) - datetimeCreated = fields.DateTime() - datetimeLastEdited = fields.DateTime() - topologyIds = fields.List(fields.String()) - portfolioIds = fields.List(fields.String()) - authorizations = fields.List(fields.Nested(ProjectAuthorizations)) - - -class Project(Model): - """Model representing a Project.""" - - collection_name = 'projects' - - def check_user_access(self, user_id, edit_access): - """Raises an error if the user with given [user_id] has insufficient access. - - :param user_id: The User ID of the user. - :param edit_access: True when edit access should be checked, otherwise view access. - """ - for authorization in self.obj['authorizations']: - if user_id == authorization['userId'] and authorization['level'] != 'VIEW' or not edit_access: - return - raise Forbidden("Forbidden from retrieving project.") - - @classmethod - def get_for_user(cls, user_id): - """Get all projects for the specified user id.""" - return db.fetch_all({'authorizations.userId': user_id}, Project.collection_name) diff --git a/opendc-web/opendc-web-api/opendc/models/scenario.py b/opendc-web/opendc-web-api/opendc/models/scenario.py deleted file mode 100644 index 47771e06..00000000 --- a/opendc-web/opendc-web-api/opendc/models/scenario.py +++ /dev/null @@ -1,93 +0,0 @@ -from datetime import datetime - -from bson import ObjectId -from marshmallow import Schema, fields - -from opendc.exts import db -from opendc.models.model import Model -from opendc.models.portfolio import Portfolio - - -class SimulationSchema(Schema): - """ - Simulation details. - """ - state = fields.String() - - -class TraceSchema(Schema): - """ - Schema for specifying the trace of a scenario. - """ - traceId = fields.String() - loadSamplingFraction = fields.Float() - - -class TopologySchema(Schema): - """ - Schema for topology specification for a scenario. - """ - topologyId = fields.String() - - -class OperationalSchema(Schema): - """ - Schema for the operational phenomena for a scenario. - """ - failuresEnabled = fields.Boolean() - performanceInterferenceEnabled = fields.Boolean() - schedulerName = fields.String() - - -class ScenarioSchema(Schema): - """ - Schema representing a scenario. - """ - _id = fields.String(dump_only=True) - portfolioId = fields.String() - name = fields.String(required=True) - trace = fields.Nested(TraceSchema) - topology = fields.Nested(TopologySchema) - operational = fields.Nested(OperationalSchema) - simulation = fields.Nested(SimulationSchema, dump_only=True) - results = fields.Dict(dump_only=True) - - -class Scenario(Model): - """Model representing a Scenario.""" - - collection_name = 'scenarios' - - def check_user_access(self, user_id, edit_access): - """Raises an error if the user with given [user_id] has insufficient access. - - Checks access on the parent project. - - :param user_id: The User ID of the user. - :param edit_access: True when edit access should be checked, otherwise view access. - """ - portfolio = Portfolio.from_id(self.obj['portfolioId']) - portfolio.check_user_access(user_id, edit_access) - - @classmethod - def get_jobs(cls): - """Obtain the scenarios that have been queued. - """ - return cls(db.fetch_all({'simulation.state': 'QUEUED'}, cls.collection_name)) - - @classmethod - def get_for_portfolio(cls, portfolio_id): - """Get all scenarios for the specified portfolio id.""" - return db.fetch_all({'portfolioId': ObjectId(portfolio_id)}, cls.collection_name) - - def update_state(self, new_state, results=None): - """Atomically update the state of the Scenario. - """ - update = {'$set': {'simulation.state': new_state, 'simulation.heartbeat': datetime.now()}} - if results: - update['$set']['results'] = results - return db.fetch_and_update( - query={'_id': self.obj['_id'], 'simulation.state': self.obj['simulation']['state']}, - update=update, - collection=self.collection_name - ) diff --git a/opendc-web/opendc-web-api/opendc/models/topology.py b/opendc-web/opendc-web-api/opendc/models/topology.py deleted file mode 100644 index 44994818..00000000 --- a/opendc-web/opendc-web-api/opendc/models/topology.py +++ /dev/null @@ -1,108 +0,0 @@ -from bson import ObjectId -from marshmallow import Schema, fields - -from opendc.exts import db -from opendc.models.project import Project -from opendc.models.model import Model - - -class MemorySchema(Schema): - """ - Schema representing a memory unit. - """ - _id = fields.String() - name = fields.String() - speedMbPerS = fields.Integer() - sizeMb = fields.Integer() - energyConsumptionW = fields.Integer() - - -class PuSchema(Schema): - """ - Schema representing a processing unit. - """ - _id = fields.String() - name = fields.String() - clockRateMhz = fields.Integer() - numberOfCores = fields.Integer() - energyConsumptionW = fields.Integer() - - -class MachineSchema(Schema): - """ - Schema representing a machine. - """ - _id = fields.String() - position = fields.Integer() - cpus = fields.List(fields.Nested(PuSchema)) - gpus = fields.List(fields.Nested(PuSchema)) - memories = fields.List(fields.Nested(MemorySchema)) - storages = fields.List(fields.Nested(MemorySchema)) - rackId = fields.String() - - -class ObjectSchema(Schema): - """ - Schema representing a room object. - """ - _id = fields.String() - name = fields.String() - capacity = fields.Integer() - powerCapacityW = fields.Integer() - machines = fields.List(fields.Nested(MachineSchema)) - tileId = fields.String() - - -class TileSchema(Schema): - """ - Schema representing a room tile. - """ - _id = fields.String() - topologyId = fields.String() - positionX = fields.Integer() - positionY = fields.Integer() - rack = fields.Nested(ObjectSchema) - roomId = fields.String() - - -class RoomSchema(Schema): - """ - Schema representing a room. - """ - _id = fields.String() - name = fields.String(required=True) - topologyId = fields.String() - tiles = fields.List(fields.Nested(TileSchema), required=True) - - -class TopologySchema(Schema): - """ - Schema representing a datacenter topology. - """ - _id = fields.String(dump_only=True) - projectId = fields.String() - name = fields.String(required=True) - rooms = fields.List(fields.Nested(RoomSchema), required=True) - datetimeLastEdited = fields.DateTime() - - -class Topology(Model): - """Model representing a Project.""" - - collection_name = 'topologies' - - def check_user_access(self, user_id, edit_access): - """Raises an error if the user with given [user_id] has insufficient access. - - Checks access on the parent project. - - :param user_id: The User ID of the user. - :param edit_access: True when edit access should be checked, otherwise view access. - """ - project = Project.from_id(self.obj['projectId']) - project.check_user_access(user_id, edit_access) - - @classmethod - def get_for_project(cls, project_id): - """Get all topologies for the specified project id.""" - return db.fetch_all({'projectId': ObjectId(project_id)}, cls.collection_name) diff --git a/opendc-web/opendc-web-api/opendc/models/trace.py b/opendc-web/opendc-web-api/opendc/models/trace.py deleted file mode 100644 index 69287f29..00000000 --- a/opendc-web/opendc-web-api/opendc/models/trace.py +++ /dev/null @@ -1,16 +0,0 @@ -from marshmallow import Schema, fields - -from opendc.models.model import Model - - -class TraceSchema(Schema): - """Schema for a Trace.""" - _id = fields.String(dump_only=True) - name = fields.String() - type = fields.String() - - -class Trace(Model): - """Model representing a Trace.""" - - collection_name = 'traces' diff --git a/opendc-web/opendc-web-api/opendc/util.py b/opendc-web/opendc-web-api/opendc/util.py deleted file mode 100644 index e7dc07a4..00000000 --- a/opendc-web/opendc-web-api/opendc/util.py +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright (c) 2021 AtLarge Research -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -import flask -from bson.objectid import ObjectId - - -class JSONEncoder(flask.json.JSONEncoder): - """ - A customized JSON encoder to handle unsupported types. - """ - def default(self, o): - if isinstance(o, ObjectId): - return str(o) - return flask.json.JSONEncoder.default(self, o) -- cgit v1.2.3