diff options
| author | Fabian Mastenbroek <mail.fabianm@gmail.com> | 2021-05-18 20:34:13 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2021-05-18 20:34:13 +0200 |
| commit | 56bd2ef6b0583fee1dd2da5dceaf57feb07649c9 (patch) | |
| tree | 6d4cfbc44c97cd3ec1e30aa977cd08f404b41b0d /opendc-web/opendc-web-api/opendc | |
| parent | 02776c958a3254735b2be7d9fb1627f75e7f80cd (diff) | |
| parent | ce95cfdf803043e66e2279d0f76c6bfc64e7864e (diff) | |
Migrate to Auth0 as Identity Provider
This pull request removes the hard dependency on Google for
authenticating users and migrates to Auth0 as Identity Provider for OpenDC.
This has as benefit that we can authenticate users without having to manage
user data ourselves and do not have a dependency on Google accounts anymore.
- Frontend cleanup:
- Use CSS modules everywhere to encapsulate the styling of React components.
- Perform all communication in the frontend via the REST API (as opposed to WebSockets).
The original approach was aimed at collaborative editing, but made normal operations
harder to implement and debug. If we want to implement collaborative editing in the
future, we can expose only a small WebSocket API specifically for collaborative editing.
- Move to FontAwesome 5 (using the official React libraries)
- Use Reactstrap where possible. Previously, we mixed raw Bootstrap classes with
Reactstrap, which is confusing.
- Reduce the scope of the Redux state. Some state in the frontend application can be
kept locally and does not need to be managed by Redux.
- Migrate from Create React App (CRA) to Next.js since it allows us to pre-render
multiple pages as well as opt-in to Server Side Rendering.
- Remove the Google login and use Auth0 for authentication now.
- Use Node 16
- Backend cleanup:
- Remove Socket.IO endpoint from backend, since it is not needed by the frontend
anymore. Removing it reduces the attack surface of OpenDC as well as the maintenance efforts.
- Use Auth0 JWT token for authorizing API accesses
- Refactor API endpoints to use Flask Restful as opposed to our custom in-house
routing logic. Previously, this was needed to support the Socket.IO endpoint,
but increases maintenance effort.
- Expose Swagger UI from API
- Use Python 3.9 and uwsgi to host Flask application
- Actualize OpenAPI schema and update to version 3.0.
**Breaking API Changes**
* This pull request removes the users collection from the database table. Instead, we now use the user identifier passed by Auth0 to identify the data that belongs to a user.
Diffstat (limited to 'opendc-web/opendc-web-api/opendc')
80 files changed, 1301 insertions, 2353 deletions
diff --git a/opendc-web/opendc-web-api/opendc/api/portfolios.py b/opendc-web/opendc-web-api/opendc/api/portfolios.py new file mode 100644 index 00000000..b07e9da5 --- /dev/null +++ b/opendc-web/opendc-web-api/opendc/api/portfolios.py @@ -0,0 +1,135 @@ +# 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 +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() + portfolio.check_user_access(current_user['sub'], False) + + data = 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 = 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() + return {'data': old_object} + + 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 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 = 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 new file mode 100644 index 00000000..7bb17e7d --- /dev/null +++ b/opendc-web/opendc-web-api/opendc/api/prefabs.py @@ -0,0 +1,120 @@ +# 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.database import Database +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', Database.datetime_to_string(datetime.now())) + prefab.set_property('datetimeLastEdited', Database.datetime_to_string(datetime.now())) + + user_id = current_user['sub'] + prefab.set_property('authorId', user_id) + + prefab.insert() + return {'data': prefab.obj} + + 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']) + return {'data': prefab.obj} + + 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('datetime_last_edited', Database.datetime_to_string(datetime.now())) + prefab.update() + + return {'data': prefab.obj} + + 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() + + return {'data': old_object} + + 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 new file mode 100644 index 00000000..8c44b680 --- /dev/null +++ b/opendc-web/opendc-web-api/opendc/api/projects.py @@ -0,0 +1,195 @@ +# 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 +from opendc.database import Database + + +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) + return {'data': projects} + + 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', Database.datetime_to_string(datetime.now())) + project.set_property('datetimeLastEdited', Database.datetime_to_string(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() + + return {'data': project.obj} + + +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) + + return {'data': project.obj} + + 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', Database.datetime_to_string(datetime.now())) + project.update() + + return {'data': project.obj} + + 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() + + return {'data': old_object} + + 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 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', Database.datetime_to_string(datetime.now())) + project.update() + + return {'data': topology.obj} + + 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 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() + + return {'data': portfolio.obj} + + 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 new file mode 100644 index 00000000..b566950a --- /dev/null +++ b/opendc-web/opendc-web-api/opendc/api/scenarios.py @@ -0,0 +1,81 @@ +# 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 + + +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() + scenario.check_user_access(current_user['sub'], False) + data = 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 = 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() + return {'data': old_object} + + 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 new file mode 100644 index 00000000..b00d8c31 --- /dev/null +++ b/opendc-web/opendc-web-api/opendc/api/schedulers.py @@ -0,0 +1,46 @@ +# 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 new file mode 100644 index 00000000..eedf049d --- /dev/null +++ b/opendc-web/opendc-web-api/opendc/api/topologies.py @@ -0,0 +1,93 @@ +# 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.database import Database +from opendc.models.project import Project +from opendc.models.topology import Topology as TopologyModel, TopologySchema +from opendc.exts import current_user, requires_auth + + +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() + topology.check_user_access(current_user['sub'], False) + data = 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', Database.datetime_to_string(datetime.now())) + + topology.update() + data = 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() + return {'data': old_object} + + 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 new file mode 100644 index 00000000..f685f00c --- /dev/null +++ b/opendc-web/opendc-web-api/opendc/api/traces.py @@ -0,0 +1,51 @@ +# 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 + + +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 = traces.obj + 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 = trace.obj + return {'data': data} diff --git a/opendc-web/opendc-web-api/opendc/api/v2/__init__.py b/opendc-web/opendc-web-api/opendc/api/v2/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/opendc-web/opendc-web-api/opendc/api/v2/__init__.py +++ /dev/null diff --git a/opendc-web/opendc-web-api/opendc/api/v2/paths.json b/opendc-web/opendc-web-api/opendc/api/v2/paths.json deleted file mode 100644 index 652be5bc..00000000 --- a/opendc-web/opendc-web-api/opendc/api/v2/paths.json +++ /dev/null @@ -1,19 +0,0 @@ -[ - "/users", - "/users/{userId}", - "/projects", - "/projects/{projectId}", - "/projects/{projectId}/authorizations", - "/projects/{projectId}/topologies", - "/projects/{projectId}/portfolios", - "/topologies/{topologyId}", - "/portfolios/{portfolioId}", - "/portfolios/{portfolioId}/scenarios", - "/scenarios/{scenarioId}", - "/schedulers", - "/traces", - "/traces/{traceId}", - "/prefabs", - "/prefabs/{prefabId}", - "/prefabs/authorizations" -] diff --git a/opendc-web/opendc-web-api/opendc/api/v2/portfolios/__init__.py b/opendc-web/opendc-web-api/opendc/api/v2/portfolios/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/opendc-web/opendc-web-api/opendc/api/v2/portfolios/__init__.py +++ /dev/null diff --git a/opendc-web/opendc-web-api/opendc/api/v2/portfolios/portfolioId/__init__.py b/opendc-web/opendc-web-api/opendc/api/v2/portfolios/portfolioId/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/opendc-web/opendc-web-api/opendc/api/v2/portfolios/portfolioId/__init__.py +++ /dev/null diff --git a/opendc-web/opendc-web-api/opendc/api/v2/portfolios/portfolioId/endpoint.py b/opendc-web/opendc-web-api/opendc/api/v2/portfolios/portfolioId/endpoint.py deleted file mode 100644 index 0ba61a13..00000000 --- a/opendc-web/opendc-web-api/opendc/api/v2/portfolios/portfolioId/endpoint.py +++ /dev/null @@ -1,67 +0,0 @@ -from opendc.models.portfolio import Portfolio -from opendc.models.project import Project -from opendc.util.rest import Response - - -def GET(request): - """Get this Portfolio.""" - - request.check_required_parameters(path={'portfolioId': 'string'}) - - portfolio = Portfolio.from_id(request.params_path['portfolioId']) - - portfolio.check_exists() - portfolio.check_user_access(request.google_id, False) - - return Response(200, 'Successfully retrieved portfolio.', portfolio.obj) - - -def PUT(request): - """Update this Portfolio.""" - - request.check_required_parameters(path={'portfolioId': 'string'}, body={'portfolio': { - 'name': 'string', - 'targets': { - 'enabledMetrics': 'list', - 'repeatsPerScenario': 'int', - }, - }}) - - portfolio = Portfolio.from_id(request.params_path['portfolioId']) - - portfolio.check_exists() - portfolio.check_user_access(request.google_id, True) - - portfolio.set_property('name', - request.params_body['portfolio']['name']) - portfolio.set_property('targets.enabledMetrics', - request.params_body['portfolio']['targets']['enabledMetrics']) - portfolio.set_property('targets.repeatsPerScenario', - request.params_body['portfolio']['targets']['repeatsPerScenario']) - - portfolio.update() - - return Response(200, 'Successfully updated portfolio.', portfolio.obj) - - -def DELETE(request): - """Delete this Portfolio.""" - - request.check_required_parameters(path={'portfolioId': 'string'}) - - portfolio = Portfolio.from_id(request.params_path['portfolioId']) - - portfolio.check_exists() - portfolio.check_user_access(request.google_id, 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() - - return Response(200, 'Successfully deleted portfolio.', old_object) diff --git a/opendc-web/opendc-web-api/opendc/api/v2/portfolios/portfolioId/scenarios/__init__.py b/opendc-web/opendc-web-api/opendc/api/v2/portfolios/portfolioId/scenarios/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/opendc-web/opendc-web-api/opendc/api/v2/portfolios/portfolioId/scenarios/__init__.py +++ /dev/null diff --git a/opendc-web/opendc-web-api/opendc/api/v2/portfolios/portfolioId/scenarios/endpoint.py b/opendc-web/opendc-web-api/opendc/api/v2/portfolios/portfolioId/scenarios/endpoint.py deleted file mode 100644 index 2f042e06..00000000 --- a/opendc-web/opendc-web-api/opendc/api/v2/portfolios/portfolioId/scenarios/endpoint.py +++ /dev/null @@ -1,49 +0,0 @@ -from opendc.models.portfolio import Portfolio -from opendc.models.scenario import Scenario -from opendc.models.topology import Topology -from opendc.util.rest import Response - - -def POST(request): - """Add a new Scenario for this Portfolio.""" - - request.check_required_parameters(path={'portfolioId': 'string'}, - body={ - 'scenario': { - 'name': 'string', - 'trace': { - 'traceId': 'string', - 'loadSamplingFraction': 'float', - }, - 'topology': { - 'topologyId': 'string', - }, - 'operational': { - 'failuresEnabled': 'bool', - 'performanceInterferenceEnabled': 'bool', - 'schedulerName': 'string', - }, - } - }) - - portfolio = Portfolio.from_id(request.params_path['portfolioId']) - - portfolio.check_exists() - portfolio.check_user_access(request.google_id, True) - - scenario = Scenario(request.params_body['scenario']) - - topology = Topology.from_id(scenario.obj['topology']['topologyId']) - topology.check_exists() - topology.check_user_access(request.google_id, 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() - - return Response(200, 'Successfully added Scenario.', scenario.obj) diff --git a/opendc-web/opendc-web-api/opendc/api/v2/portfolios/portfolioId/scenarios/test_endpoint.py b/opendc-web/opendc-web-api/opendc/api/v2/portfolios/portfolioId/scenarios/test_endpoint.py deleted file mode 100644 index e5982b7f..00000000 --- a/opendc-web/opendc-web-api/opendc/api/v2/portfolios/portfolioId/scenarios/test_endpoint.py +++ /dev/null @@ -1,125 +0,0 @@ -from opendc.util.database import DB - -test_id = 24 * '1' - - -def test_add_scenario_missing_parameter(client): - assert '400' in client.post('/v2/portfolios/1/scenarios').status - - -def test_add_scenario_non_existing_portfolio(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value=None) - assert '404' in client.post(f'/v2/portfolios/{test_id}/scenarios', - json={ - 'scenario': { - 'name': 'test', - 'trace': { - 'traceId': test_id, - 'loadSamplingFraction': 1.0, - }, - 'topology': { - 'topologyId': test_id, - }, - 'operational': { - 'failuresEnabled': True, - 'performanceInterferenceEnabled': False, - 'schedulerName': 'DEFAULT', - }, - } - }).status - - -def test_add_scenario_not_authorized(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': test_id, - 'projectId': test_id, - 'portfolioId': test_id, - 'authorizations': [{ - 'projectId': test_id, - 'authorizationLevel': 'VIEW' - }] - }) - assert '403' in client.post(f'/v2/portfolios/{test_id}/scenarios', - json={ - 'scenario': { - 'name': 'test', - 'trace': { - 'traceId': test_id, - 'loadSamplingFraction': 1.0, - }, - 'topology': { - 'topologyId': test_id, - }, - 'operational': { - 'failuresEnabled': True, - 'performanceInterferenceEnabled': False, - 'schedulerName': 'DEFAULT', - }, - } - }).status - - -def test_add_scenario(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': test_id, - 'projectId': test_id, - 'portfolioId': test_id, - 'portfolioIds': [test_id], - 'scenarioIds': [test_id], - 'authorizations': [{ - 'projectId': test_id, - 'authorizationLevel': 'EDIT' - }], - 'simulation': { - 'state': 'QUEUED', - }, - }) - mocker.patch.object(DB, - 'insert', - return_value={ - '_id': test_id, - 'name': 'test', - 'trace': { - 'traceId': test_id, - 'loadSamplingFraction': 1.0, - }, - 'topology': { - 'topologyId': test_id, - }, - 'operational': { - 'failuresEnabled': True, - 'performanceInterferenceEnabled': False, - 'schedulerName': 'DEFAULT', - }, - 'portfolioId': test_id, - 'simulationState': { - 'state': 'QUEUED', - }, - }) - mocker.patch.object(DB, 'update', return_value=None) - res = client.post( - f'/v2/portfolios/{test_id}/scenarios', - json={ - 'scenario': { - 'name': 'test', - 'trace': { - 'traceId': test_id, - 'loadSamplingFraction': 1.0, - }, - 'topology': { - 'topologyId': test_id, - }, - 'operational': { - 'failuresEnabled': True, - 'performanceInterferenceEnabled': False, - 'schedulerName': 'DEFAULT', - }, - } - }) - assert 'portfolioId' in res.json['content'] - assert 'simulation' in res.json['content'] - assert '200' in res.status diff --git a/opendc-web/opendc-web-api/opendc/api/v2/portfolios/portfolioId/test_endpoint.py b/opendc-web/opendc-web-api/opendc/api/v2/portfolios/portfolioId/test_endpoint.py deleted file mode 100644 index 52f71aa4..00000000 --- a/opendc-web/opendc-web-api/opendc/api/v2/portfolios/portfolioId/test_endpoint.py +++ /dev/null @@ -1,152 +0,0 @@ -from opendc.util.database import DB - -test_id = 24 * '1' -test_id_2 = 24 * '2' - - -def test_get_portfolio_non_existing(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value=None) - assert '404' in client.get(f'/v2/portfolios/{test_id}').status - - -def test_get_portfolio_no_authorizations(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value={'projectId': test_id, 'authorizations': []}) - res = client.get(f'/v2/portfolios/{test_id}') - assert '403' in res.status - - -def test_get_portfolio_not_authorized(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - 'projectId': test_id, - '_id': test_id, - 'authorizations': [{ - 'projectId': test_id_2, - 'authorizationLevel': 'OWN' - }] - }) - res = client.get(f'/v2/portfolios/{test_id}') - assert '403' in res.status - - -def test_get_portfolio(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - 'projectId': test_id, - '_id': test_id, - 'authorizations': [{ - 'projectId': test_id, - 'authorizationLevel': 'EDIT' - }] - }) - res = client.get(f'/v2/portfolios/{test_id}') - assert '200' in res.status - - -def test_update_portfolio_missing_parameter(client): - assert '400' in client.put(f'/v2/portfolios/{test_id}').status - - -def test_update_portfolio_non_existing(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value=None) - assert '404' in client.put(f'/v2/portfolios/{test_id}', json={ - 'portfolio': { - 'name': 'test', - 'targets': { - 'enabledMetrics': ['test'], - 'repeatsPerScenario': 2 - } - } - }).status - - -def test_update_portfolio_not_authorized(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': test_id, - 'projectId': test_id, - 'authorizations': [{ - 'projectId': test_id, - 'authorizationLevel': 'VIEW' - }] - }) - mocker.patch.object(DB, 'update', return_value={}) - assert '403' in client.put(f'/v2/portfolios/{test_id}', json={ - 'portfolio': { - 'name': 'test', - 'targets': { - 'enabledMetrics': ['test'], - 'repeatsPerScenario': 2 - } - } - }).status - - -def test_update_portfolio(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': test_id, - 'projectId': test_id, - 'authorizations': [{ - 'projectId': test_id, - 'authorizationLevel': 'OWN' - }], - 'targets': { - 'enabledMetrics': [], - 'repeatsPerScenario': 1 - } - }) - mocker.patch.object(DB, 'update', return_value={}) - - res = client.put(f'/v2/portfolios/{test_id}', json={'portfolio': { - 'name': 'test', - 'targets': { - 'enabledMetrics': ['test'], - 'repeatsPerScenario': 2 - } - }}) - assert '200' in res.status - - -def test_delete_project_non_existing(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value=None) - assert '404' in client.delete(f'/v2/portfolios/{test_id}').status - - -def test_delete_project_different_user(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': test_id, - 'projectId': test_id, - 'googleId': 'other_test', - 'authorizations': [{ - 'projectId': test_id, - 'authorizationLevel': 'VIEW' - }] - }) - mocker.patch.object(DB, 'delete_one', return_value=None) - assert '403' in client.delete(f'/v2/portfolios/{test_id}').status - - -def test_delete_project(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': test_id, - 'projectId': test_id, - 'googleId': 'test', - 'portfolioIds': [test_id], - 'authorizations': [{ - 'projectId': test_id, - 'authorizationLevel': 'OWN' - }] - }) - mocker.patch.object(DB, 'delete_one', return_value={}) - mocker.patch.object(DB, 'update', return_value=None) - res = client.delete(f'/v2/portfolios/{test_id}') - assert '200' in res.status diff --git a/opendc-web/opendc-web-api/opendc/api/v2/prefabs/__init__.py b/opendc-web/opendc-web-api/opendc/api/v2/prefabs/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/opendc-web/opendc-web-api/opendc/api/v2/prefabs/__init__.py +++ /dev/null diff --git a/opendc-web/opendc-web-api/opendc/api/v2/prefabs/authorizations/__init__.py b/opendc-web/opendc-web-api/opendc/api/v2/prefabs/authorizations/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/opendc-web/opendc-web-api/opendc/api/v2/prefabs/authorizations/__init__.py +++ /dev/null diff --git a/opendc-web/opendc-web-api/opendc/api/v2/prefabs/authorizations/endpoint.py b/opendc-web/opendc-web-api/opendc/api/v2/prefabs/authorizations/endpoint.py deleted file mode 100644 index 0d9ad5cd..00000000 --- a/opendc-web/opendc-web-api/opendc/api/v2/prefabs/authorizations/endpoint.py +++ /dev/null @@ -1,22 +0,0 @@ -from opendc.models.prefab import Prefab -from opendc.util.database import DB -from opendc.models.user import User -from opendc.util.rest import Response - - -def GET(request): - """Return all prefabs the user is authorized to access""" - - user = User.from_google_id(request.google_id) - - user.check_exists() - - own_prefabs = DB.fetch_all({'authorId': user.get_id()}, Prefab.collection_name) - public_prefabs = DB.fetch_all({'visibility': 'public'}, Prefab.collection_name) - - authorizations = {"authorizations": []} - - authorizations["authorizations"].append(own_prefabs) - authorizations["authorizations"].append(public_prefabs) - - return Response(200, 'Successfully fetched authorizations.', authorizations) diff --git a/opendc-web/opendc-web-api/opendc/api/v2/prefabs/authorizations/test_endpoint.py b/opendc-web/opendc-web-api/opendc/api/v2/prefabs/authorizations/test_endpoint.py deleted file mode 100644 index 6d36d428..00000000 --- a/opendc-web/opendc-web-api/opendc/api/v2/prefabs/authorizations/test_endpoint.py +++ /dev/null @@ -1,71 +0,0 @@ -from opendc.util.database import DB -from unittest.mock import Mock - -test_id = 24 * '1' - - -def test_get_authorizations(client, mocker): - DB.fetch_all = Mock() - mocker.patch.object(DB, 'fetch_one', return_value={'_id': test_id}) - DB.fetch_all.side_effect = [ - [{ - '_id': test_id, - 'datetimeCreated': '000', - 'datetimeLastEdited': '000', - 'authorId': test_id, - 'visibility' : 'private' - }, - { - '_id': '2' * 24, - 'datetimeCreated': '000', - 'datetimeLastEdited': '000', - 'authorId': test_id, - 'visibility' : 'private' - }, - { - '_id': '3' * 24, - 'datetimeCreated': '000', - 'datetimeLastEdited': '000', - 'authorId': test_id, - 'visibility' : 'public' - }, - { - '_id': '4' * 24, - 'datetimeCreated': '000', - 'datetimeLastEdited': '000', - 'authorId': test_id, - 'visibility' : 'public' - }], - [{ - '_id': '5' * 24, - 'datetimeCreated': '000', - 'datetimeLastEdited': '000', - 'authorId': '2' * 24, - 'visibility' : 'public' - }, - { - '_id': '6' * 24, - 'datetimeCreated': '000', - 'datetimeLastEdited': '000', - 'authorId': '2' * 24, - 'visibility' : 'public' - }, - { - '_id': '7' * 24, - 'datetimeCreated': '000', - 'datetimeLastEdited': '000', - 'authorId': '2' * 24, - 'visibility' : 'public' - }, - { - '_id': '8' * 24, - 'datetimeCreated': '000', - 'datetimeLastEdited': '000', - 'authorId': '2' * 24, - 'visibility' : 'public' - }] - ] - mocker.patch.object(DB, 'fetch_one', return_value={'_id': test_id}) - res = client.get('/v2/prefabs/authorizations') - assert '200' in res.status - diff --git a/opendc-web/opendc-web-api/opendc/api/v2/prefabs/endpoint.py b/opendc-web/opendc-web-api/opendc/api/v2/prefabs/endpoint.py deleted file mode 100644 index 723a2f0d..00000000 --- a/opendc-web/opendc-web-api/opendc/api/v2/prefabs/endpoint.py +++ /dev/null @@ -1,23 +0,0 @@ -from datetime import datetime - -from opendc.models.prefab import Prefab -from opendc.models.user import User -from opendc.util.database import Database -from opendc.util.rest import Response - - -def POST(request): - """Create a new prefab, and return that new prefab.""" - - request.check_required_parameters(body={'prefab': {'name': 'string'}}) - - prefab = Prefab(request.params_body['prefab']) - prefab.set_property('datetimeCreated', Database.datetime_to_string(datetime.now())) - prefab.set_property('datetimeLastEdited', Database.datetime_to_string(datetime.now())) - - user = User.from_google_id(request.google_id) - prefab.set_property('authorId', user.get_id()) - - prefab.insert() - - return Response(200, 'Successfully created prefab.', prefab.obj) diff --git a/opendc-web/opendc-web-api/opendc/api/v2/prefabs/prefabId/__init__.py b/opendc-web/opendc-web-api/opendc/api/v2/prefabs/prefabId/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/opendc-web/opendc-web-api/opendc/api/v2/prefabs/prefabId/__init__.py +++ /dev/null diff --git a/opendc-web/opendc-web-api/opendc/api/v2/prefabs/prefabId/endpoint.py b/opendc-web/opendc-web-api/opendc/api/v2/prefabs/prefabId/endpoint.py deleted file mode 100644 index 7b81f546..00000000 --- a/opendc-web/opendc-web-api/opendc/api/v2/prefabs/prefabId/endpoint.py +++ /dev/null @@ -1,50 +0,0 @@ -from datetime import datetime - -from opendc.models.prefab import Prefab -from opendc.util.database import Database -from opendc.util.rest import Response - - -def GET(request): - """Get this Prefab.""" - - request.check_required_parameters(path={'prefabId': 'string'}) - - prefab = Prefab.from_id(request.params_path['prefabId']) - prefab.check_exists() - prefab.check_user_access(request.google_id) - - return Response(200, 'Successfully retrieved prefab', prefab.obj) - - -def PUT(request): - """Update a prefab's name and/or contents.""" - - request.check_required_parameters(body={'prefab': {'name': 'name'}}, path={'prefabId': 'string'}) - - prefab = Prefab.from_id(request.params_path['prefabId']) - - prefab.check_exists() - prefab.check_user_access(request.google_id) - - prefab.set_property('name', request.params_body['prefab']['name']) - prefab.set_property('rack', request.params_body['prefab']['rack']) - prefab.set_property('datetime_last_edited', Database.datetime_to_string(datetime.now())) - prefab.update() - - return Response(200, 'Successfully updated prefab.', prefab.obj) - - -def DELETE(request): - """Delete this Prefab.""" - - request.check_required_parameters(path={'prefabId': 'string'}) - - prefab = Prefab.from_id(request.params_path['prefabId']) - - prefab.check_exists() - prefab.check_user_access(request.google_id) - - old_object = prefab.delete() - - return Response(200, 'Successfully deleted prefab.', old_object) diff --git a/opendc-web/opendc-web-api/opendc/api/v2/prefabs/prefabId/test_endpoint.py b/opendc-web/opendc-web-api/opendc/api/v2/prefabs/prefabId/test_endpoint.py deleted file mode 100644 index 2daeb6bf..00000000 --- a/opendc-web/opendc-web-api/opendc/api/v2/prefabs/prefabId/test_endpoint.py +++ /dev/null @@ -1,145 +0,0 @@ -from opendc.util.database import DB -from unittest.mock import Mock - -test_id = 24 * '1' -test_id_2 = 24 * '2' - - -def test_get_prefab_non_existing(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value=None) - assert '404' in client.get(f'/v2/prefabs/{test_id}').status - - -def test_get_private_prefab_not_authorized(client, mocker): - DB.fetch_one = Mock() - DB.fetch_one.side_effect = [{ - '_id': test_id, - 'name': 'test prefab', - 'authorId': test_id_2, - 'visibility': 'private', - 'rack': {} - }, - { - '_id': test_id - } - ] - res = client.get(f'/v2/prefabs/{test_id}') - assert '403' in res.status - - -def test_get_private_prefab(client, mocker): - DB.fetch_one = Mock() - DB.fetch_one.side_effect = [{ - '_id': test_id, - 'name': 'test prefab', - 'authorId': test_id, - 'visibility': 'private', - 'rack': {} - }, - { - '_id': test_id - } - ] - res = client.get(f'/v2/prefabs/{test_id}') - assert '200' in res.status - - -def test_get_public_prefab(client, mocker): - DB.fetch_one = Mock() - DB.fetch_one.side_effect = [{ - '_id': test_id, - 'name': 'test prefab', - 'authorId': test_id_2, - 'visibility': 'public', - 'rack': {} - }, - { - '_id': test_id - } - ] - res = client.get(f'/v2/prefabs/{test_id}') - assert '200' in res.status - - -def test_update_prefab_missing_parameter(client): - assert '400' in client.put(f'/v2/prefabs/{test_id}').status - - -def test_update_prefab_non_existing(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value=None) - assert '404' in client.put(f'/v2/prefabs/{test_id}', json={'prefab': {'name': 'S'}}).status - - -def test_update_prefab_not_authorized(client, mocker): - DB.fetch_one = Mock() - DB.fetch_one.side_effect = [{ - '_id': test_id, - 'name': 'test prefab', - 'authorId': test_id_2, - 'visibility': 'private', - 'rack': {} - }, - { - '_id': test_id - } - ] - mocker.patch.object(DB, 'update', return_value={}) - assert '403' in client.put(f'/v2/prefabs/{test_id}', json={'prefab': {'name': 'test prefab', 'rack': {}}}).status - - -def test_update_prefab(client, mocker): - DB.fetch_one = Mock() - DB.fetch_one.side_effect = [{ - '_id': test_id, - 'name': 'test prefab', - 'authorId': test_id, - 'visibility': 'private', - 'rack': {} - }, - { - '_id': test_id - } - ] - mocker.patch.object(DB, 'update', return_value={}) - res = client.put(f'/v2/prefabs/{test_id}', json={'prefab': {'name': 'test prefab', 'rack': {}}}) - assert '200' in res.status - - -def test_delete_prefab_non_existing(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value=None) - assert '404' in client.delete(f'/v2/prefabs/{test_id}').status - - -def test_delete_prefab_different_user(client, mocker): - DB.fetch_one = Mock() - DB.fetch_one.side_effect = [{ - '_id': test_id, - 'name': 'test prefab', - 'authorId': test_id_2, - 'visibility': 'private', - 'rack': {} - }, - { - '_id': test_id - } - ] - mocker.patch.object(DB, 'delete_one', return_value=None) - assert '403' in client.delete(f'/v2/prefabs/{test_id}').status - - -def test_delete_prefab(client, mocker): - DB.fetch_one = Mock() - DB.fetch_one.side_effect = [{ - '_id': test_id, - 'name': 'test prefab', - 'authorId': test_id, - 'visibility': 'private', - 'rack': {} - }, - { - '_id': test_id - } - ] - mocker.patch.object(DB, 'delete_one', return_value={'prefab': {'name': 'name'}}) - res = client.delete(f'/v2/prefabs/{test_id}') - assert '200' in res.status diff --git a/opendc-web/opendc-web-api/opendc/api/v2/prefabs/test_endpoint.py b/opendc-web/opendc-web-api/opendc/api/v2/prefabs/test_endpoint.py deleted file mode 100644 index 39a78c21..00000000 --- a/opendc-web/opendc-web-api/opendc/api/v2/prefabs/test_endpoint.py +++ /dev/null @@ -1,24 +0,0 @@ -from opendc.util.database import DB - -test_id = 24 * '1' - - -def test_add_prefab_missing_parameter(client): - assert '400' in client.post('/v2/prefabs').status - - -def test_add_prefab(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value={'_id': test_id, 'authorizations': []}) - mocker.patch.object(DB, - 'insert', - return_value={ - '_id': test_id, - 'datetimeCreated': '000', - 'datetimeLastEdited': '000', - 'authorId': test_id - }) - res = client.post('/v2/prefabs', json={'prefab': {'name': 'test prefab'}}) - assert 'datetimeCreated' in res.json['content'] - assert 'datetimeLastEdited' in res.json['content'] - assert 'authorId' in res.json['content'] - assert '200' in res.status diff --git a/opendc-web/opendc-web-api/opendc/api/v2/projects/__init__.py b/opendc-web/opendc-web-api/opendc/api/v2/projects/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/opendc-web/opendc-web-api/opendc/api/v2/projects/__init__.py +++ /dev/null diff --git a/opendc-web/opendc-web-api/opendc/api/v2/projects/endpoint.py b/opendc-web/opendc-web-api/opendc/api/v2/projects/endpoint.py deleted file mode 100644 index bf031382..00000000 --- a/opendc-web/opendc-web-api/opendc/api/v2/projects/endpoint.py +++ /dev/null @@ -1,32 +0,0 @@ -from datetime import datetime - -from opendc.models.project import Project -from opendc.models.topology import Topology -from opendc.models.user import User -from opendc.util.database import Database -from opendc.util.rest import Response - - -def POST(request): - """Create a new project, and return that new project.""" - - request.check_required_parameters(body={'project': {'name': 'string'}}) - - topology = Topology({'name': 'Default topology', 'rooms': []}) - topology.insert() - - project = Project(request.params_body['project']) - project.set_property('datetimeCreated', Database.datetime_to_string(datetime.now())) - project.set_property('datetimeLastEdited', Database.datetime_to_string(datetime.now())) - project.set_property('topologyIds', [topology.get_id()]) - project.set_property('portfolioIds', []) - project.insert() - - topology.set_property('projectId', project.get_id()) - topology.update() - - user = User.from_google_id(request.google_id) - user.obj['authorizations'].append({'projectId': project.get_id(), 'authorizationLevel': 'OWN'}) - user.update() - - return Response(200, 'Successfully created project.', project.obj) diff --git a/opendc-web/opendc-web-api/opendc/api/v2/projects/projectId/__init__.py b/opendc-web/opendc-web-api/opendc/api/v2/projects/projectId/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/opendc-web/opendc-web-api/opendc/api/v2/projects/projectId/__init__.py +++ /dev/null diff --git a/opendc-web/opendc-web-api/opendc/api/v2/projects/projectId/authorizations/__init__.py b/opendc-web/opendc-web-api/opendc/api/v2/projects/projectId/authorizations/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/opendc-web/opendc-web-api/opendc/api/v2/projects/projectId/authorizations/__init__.py +++ /dev/null diff --git a/opendc-web/opendc-web-api/opendc/api/v2/projects/projectId/authorizations/endpoint.py b/opendc-web/opendc-web-api/opendc/api/v2/projects/projectId/authorizations/endpoint.py deleted file mode 100644 index 9f6a60ec..00000000 --- a/opendc-web/opendc-web-api/opendc/api/v2/projects/projectId/authorizations/endpoint.py +++ /dev/null @@ -1,17 +0,0 @@ -from opendc.models.project import Project -from opendc.util.rest import Response - - -def GET(request): - """Find all authorizations for a Project.""" - - request.check_required_parameters(path={'projectId': 'string'}) - - project = Project.from_id(request.params_path['projectId']) - - project.check_exists() - project.check_user_access(request.google_id, False) - - authorizations = project.get_all_authorizations() - - return Response(200, 'Successfully retrieved project authorizations', authorizations) diff --git a/opendc-web/opendc-web-api/opendc/api/v2/projects/projectId/authorizations/test_endpoint.py b/opendc-web/opendc-web-api/opendc/api/v2/projects/projectId/authorizations/test_endpoint.py deleted file mode 100644 index bebd6cff..00000000 --- a/opendc-web/opendc-web-api/opendc/api/v2/projects/projectId/authorizations/test_endpoint.py +++ /dev/null @@ -1,43 +0,0 @@ -from opendc.util.database import DB - -test_id = 24 * '1' -test_id_2 = 24 * '2' - - -def test_get_authorizations_non_existing(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value=None) - mocker.patch.object(DB, 'fetch_all', return_value=None) - assert '404' in client.get(f'/v2/projects/{test_id}/authorizations').status - - -def test_get_authorizations_not_authorized(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': test_id, - 'name': 'test trace', - 'authorizations': [{ - 'projectId': test_id_2, - 'authorizationLevel': 'OWN' - }] - }) - mocker.patch.object(DB, 'fetch_all', return_value=[]) - res = client.get(f'/v2/projects/{test_id}/authorizations') - assert '403' in res.status - - -def test_get_authorizations(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': test_id, - 'name': 'test trace', - 'authorizations': [{ - 'projectId': test_id, - 'authorizationLevel': 'OWN' - }] - }) - mocker.patch.object(DB, 'fetch_all', return_value=[]) - res = client.get(f'/v2/projects/{test_id}/authorizations') - assert len(res.json['content']) == 0 - assert '200' in res.status diff --git a/opendc-web/opendc-web-api/opendc/api/v2/projects/projectId/endpoint.py b/opendc-web/opendc-web-api/opendc/api/v2/projects/projectId/endpoint.py deleted file mode 100644 index caac37ca..00000000 --- a/opendc-web/opendc-web-api/opendc/api/v2/projects/projectId/endpoint.py +++ /dev/null @@ -1,66 +0,0 @@ -from datetime import datetime - -from opendc.models.portfolio import Portfolio -from opendc.models.project import Project -from opendc.models.topology import Topology -from opendc.models.user import User -from opendc.util.database import Database -from opendc.util.rest import Response - - -def GET(request): - """Get this Project.""" - - request.check_required_parameters(path={'projectId': 'string'}) - - project = Project.from_id(request.params_path['projectId']) - - project.check_exists() - project.check_user_access(request.google_id, False) - - return Response(200, 'Successfully retrieved project', project.obj) - - -def PUT(request): - """Update a project's name.""" - - request.check_required_parameters(body={'project': {'name': 'name'}}, path={'projectId': 'string'}) - - project = Project.from_id(request.params_path['projectId']) - - project.check_exists() - project.check_user_access(request.google_id, True) - - project.set_property('name', request.params_body['project']['name']) - project.set_property('datetime_last_edited', Database.datetime_to_string(datetime.now())) - project.update() - - return Response(200, 'Successfully updated project.', project.obj) - - -def DELETE(request): - """Delete this Project.""" - - request.check_required_parameters(path={'projectId': 'string'}) - - project = Project.from_id(request.params_path['projectId']) - - project.check_exists() - project.check_user_access(request.google_id, 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() - - user = User.from_google_id(request.google_id) - user.obj['authorizations'] = list( - filter(lambda x: x['projectId'] != project.get_id(), user.obj['authorizations'])) - user.update() - - old_object = project.delete() - - return Response(200, 'Successfully deleted project.', old_object) diff --git a/opendc-web/opendc-web-api/opendc/api/v2/projects/projectId/portfolios/__init__.py b/opendc-web/opendc-web-api/opendc/api/v2/projects/projectId/portfolios/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/opendc-web/opendc-web-api/opendc/api/v2/projects/projectId/portfolios/__init__.py +++ /dev/null diff --git a/opendc-web/opendc-web-api/opendc/api/v2/projects/projectId/portfolios/endpoint.py b/opendc-web/opendc-web-api/opendc/api/v2/projects/projectId/portfolios/endpoint.py deleted file mode 100644 index 2cdb1194..00000000 --- a/opendc-web/opendc-web-api/opendc/api/v2/projects/projectId/portfolios/endpoint.py +++ /dev/null @@ -1,35 +0,0 @@ -from opendc.models.portfolio import Portfolio -from opendc.models.project import Project -from opendc.util.rest import Response - - -def POST(request): - """Add a new Portfolio for this Project.""" - - request.check_required_parameters(path={'projectId': 'string'}, - body={ - 'portfolio': { - 'name': 'string', - 'targets': { - 'enabledMetrics': 'list', - 'repeatsPerScenario': 'int', - }, - } - }) - - project = Project.from_id(request.params_path['projectId']) - - project.check_exists() - project.check_user_access(request.google_id, True) - - portfolio = Portfolio(request.params_body['portfolio']) - - portfolio.set_property('projectId', project.get_id()) - portfolio.set_property('scenarioIds', []) - - portfolio.insert() - - project.obj['portfolioIds'].append(portfolio.get_id()) - project.update() - - return Response(200, 'Successfully added Portfolio.', portfolio.obj) diff --git a/opendc-web/opendc-web-api/opendc/api/v2/projects/projectId/portfolios/test_endpoint.py b/opendc-web/opendc-web-api/opendc/api/v2/projects/projectId/portfolios/test_endpoint.py deleted file mode 100644 index 04c699b5..00000000 --- a/opendc-web/opendc-web-api/opendc/api/v2/projects/projectId/portfolios/test_endpoint.py +++ /dev/null @@ -1,85 +0,0 @@ -from opendc.util.database import DB - -test_id = 24 * '1' - - -def test_add_portfolio_missing_parameter(client): - assert '400' in client.post(f'/v2/projects/{test_id}/portfolios').status - - -def test_add_portfolio_non_existing_project(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value=None) - assert '404' in client.post(f'/v2/projects/{test_id}/portfolios', - json={ - 'portfolio': { - 'name': 'test', - 'targets': { - 'enabledMetrics': ['test'], - 'repeatsPerScenario': 2 - } - } - }).status - - -def test_add_portfolio_not_authorized(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': test_id, - 'projectId': test_id, - 'authorizations': [{ - 'projectId': test_id, - 'authorizationLevel': 'VIEW' - }] - }) - assert '403' in client.post(f'/v2/projects/{test_id}/portfolios', - json={ - 'portfolio': { - 'name': 'test', - 'targets': { - 'enabledMetrics': ['test'], - 'repeatsPerScenario': 2 - } - } - }).status - - -def test_add_portfolio(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': test_id, - 'projectId': test_id, - 'portfolioIds': [test_id], - 'authorizations': [{ - 'projectId': test_id, - 'authorizationLevel': 'EDIT' - }] - }) - mocker.patch.object(DB, - 'insert', - return_value={ - '_id': test_id, - 'name': 'test', - 'targets': { - 'enabledMetrics': ['test'], - 'repeatsPerScenario': 2 - }, - 'projectId': test_id, - 'scenarioIds': [], - }) - mocker.patch.object(DB, 'update', return_value=None) - res = client.post( - f'/v2/projects/{test_id}/portfolios', - json={ - 'portfolio': { - 'name': 'test', - 'targets': { - 'enabledMetrics': ['test'], - 'repeatsPerScenario': 2 - } - } - }) - assert 'projectId' in res.json['content'] - assert 'scenarioIds' in res.json['content'] - assert '200' in res.status diff --git a/opendc-web/opendc-web-api/opendc/api/v2/projects/projectId/test_endpoint.py b/opendc-web/opendc-web-api/opendc/api/v2/projects/projectId/test_endpoint.py deleted file mode 100644 index f9ffaf37..00000000 --- a/opendc-web/opendc-web-api/opendc/api/v2/projects/projectId/test_endpoint.py +++ /dev/null @@ -1,122 +0,0 @@ -from opendc.util.database import DB - -test_id = 24 * '1' -test_id_2 = 24 * '2' - - -def test_get_project_non_existing(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value=None) - assert '404' in client.get(f'/v2/projects/{test_id}').status - - -def test_get_project_no_authorizations(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value={'authorizations': []}) - res = client.get(f'/v2/projects/{test_id}') - assert '403' in res.status - - -def test_get_project_not_authorized(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': test_id, - 'authorizations': [{ - 'projectId': test_id_2, - 'authorizationLevel': 'OWN' - }] - }) - res = client.get(f'/v2/projects/{test_id}') - assert '403' in res.status - - -def test_get_project(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': test_id, - 'authorizations': [{ - 'projectId': test_id, - 'authorizationLevel': 'EDIT' - }] - }) - res = client.get(f'/v2/projects/{test_id}') - assert '200' in res.status - - -def test_update_project_missing_parameter(client): - assert '400' in client.put(f'/v2/projects/{test_id}').status - - -def test_update_project_non_existing(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value=None) - assert '404' in client.put(f'/v2/projects/{test_id}', json={'project': {'name': 'S'}}).status - - -def test_update_project_not_authorized(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': test_id, - 'authorizations': [{ - 'projectId': test_id, - 'authorizationLevel': 'VIEW' - }] - }) - mocker.patch.object(DB, 'update', return_value={}) - assert '403' in client.put(f'/v2/projects/{test_id}', json={'project': {'name': 'S'}}).status - - -def test_update_project(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': test_id, - 'authorizations': [{ - 'projectId': test_id, - 'authorizationLevel': 'OWN' - }] - }) - mocker.patch.object(DB, 'update', return_value={}) - - res = client.put(f'/v2/projects/{test_id}', json={'project': {'name': 'S'}}) - assert '200' in res.status - - -def test_delete_project_non_existing(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value=None) - assert '404' in client.delete(f'/v2/projects/{test_id}').status - - -def test_delete_project_different_user(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': test_id, - 'googleId': 'other_test', - 'authorizations': [{ - 'projectId': test_id, - 'authorizationLevel': 'VIEW' - }], - 'topologyIds': [] - }) - mocker.patch.object(DB, 'delete_one', return_value=None) - assert '403' in client.delete(f'/v2/projects/{test_id}').status - - -def test_delete_project(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': test_id, - 'googleId': 'test', - 'authorizations': [{ - 'projectId': test_id, - 'authorizationLevel': 'OWN' - }], - 'topologyIds': [], - 'portfolioIds': [], - }) - mocker.patch.object(DB, 'update', return_value=None) - mocker.patch.object(DB, 'delete_one', return_value={'googleId': 'test'}) - res = client.delete(f'/v2/projects/{test_id}') - assert '200' in res.status diff --git a/opendc-web/opendc-web-api/opendc/api/v2/projects/projectId/topologies/__init__.py b/opendc-web/opendc-web-api/opendc/api/v2/projects/projectId/topologies/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/opendc-web/opendc-web-api/opendc/api/v2/projects/projectId/topologies/__init__.py +++ /dev/null diff --git a/opendc-web/opendc-web-api/opendc/api/v2/projects/projectId/topologies/endpoint.py b/opendc-web/opendc-web-api/opendc/api/v2/projects/projectId/topologies/endpoint.py deleted file mode 100644 index 44a0d575..00000000 --- a/opendc-web/opendc-web-api/opendc/api/v2/projects/projectId/topologies/endpoint.py +++ /dev/null @@ -1,31 +0,0 @@ -from datetime import datetime - -from opendc.models.project import Project -from opendc.models.topology import Topology -from opendc.util.rest import Response -from opendc.util.database import Database - - -def POST(request): - """Add a new Topology to the specified project and return it""" - - request.check_required_parameters(path={'projectId': 'string'}, body={'topology': {'name': 'string'}}) - - project = Project.from_id(request.params_path['projectId']) - - project.check_exists() - project.check_user_access(request.google_id, True) - - topology = Topology({ - 'projectId': project.get_id(), - 'name': request.params_body['topology']['name'], - 'rooms': request.params_body['topology']['rooms'], - }) - - topology.insert() - - project.obj['topologyIds'].append(topology.get_id()) - project.set_property('datetimeLastEdited', Database.datetime_to_string(datetime.now())) - project.update() - - return Response(200, 'Successfully inserted topology.', topology.obj) diff --git a/opendc-web/opendc-web-api/opendc/api/v2/projects/projectId/topologies/test_endpoint.py b/opendc-web/opendc-web-api/opendc/api/v2/projects/projectId/topologies/test_endpoint.py deleted file mode 100644 index 71e88f00..00000000 --- a/opendc-web/opendc-web-api/opendc/api/v2/projects/projectId/topologies/test_endpoint.py +++ /dev/null @@ -1,52 +0,0 @@ -from opendc.util.database import DB - -test_id = 24 * '1' - - -def test_add_topology_missing_parameter(client): - assert '400' in client.post(f'/v2/projects/{test_id}/topologies').status - - -def test_add_topology(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': test_id, - 'authorizations': [{ - 'projectId': test_id, - 'authorizationLevel': 'OWN' - }], - 'topologyIds': [] - }) - mocker.patch.object(DB, - 'insert', - return_value={ - '_id': test_id, - 'datetimeCreated': '000', - 'datetimeLastEdited': '000', - 'topologyIds': [] - }) - mocker.patch.object(DB, 'update', return_value={}) - res = client.post(f'/v2/projects/{test_id}/topologies', json={'topology': {'name': 'test project', 'rooms': []}}) - assert 'rooms' in res.json['content'] - assert '200' in res.status - - -def test_add_topology_not_authorized(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': test_id, - 'projectId': test_id, - 'authorizations': [{ - 'projectId': test_id, - 'authorizationLevel': 'VIEW' - }] - }) - assert '403' in client.post(f'/v2/projects/{test_id}/topologies', - json={ - 'topology': { - 'name': 'test_topology', - 'rooms': {} - } - }).status diff --git a/opendc-web/opendc-web-api/opendc/api/v2/projects/test_endpoint.py b/opendc-web/opendc-web-api/opendc/api/v2/projects/test_endpoint.py deleted file mode 100644 index 9444b1e4..00000000 --- a/opendc-web/opendc-web-api/opendc/api/v2/projects/test_endpoint.py +++ /dev/null @@ -1,25 +0,0 @@ -from opendc.util.database import DB - -test_id = 24 * '1' - - -def test_add_project_missing_parameter(client): - assert '400' in client.post('/v2/projects').status - - -def test_add_project(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value={'_id': test_id, 'authorizations': []}) - mocker.patch.object(DB, - 'insert', - return_value={ - '_id': test_id, - 'datetimeCreated': '000', - 'datetimeLastEdited': '000', - 'topologyIds': [] - }) - mocker.patch.object(DB, 'update', return_value={}) - res = client.post('/v2/projects', json={'project': {'name': 'test project'}}) - assert 'datetimeCreated' in res.json['content'] - assert 'datetimeLastEdited' in res.json['content'] - assert 'topologyIds' in res.json['content'] - assert '200' in res.status diff --git a/opendc-web/opendc-web-api/opendc/api/v2/scenarios/__init__.py b/opendc-web/opendc-web-api/opendc/api/v2/scenarios/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/opendc-web/opendc-web-api/opendc/api/v2/scenarios/__init__.py +++ /dev/null diff --git a/opendc-web/opendc-web-api/opendc/api/v2/scenarios/scenarioId/__init__.py b/opendc-web/opendc-web-api/opendc/api/v2/scenarios/scenarioId/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/opendc-web/opendc-web-api/opendc/api/v2/scenarios/scenarioId/__init__.py +++ /dev/null diff --git a/opendc-web/opendc-web-api/opendc/api/v2/scenarios/scenarioId/endpoint.py b/opendc-web/opendc-web-api/opendc/api/v2/scenarios/scenarioId/endpoint.py deleted file mode 100644 index 88a74e9c..00000000 --- a/opendc-web/opendc-web-api/opendc/api/v2/scenarios/scenarioId/endpoint.py +++ /dev/null @@ -1,59 +0,0 @@ -from opendc.models.scenario import Scenario -from opendc.models.portfolio import Portfolio -from opendc.util.rest import Response - - -def GET(request): - """Get this Scenario.""" - - request.check_required_parameters(path={'scenarioId': 'string'}) - - scenario = Scenario.from_id(request.params_path['scenarioId']) - - scenario.check_exists() - scenario.check_user_access(request.google_id, False) - - return Response(200, 'Successfully retrieved scenario.', scenario.obj) - - -def PUT(request): - """Update this Scenarios name.""" - - request.check_required_parameters(path={'scenarioId': 'string'}, body={'scenario': { - 'name': 'string', - }}) - - scenario = Scenario.from_id(request.params_path['scenarioId']) - - scenario.check_exists() - scenario.check_user_access(request.google_id, True) - - scenario.set_property('name', - request.params_body['scenario']['name']) - - scenario.update() - - return Response(200, 'Successfully updated scenario.', scenario.obj) - - -def DELETE(request): - """Delete this Scenario.""" - - request.check_required_parameters(path={'scenarioId': 'string'}) - - scenario = Scenario.from_id(request.params_path['scenarioId']) - - scenario.check_exists() - scenario.check_user_access(request.google_id, 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() - - return Response(200, 'Successfully deleted scenario.', old_object) diff --git a/opendc-web/opendc-web-api/opendc/api/v2/scenarios/scenarioId/test_endpoint.py b/opendc-web/opendc-web-api/opendc/api/v2/scenarios/scenarioId/test_endpoint.py deleted file mode 100644 index cd4bcdf8..00000000 --- a/opendc-web/opendc-web-api/opendc/api/v2/scenarios/scenarioId/test_endpoint.py +++ /dev/null @@ -1,149 +0,0 @@ -from opendc.util.database import DB - -test_id = 24 * '1' -test_id_2 = 24 * '2' - - -def test_get_scenario_non_existing(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value=None) - assert '404' in client.get(f'/v2/scenarios/{test_id}').status - - -def test_get_scenario_no_authorizations(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value={ - 'portfolioId': '1', - 'authorizations': [] - }) - res = client.get(f'/v2/scenarios/{test_id}') - assert '403' in res.status - - -def test_get_scenario_not_authorized(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - 'projectId': test_id, - 'portfolioId': test_id, - '_id': test_id, - 'authorizations': [{ - 'projectId': test_id_2, - 'authorizationLevel': 'OWN' - }] - }) - res = client.get(f'/v2/scenarios/{test_id}') - assert '403' in res.status - - -def test_get_scenario(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - 'projectId': test_id, - 'portfolioId': test_id, - '_id': test_id, - 'authorizations': [{ - 'projectId': test_id, - 'authorizationLevel': 'EDIT' - }] - }) - res = client.get(f'/v2/scenarios/{test_id}') - assert '200' in res.status - - -def test_update_scenario_missing_parameter(client): - assert '400' in client.put(f'/v2/scenarios/{test_id}').status - - -def test_update_scenario_non_existing(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value=None) - assert '404' in client.put(f'/v2/scenarios/{test_id}', json={ - 'scenario': { - 'name': 'test', - } - }).status - - -def test_update_scenario_not_authorized(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': test_id, - 'projectId': test_id, - 'portfolioId': test_id, - 'authorizations': [{ - 'projectId': test_id, - 'authorizationLevel': 'VIEW' - }] - }) - mocker.patch.object(DB, 'update', return_value={}) - assert '403' in client.put(f'/v2/scenarios/{test_id}', json={ - 'scenario': { - 'name': 'test', - } - }).status - - -def test_update_scenario(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': test_id, - 'projectId': test_id, - 'portfolioId': test_id, - 'authorizations': [{ - 'projectId': test_id, - 'authorizationLevel': 'OWN' - }], - 'targets': { - 'enabledMetrics': [], - 'repeatsPerScenario': 1 - } - }) - mocker.patch.object(DB, 'update', return_value={}) - - res = client.put(f'/v2/scenarios/{test_id}', json={'scenario': { - 'name': 'test', - }}) - assert '200' in res.status - - -def test_delete_project_non_existing(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value=None) - assert '404' in client.delete(f'/v2/scenarios/{test_id}').status - - -def test_delete_project_different_user(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': test_id, - 'projectId': test_id, - 'portfolioId': test_id, - 'googleId': 'other_test', - 'authorizations': [{ - 'projectId': test_id, - 'authorizationLevel': 'VIEW' - }] - }) - mocker.patch.object(DB, 'delete_one', return_value=None) - assert '403' in client.delete(f'/v2/scenarios/{test_id}').status - - -def test_delete_project(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': test_id, - 'projectId': test_id, - 'portfolioId': test_id, - 'googleId': 'test', - 'scenarioIds': [test_id], - 'authorizations': [{ - 'projectId': test_id, - 'authorizationLevel': 'OWN' - }] - }) - mocker.patch.object(DB, 'delete_one', return_value={}) - mocker.patch.object(DB, 'update', return_value=None) - res = client.delete(f'/v2/scenarios/{test_id}') - assert '200' in res.status diff --git a/opendc-web/opendc-web-api/opendc/api/v2/schedulers/__init__.py b/opendc-web/opendc-web-api/opendc/api/v2/schedulers/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/opendc-web/opendc-web-api/opendc/api/v2/schedulers/__init__.py +++ /dev/null diff --git a/opendc-web/opendc-web-api/opendc/api/v2/schedulers/endpoint.py b/opendc-web/opendc-web-api/opendc/api/v2/schedulers/endpoint.py deleted file mode 100644 index f33159bf..00000000 --- a/opendc-web/opendc-web-api/opendc/api/v2/schedulers/endpoint.py +++ /dev/null @@ -1,19 +0,0 @@ -from opendc.util.rest import Response - -SCHEDULERS = [ - 'mem', - 'mem-inv', - 'core-mem', - 'core-mem-inv', - 'active-servers', - 'active-servers-inv', - 'provisioned-cores', - 'provisioned-cores-inv', - 'random' -] - - -def GET(_): - """Get all available Schedulers.""" - - return Response(200, 'Successfully retrieved Schedulers.', [{'name': name} for name in SCHEDULERS]) diff --git a/opendc-web/opendc-web-api/opendc/api/v2/schedulers/test_endpoint.py b/opendc-web/opendc-web-api/opendc/api/v2/schedulers/test_endpoint.py deleted file mode 100644 index 4950ca4c..00000000 --- a/opendc-web/opendc-web-api/opendc/api/v2/schedulers/test_endpoint.py +++ /dev/null @@ -1,2 +0,0 @@ -def test_get_schedulers(client): - assert '200' in client.get('/v2/schedulers').status diff --git a/opendc-web/opendc-web-api/opendc/api/v2/topologies/__init__.py b/opendc-web/opendc-web-api/opendc/api/v2/topologies/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/opendc-web/opendc-web-api/opendc/api/v2/topologies/__init__.py +++ /dev/null diff --git a/opendc-web/opendc-web-api/opendc/api/v2/topologies/topologyId/__init__.py b/opendc-web/opendc-web-api/opendc/api/v2/topologies/topologyId/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/opendc-web/opendc-web-api/opendc/api/v2/topologies/topologyId/__init__.py +++ /dev/null diff --git a/opendc-web/opendc-web-api/opendc/api/v2/topologies/topologyId/endpoint.py b/opendc-web/opendc-web-api/opendc/api/v2/topologies/topologyId/endpoint.py deleted file mode 100644 index ea82b2e2..00000000 --- a/opendc-web/opendc-web-api/opendc/api/v2/topologies/topologyId/endpoint.py +++ /dev/null @@ -1,58 +0,0 @@ -from datetime import datetime - -from opendc.util.database import Database -from opendc.models.project import Project -from opendc.models.topology import Topology -from opendc.util.rest import Response - - -def GET(request): - """Get this Topology.""" - - request.check_required_parameters(path={'topologyId': 'string'}) - - topology = Topology.from_id(request.params_path['topologyId']) - - topology.check_exists() - topology.check_user_access(request.google_id, False) - - return Response(200, 'Successfully retrieved topology.', topology.obj) - - -def PUT(request): - """Update this topology""" - request.check_required_parameters(path={'topologyId': 'string'}, body={'topology': {'name': 'string', 'rooms': {}}}) - topology = Topology.from_id(request.params_path['topologyId']) - - topology.check_exists() - topology.check_user_access(request.google_id, True) - - topology.set_property('name', request.params_body['topology']['name']) - topology.set_property('rooms', request.params_body['topology']['rooms']) - topology.set_property('datetimeLastEdited', Database.datetime_to_string(datetime.now())) - - topology.update() - - return Response(200, 'Successfully updated topology.', topology.obj) - - -def DELETE(request): - """Delete this topology""" - request.check_required_parameters(path={'topologyId': 'string'}) - - topology = Topology.from_id(request.params_path['topologyId']) - - topology.check_exists() - topology.check_user_access(request.google_id, 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() - - return Response(200, 'Successfully deleted topology.', old_object) diff --git a/opendc-web/opendc-web-api/opendc/api/v2/topologies/topologyId/test_endpoint.py b/opendc-web/opendc-web-api/opendc/api/v2/topologies/topologyId/test_endpoint.py deleted file mode 100644 index 4da0bc64..00000000 --- a/opendc-web/opendc-web-api/opendc/api/v2/topologies/topologyId/test_endpoint.py +++ /dev/null @@ -1,119 +0,0 @@ -from opendc.util.database import DB - -test_id = 24 * '1' -test_id_2 = 24 * '2' - - -def test_get_topology(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': test_id, - 'projectId': test_id, - 'authorizations': [{ - 'projectId': test_id, - 'authorizationLevel': 'EDIT' - }] - }) - res = client.get(f'/v2/topologies/{test_id}') - assert '200' in res.status - - -def test_get_topology_non_existing(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value=None) - assert '404' in client.get('/v2/topologies/1').status - - -def test_get_topology_not_authorized(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': test_id, - 'projectId': test_id, - 'authorizations': [{ - 'projectId': test_id_2, - 'authorizationLevel': 'OWN' - }] - }) - res = client.get(f'/v2/topologies/{test_id}') - assert '403' in res.status - - -def test_get_topology_no_authorizations(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value={'projectId': test_id, 'authorizations': []}) - res = client.get(f'/v2/topologies/{test_id}') - assert '403' in res.status - - -def test_update_topology_missing_parameter(client): - assert '400' in client.put(f'/v2/topologies/{test_id}').status - - -def test_update_topology_non_existent(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value=None) - assert '404' in client.put(f'/v2/topologies/{test_id}', json={'topology': {'name': 'test_topology', 'rooms': {}}}).status - - -def test_update_topology_not_authorized(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': test_id, - 'projectId': test_id, - 'authorizations': [{ - 'projectId': test_id, - 'authorizationLevel': 'VIEW' - }] - }) - mocker.patch.object(DB, 'update', return_value={}) - assert '403' in client.put(f'/v2/topologies/{test_id}', json={ - 'topology': { - 'name': 'updated_topology', - 'rooms': {} - } - }).status - - -def test_update_topology(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': test_id, - 'projectId': test_id, - 'authorizations': [{ - 'projectId': test_id, - 'authorizationLevel': 'OWN' - }] - }) - mocker.patch.object(DB, 'update', return_value={}) - - assert '200' in client.put(f'/v2/topologies/{test_id}', json={ - 'topology': { - 'name': 'updated_topology', - 'rooms': {} - } - }).status - - -def test_delete_topology(client, mocker): - mocker.patch.object(DB, - 'fetch_one', - return_value={ - '_id': test_id, - 'projectId': test_id, - 'googleId': 'test', - 'topologyIds': [test_id], - 'authorizations': [{ - 'projectId': test_id, - 'authorizationLevel': 'OWN' - }] - }) - mocker.patch.object(DB, 'delete_one', return_value={}) - mocker.patch.object(DB, 'update', return_value=None) - res = client.delete(f'/v2/topologies/{test_id}') - assert '200' in res.status - - -def test_delete_nonexistent_topology(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value=None) - assert '404' in client.delete(f'/v2/topologies/{test_id}').status diff --git a/opendc-web/opendc-web-api/opendc/api/v2/traces/__init__.py b/opendc-web/opendc-web-api/opendc/api/v2/traces/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/opendc-web/opendc-web-api/opendc/api/v2/traces/__init__.py +++ /dev/null diff --git a/opendc-web/opendc-web-api/opendc/api/v2/traces/endpoint.py b/opendc-web/opendc-web-api/opendc/api/v2/traces/endpoint.py deleted file mode 100644 index ee699e02..00000000 --- a/opendc-web/opendc-web-api/opendc/api/v2/traces/endpoint.py +++ /dev/null @@ -1,10 +0,0 @@ -from opendc.models.trace import Trace -from opendc.util.rest import Response - - -def GET(_): - """Get all available Traces.""" - - traces = Trace.get_all() - - return Response(200, 'Successfully retrieved Traces', traces.obj) diff --git a/opendc-web/opendc-web-api/opendc/api/v2/traces/test_endpoint.py b/opendc-web/opendc-web-api/opendc/api/v2/traces/test_endpoint.py deleted file mode 100644 index 36846bd9..00000000 --- a/opendc-web/opendc-web-api/opendc/api/v2/traces/test_endpoint.py +++ /dev/null @@ -1,6 +0,0 @@ -from opendc.util.database import DB - - -def test_get_traces(client, mocker): - mocker.patch.object(DB, 'fetch_all', return_value=[]) - assert '200' in client.get('/v2/traces').status diff --git a/opendc-web/opendc-web-api/opendc/api/v2/traces/traceId/__init__.py b/opendc-web/opendc-web-api/opendc/api/v2/traces/traceId/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/opendc-web/opendc-web-api/opendc/api/v2/traces/traceId/__init__.py +++ /dev/null diff --git a/opendc-web/opendc-web-api/opendc/api/v2/traces/traceId/endpoint.py b/opendc-web/opendc-web-api/opendc/api/v2/traces/traceId/endpoint.py deleted file mode 100644 index 670f88d1..00000000 --- a/opendc-web/opendc-web-api/opendc/api/v2/traces/traceId/endpoint.py +++ /dev/null @@ -1,14 +0,0 @@ -from opendc.models.trace import Trace -from opendc.util.rest import Response - - -def GET(request): - """Get this Trace.""" - - request.check_required_parameters(path={'traceId': 'string'}) - - trace = Trace.from_id(request.params_path['traceId']) - - trace.check_exists() - - return Response(200, 'Successfully retrieved trace.', trace.obj) diff --git a/opendc-web/opendc-web-api/opendc/api/v2/traces/traceId/test_endpoint.py b/opendc-web/opendc-web-api/opendc/api/v2/traces/traceId/test_endpoint.py deleted file mode 100644 index 0c51538b..00000000 --- a/opendc-web/opendc-web-api/opendc/api/v2/traces/traceId/test_endpoint.py +++ /dev/null @@ -1,15 +0,0 @@ -from opendc.util.database import DB - -test_id = 24 * '1' - - -def test_get_trace_non_existing(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value=None) - assert '404' in client.get(f'/v2/traces/{test_id}').status - - -def test_get_trace(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value={'name': 'test trace'}) - res = client.get(f'/v2/traces/{test_id}') - assert 'name' in res.json['content'] - assert '200' in res.status diff --git a/opendc-web/opendc-web-api/opendc/api/v2/users/__init__.py b/opendc-web/opendc-web-api/opendc/api/v2/users/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/opendc-web/opendc-web-api/opendc/api/v2/users/__init__.py +++ /dev/null diff --git a/opendc-web/opendc-web-api/opendc/api/v2/users/endpoint.py b/opendc-web/opendc-web-api/opendc/api/v2/users/endpoint.py deleted file mode 100644 index 0dcf2463..00000000 --- a/opendc-web/opendc-web-api/opendc/api/v2/users/endpoint.py +++ /dev/null @@ -1,30 +0,0 @@ -from opendc.models.user import User -from opendc.util.rest import Response - - -def GET(request): - """Search for a User using their email address.""" - - request.check_required_parameters(query={'email': 'string'}) - - user = User.from_email(request.params_query['email']) - - user.check_exists() - - return Response(200, 'Successfully retrieved user.', user.obj) - - -def POST(request): - """Add a new User.""" - - request.check_required_parameters(body={'user': {'email': 'string'}}) - - user = User(request.params_body['user']) - user.set_property('googleId', request.google_id) - user.set_property('authorizations', []) - - user.check_already_exists() - - user.insert() - - return Response(200, 'Successfully created user.', user.obj) diff --git a/opendc-web/opendc-web-api/opendc/api/v2/users/test_endpoint.py b/opendc-web/opendc-web-api/opendc/api/v2/users/test_endpoint.py deleted file mode 100644 index 13b63b20..00000000 --- a/opendc-web/opendc-web-api/opendc/api/v2/users/test_endpoint.py +++ /dev/null @@ -1,34 +0,0 @@ -from opendc.util.database import DB - - -def test_get_user_by_email_missing_parameter(client): - assert '400' in client.get('/v2/users').status - - -def test_get_user_by_email_non_existing(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value=None) - assert '404' in client.get('/v2/users?email=test@test.com').status - - -def test_get_user_by_email(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value={'email': 'test@test.com'}) - res = client.get('/v2/users?email=test@test.com') - assert 'email' in res.json['content'] - assert '200' in res.status - - -def test_add_user_missing_parameter(client): - assert '400' in client.post('/v2/users').status - - -def test_add_user_existing(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value={'email': 'test@test.com'}) - assert '409' in client.post('/v2/users', json={'user': {'email': 'test@test.com'}}).status - - -def test_add_user(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value=None) - mocker.patch.object(DB, 'insert', return_value={'email': 'test@test.com'}) - res = client.post('/v2/users', json={'user': {'email': 'test@test.com'}}) - assert 'email' in res.json['content'] - assert '200' in res.status diff --git a/opendc-web/opendc-web-api/opendc/api/v2/users/userId/__init__.py b/opendc-web/opendc-web-api/opendc/api/v2/users/userId/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/opendc-web/opendc-web-api/opendc/api/v2/users/userId/__init__.py +++ /dev/null diff --git a/opendc-web/opendc-web-api/opendc/api/v2/users/userId/endpoint.py b/opendc-web/opendc-web-api/opendc/api/v2/users/userId/endpoint.py deleted file mode 100644 index be3462c0..00000000 --- a/opendc-web/opendc-web-api/opendc/api/v2/users/userId/endpoint.py +++ /dev/null @@ -1,59 +0,0 @@ -from opendc.models.project import Project -from opendc.models.user import User -from opendc.util.rest import Response - - -def GET(request): - """Get this User.""" - - request.check_required_parameters(path={'userId': 'string'}) - - user = User.from_id(request.params_path['userId']) - - user.check_exists() - - return Response(200, 'Successfully retrieved user.', user.obj) - - -def PUT(request): - """Update this User's given name and/or family name.""" - - request.check_required_parameters(body={'user': { - 'givenName': 'string', - 'familyName': 'string' - }}, - path={'userId': 'string'}) - - user = User.from_id(request.params_path['userId']) - - user.check_exists() - user.check_correct_user(request.google_id) - - user.set_property('givenName', request.params_body['user']['givenName']) - user.set_property('familyName', request.params_body['user']['familyName']) - - user.update() - - return Response(200, 'Successfully updated user.', user.obj) - - -def DELETE(request): - """Delete this User.""" - - request.check_required_parameters(path={'userId': 'string'}) - - user = User.from_id(request.params_path['userId']) - - user.check_exists() - user.check_correct_user(request.google_id) - - for authorization in user.obj['authorizations']: - if authorization['authorizationLevel'] != 'OWN': - continue - - project = Project.from_id(authorization['projectId']) - project.delete() - - old_object = user.delete() - - return Response(200, 'Successfully deleted user.', old_object) diff --git a/opendc-web/opendc-web-api/opendc/api/v2/users/userId/test_endpoint.py b/opendc-web/opendc-web-api/opendc/api/v2/users/userId/test_endpoint.py deleted file mode 100644 index 4085642f..00000000 --- a/opendc-web/opendc-web-api/opendc/api/v2/users/userId/test_endpoint.py +++ /dev/null @@ -1,56 +0,0 @@ -from opendc.util.database import DB - -test_id = 24 * '1' - - -def test_get_user_non_existing(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value=None) - assert '404' in client.get(f'/v2/users/{test_id}').status - - -def test_get_user(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value={'email': 'test@test.com'}) - res = client.get(f'/v2/users/{test_id}') - assert 'email' in res.json['content'] - assert '200' in res.status - - -def test_update_user_missing_parameter(client): - assert '400' in client.put(f'/v2/users/{test_id}').status - - -def test_update_user_non_existing(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value=None) - assert '404' in client.put(f'/v2/users/{test_id}', json={'user': {'givenName': 'A', 'familyName': 'B'}}).status - - -def test_update_user_different_user(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value={'_id': test_id, 'googleId': 'other_test'}) - assert '403' in client.put(f'/v2/users/{test_id}', json={'user': {'givenName': 'A', 'familyName': 'B'}}).status - - -def test_update_user(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value={'_id': test_id, 'googleId': 'test'}) - mocker.patch.object(DB, 'update', return_value={'givenName': 'A', 'familyName': 'B'}) - res = client.put(f'/v2/users/{test_id}', json={'user': {'givenName': 'A', 'familyName': 'B'}}) - assert 'givenName' in res.json['content'] - assert '200' in res.status - - -def test_delete_user_non_existing(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value=None) - assert '404' in client.delete(f'/v2/users/{test_id}').status - - -def test_delete_user_different_user(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value={'_id': test_id, 'googleId': 'other_test'}) - assert '403' in client.delete(f'/v2/users/{test_id}').status - - -def test_delete_user(client, mocker): - mocker.patch.object(DB, 'fetch_one', return_value={'_id': test_id, 'googleId': 'test', 'authorizations': []}) - mocker.patch.object(DB, 'delete_one', return_value={'googleId': 'test'}) - res = client.delete(f'/v2/users/{test_id}', ) - - assert 'googleId' in res.json['content'] - assert '200' in res.status diff --git a/opendc-web/opendc-web-api/opendc/auth.py b/opendc-web/opendc-web-api/opendc/auth.py new file mode 100644 index 00000000..1870f01c --- /dev/null +++ b/opendc-web/opendc-web-api/opendc/auth.py @@ -0,0 +1,240 @@ +# 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/util/database.py b/opendc-web/opendc-web-api/opendc/database.py index dd26533d..f9a33b66 100644 --- a/opendc-web/opendc-web-api/opendc/util/database.py +++ b/opendc-web/opendc-web-api/opendc/database.py @@ -1,3 +1,23 @@ +# 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 datetime import datetime @@ -9,19 +29,27 @@ CONNECTION_POOL = None class Database: """Object holding functionality for database access.""" - def __init__(self): - self.opendc_db = None - - def initialize_database(self, user, password, database, host): + 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)) - self.opendc_db = client.opendc + 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 @@ -72,6 +100,3 @@ class Database: def string_to_datetime(string_to_convert): """Return a datetime corresponding to the given string representation.""" return datetime.strptime(string_to_convert, DATETIME_STRING_FORMAT) - - -DB = Database() diff --git a/opendc-web/opendc-web-api/opendc/exts.py b/opendc-web/opendc-web-api/opendc/exts.py new file mode 100644 index 00000000..f088a29c --- /dev/null +++ b/opendc-web/opendc-web-api/opendc/exts.py @@ -0,0 +1,60 @@ +import os +from functools import wraps + +from flask import g, _request_ctx_stack +from werkzeug.local import LocalProxy + +from opendc.database import Database +from opendc.auth import AuthContext, AsymmetricJwtAlgorithm, get_token + + +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)) diff --git a/opendc-web/opendc-web-api/opendc/models/model.py b/opendc-web/opendc-web-api/opendc/models/model.py index f9dfc9ad..28299453 100644 --- a/opendc-web/opendc-web-api/opendc/models/model.py +++ b/opendc-web/opendc-web-api/opendc/models/model.py @@ -1,8 +1,7 @@ from bson.objectid import ObjectId +from werkzeug.exceptions import NotFound -from opendc.util.database import DB -from opendc.util.exceptions import ClientError -from opendc.util.rest import Response +from opendc.exts import db class Model: @@ -15,15 +14,13 @@ class Model: """Fetches the document with given ID from the collection.""" if isinstance(_id, str) and len(_id) == 24: _id = ObjectId(_id) - elif not isinstance(_id, ObjectId): - return cls(None) - return cls(DB.fetch_one({'_id': _id}, cls.collection_name)) + 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)) + return cls(db.fetch_all({}, cls.collection_name)) def __init__(self, obj): self.obj = obj @@ -35,7 +32,7 @@ class Model: def check_exists(self): """Raises an error if the enclosed object does not exist.""" if self.obj is None: - raise ClientError(Response(404, 'Not found.')) + 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.""" @@ -48,11 +45,11 @@ class Model: def insert(self): """Inserts the enclosed object and generates a UUID for it.""" self.obj['_id'] = ObjectId() - DB.insert(self.obj, self.collection_name) + 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) + db.update(self.get_id(), self.obj, self.collection_name) def delete(self): """Deletes the enclosed object in the database, if it existed.""" @@ -60,5 +57,5 @@ class Model: return None old_object = self.obj.copy() - DB.delete_one({'_id': self.get_id()}, self.collection_name) + 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 index 32961b63..aff1d3f0 100644 --- a/opendc-web/opendc-web-api/opendc/models/portfolio.py +++ b/opendc-web/opendc-web-api/opendc/models/portfolio.py @@ -1,7 +1,26 @@ +from marshmallow import Schema, fields + +from opendc.models.project import Project from opendc.models.model import Model -from opendc.models.user import User -from opendc.util.exceptions import ClientError -from opendc.util.rest import Response + + +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() + projectId = fields.String() + name = fields.String(required=True) + scenarioIds = fields.List(fields.String()) + targets = fields.Nested(TargetSchema) class Portfolio(Model): @@ -9,16 +28,13 @@ class Portfolio(Model): collection_name = 'portfolios' - def check_user_access(self, google_id, edit_access): - """Raises an error if the user with given [google_id] has insufficient access. + 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 google_id: The Google ID of the user. + :param user_id: The User ID of the user. :param edit_access: True when edit access should be checked, otherwise view access. """ - user = User.from_google_id(google_id) - authorizations = list( - filter(lambda x: str(x['projectId']) == str(self.obj['projectId']), user.obj['authorizations'])) - if len(authorizations) == 0 or (edit_access and authorizations[0]['authorizationLevel'] == 'VIEW'): - raise ClientError(Response(403, 'Forbidden from retrieving/editing portfolio.')) + project = Project.from_id(self.obj['projectId']) + project.check_user_access(user_id, edit_access) diff --git a/opendc-web/opendc-web-api/opendc/models/prefab.py b/opendc-web/opendc-web-api/opendc/models/prefab.py index edf1d4c4..d83ef4cb 100644 --- a/opendc-web/opendc-web-api/opendc/models/prefab.py +++ b/opendc-web/opendc-web-api/opendc/models/prefab.py @@ -1,28 +1,30 @@ +from marshmallow import Schema, fields +from werkzeug.exceptions import Forbidden + +from opendc.models.topology import ObjectSchema from opendc.models.model import Model -from opendc.models.user import User -from opendc.util.exceptions import ClientError -from opendc.util.rest import Response + + +class PrefabSchema(Schema): + """ + Schema for a Prefab. + """ + _id = fields.String() + name = fields.String(required=True) + datetimeCreated = fields.DateTime() + datetimeLastEdited = fields.DateTime() + rack = fields.Nested(ObjectSchema) class Prefab(Model): - """Model representing a Project.""" + """Model representing a Prefab.""" collection_name = 'prefabs' - def check_user_access(self, google_id): - """Raises an error if the user with given [google_id] has insufficient access to view this prefab. + 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 google_id: The Google ID of the user. + :param user_id: The user ID of the user. """ - user = User.from_google_id(google_id) - - # TODO(Jacob) add special handling for OpenDC-provided prefabs - - #try: - - print(self.obj) - if self.obj['authorId'] != user.get_id() and self.obj['visibility'] == "private": - raise ClientError(Response(403, "Forbidden from retrieving prefab.")) - #except KeyError: - # OpenDC-authored objects don't necessarily have an authorId - # return + 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 index b57e9f77..ee84c73e 100644 --- a/opendc-web/opendc-web-api/opendc/models/project.py +++ b/opendc-web/opendc-web-api/opendc/models/project.py @@ -1,8 +1,20 @@ +from marshmallow import Schema, fields +from werkzeug.exceptions import Forbidden + from opendc.models.model import Model -from opendc.models.user import User -from opendc.util.database import DB -from opendc.util.exceptions import ClientError -from opendc.util.rest import Response +from opendc.exts import db + + +class ProjectSchema(Schema): + """ + Schema representing a Project. + """ + _id = fields.String() + name = fields.String(required=True) + datetimeCreated = fields.DateTime() + datetimeLastEdited = fields.DateTime() + topologyIds = fields.List(fields.String()) + portfolioIds = fields.List(fields.String()) class Project(Model): @@ -10,22 +22,18 @@ class Project(Model): collection_name = 'projects' - def check_user_access(self, google_id, edit_access): - """Raises an error if the user with given [google_id] has insufficient access. + def check_user_access(self, user_id, edit_access): + """Raises an error if the user with given [user_id] has insufficient access. - :param google_id: The Google ID of the user. + :param user_id: The User ID of the user. :param edit_access: True when edit access should be checked, otherwise view access. """ - user = User.from_google_id(google_id) - authorizations = list(filter(lambda x: str(x['projectId']) == str(self.get_id()), - user.obj['authorizations'])) - if len(authorizations) == 0 or (edit_access and authorizations[0]['authorizationLevel'] == 'VIEW'): - raise ClientError(Response(403, "Forbidden from retrieving project.")) - - def get_all_authorizations(self): - """Get all user IDs having access to this project.""" - return [ - str(user['_id']) for user in DB.fetch_all({'authorizations': { - 'projectId': self.obj['_id'] - }}, User.collection_name) - ] + 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 index 8d53e408..2911b1ae 100644 --- a/opendc-web/opendc-web-api/opendc/models/scenario.py +++ b/opendc-web/opendc-web-api/opendc/models/scenario.py @@ -1,8 +1,50 @@ +from marshmallow import Schema, fields from opendc.models.model import Model from opendc.models.portfolio import Portfolio -from opendc.models.user import User -from opendc.util.exceptions import ClientError -from opendc.util.rest import Response + + +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() + portfolioId = fields.String() + name = fields.String(required=True) + simulation = fields.Nested(SimulationSchema) + trace = fields.Nested(TraceSchema) + topology = fields.Nested(TopologySchema) + operational = fields.Nested(OperationalSchema) class Scenario(Model): @@ -10,17 +52,13 @@ class Scenario(Model): collection_name = 'scenarios' - def check_user_access(self, google_id, edit_access): - """Raises an error if the user with given [google_id] has insufficient access. + 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 google_id: The Google ID of the user. + :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']) - user = User.from_google_id(google_id) - authorizations = list( - filter(lambda x: str(x['projectId']) == str(portfolio.obj['projectId']), user.obj['authorizations'])) - if len(authorizations) == 0 or (edit_access and authorizations[0]['authorizationLevel'] == 'VIEW'): - raise ClientError(Response(403, 'Forbidden from retrieving/editing scenario.')) + portfolio.check_user_access(user_id, edit_access) diff --git a/opendc-web/opendc-web-api/opendc/models/topology.py b/opendc-web/opendc-web-api/opendc/models/topology.py index cb4c4bab..c6354ae6 100644 --- a/opendc-web/opendc-web-api/opendc/models/topology.py +++ b/opendc-web/opendc-web-api/opendc/models/topology.py @@ -1,7 +1,81 @@ +from marshmallow import Schema, fields + +from opendc.models.project import Project from opendc.models.model import Model -from opendc.models.user import User -from opendc.util.exceptions import ClientError -from opendc.util.rest import Response + + +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)) + + +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)) + + +class TileSchema(Schema): + """ + Schema representing a room tile. + """ + _id = fields.String() + positionX = fields.Integer() + positionY = fields.Integer() + rack = fields.Nested(ObjectSchema) + + +class RoomSchema(Schema): + """ + Schema representing a room. + """ + _id = fields.String() + name = fields.String(required=True) + tiles = fields.List(fields.Nested(TileSchema), required=True) + + +class TopologySchema(Schema): + """ + Schema representing a datacenter topology. + """ + _id = fields.String() + projectId = fields.String() + name = fields.String(required=True) + rooms = fields.List(fields.Nested(RoomSchema), required=True) class Topology(Model): @@ -9,19 +83,13 @@ class Topology(Model): collection_name = 'topologies' - def check_user_access(self, google_id, edit_access): - """Raises an error if the user with given [google_id] has insufficient access. + 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 google_id: The Google ID of the user. + :param user_id: The User ID of the user. :param edit_access: True when edit access should be checked, otherwise view access. """ - user = User.from_google_id(google_id) - if 'projectId' not in self.obj: - raise ClientError(Response(400, 'Missing projectId in topology.')) - - authorizations = list( - filter(lambda x: str(x['projectId']) == str(self.obj['projectId']), user.obj['authorizations'])) - if len(authorizations) == 0 or (edit_access and authorizations[0]['authorizationLevel'] == 'VIEW'): - raise ClientError(Response(403, 'Forbidden from retrieving topology.')) + project = Project.from_id(self.obj['projectId']) + project.check_user_access(user_id, edit_access) diff --git a/opendc-web/opendc-web-api/opendc/models/user.py b/opendc-web/opendc-web-api/opendc/models/user.py deleted file mode 100644 index 8e8ff945..00000000 --- a/opendc-web/opendc-web-api/opendc/models/user.py +++ /dev/null @@ -1,36 +0,0 @@ -from opendc.models.model import Model -from opendc.util.database import DB -from opendc.util.exceptions import ClientError -from opendc.util.rest import Response - - -class User(Model): - """Model representing a User.""" - - collection_name = 'users' - - @classmethod - def from_email(cls, email): - """Fetches the user with given email from the collection.""" - return User(DB.fetch_one({'email': email}, User.collection_name)) - - @classmethod - def from_google_id(cls, google_id): - """Fetches the user with given Google ID from the collection.""" - return User(DB.fetch_one({'googleId': google_id}, User.collection_name)) - - def check_correct_user(self, request_google_id): - """Raises an error if a user tries to modify another user. - - :param request_google_id: - """ - if request_google_id is not None and self.obj['googleId'] != request_google_id: - raise ClientError(Response(403, f'Forbidden from editing user with ID {self.obj["_id"]}.')) - - def check_already_exists(self): - """Checks if the user already exists in the database.""" - - existing_user = DB.fetch_one({'googleId': self.obj['googleId']}, self.collection_name) - - if existing_user is not None: - raise ClientError(Response(409, 'User already exists.')) diff --git a/opendc-web/opendc-web-api/opendc/util.py b/opendc-web/opendc-web-api/opendc/util.py new file mode 100644 index 00000000..e7dc07a4 --- /dev/null +++ b/opendc-web/opendc-web-api/opendc/util.py @@ -0,0 +1,32 @@ +# 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) diff --git a/opendc-web/opendc-web-api/opendc/util/__init__.py b/opendc-web/opendc-web-api/opendc/util/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/opendc-web/opendc-web-api/opendc/util/__init__.py +++ /dev/null diff --git a/opendc-web/opendc-web-api/opendc/util/exceptions.py b/opendc-web/opendc-web-api/opendc/util/exceptions.py deleted file mode 100644 index 7724a407..00000000 --- a/opendc-web/opendc-web-api/opendc/util/exceptions.py +++ /dev/null @@ -1,64 +0,0 @@ -class RequestInitializationError(Exception): - """Raised when a Request cannot successfully be initialized""" - - -class UnimplementedEndpointError(RequestInitializationError): - """Raised when a Request path does not point to a module.""" - - -class MissingRequestParameterError(RequestInitializationError): - """Raised when a Request does not contain one or more required parameters.""" - - -class UnsupportedMethodError(RequestInitializationError): - """Raised when a Request does not use a supported REST method. - - The method must be in all-caps, supported by REST, and implemented by the module. - """ - - -class AuthorizationTokenError(RequestInitializationError): - """Raised when an authorization token is not correctly verified.""" - - -class ForeignKeyError(Exception): - """Raised when a foreign key constraint is not met.""" - - -class RowNotFoundError(Exception): - """Raised when a database row is not found.""" - def __init__(self, table_name): - super(RowNotFoundError, self).__init__('Row in `{}` table not found.'.format(table_name)) - - self.table_name = table_name - - -class ParameterError(Exception): - """Raised when a parameter is either missing or incorrectly typed.""" - - -class IncorrectParameterError(ParameterError): - """Raised when a parameter is of the wrong type.""" - def __init__(self, parameter_name, parameter_location): - super(IncorrectParameterError, - self).__init__('Incorrectly typed `{}` {} parameter.'.format(parameter_name, parameter_location)) - - self.parameter_name = parameter_name - self.parameter_location = parameter_location - - -class MissingParameterError(ParameterError): - """Raised when a parameter is missing.""" - def __init__(self, parameter_name, parameter_location): - super(MissingParameterError, - self).__init__('Missing required `{}` {} parameter.'.format(parameter_name, parameter_location)) - - self.parameter_name = parameter_name - self.parameter_location = parameter_location - - -class ClientError(Exception): - """Raised when a 4xx response is to be returned.""" - def __init__(self, response): - super(ClientError, self).__init__(str(response)) - self.response = response diff --git a/opendc-web/opendc-web-api/opendc/util/json.py b/opendc-web/opendc-web-api/opendc/util/json.py deleted file mode 100644 index 2ef4f965..00000000 --- a/opendc-web/opendc-web-api/opendc/util/json.py +++ /dev/null @@ -1,12 +0,0 @@ -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) diff --git a/opendc-web/opendc-web-api/opendc/util/parameter_checker.py b/opendc-web/opendc-web-api/opendc/util/parameter_checker.py deleted file mode 100644 index 14dd1dc0..00000000 --- a/opendc-web/opendc-web-api/opendc/util/parameter_checker.py +++ /dev/null @@ -1,85 +0,0 @@ -from opendc.util import exceptions -from opendc.util.database import Database - - -def _missing_parameter(params_required, params_actual, parent=''): - """Recursively search for the first missing parameter.""" - - for param_name in params_required: - - if param_name not in params_actual: - return '{}.{}'.format(parent, param_name) - - param_required = params_required.get(param_name) - param_actual = params_actual.get(param_name) - - if isinstance(param_required, dict): - - param_missing = _missing_parameter(param_required, param_actual, param_name) - - if param_missing is not None: - return '{}.{}'.format(parent, param_missing) - - return None - - -def _incorrect_parameter(params_required, params_actual, parent=''): - """Recursively make sure each parameter is of the correct type.""" - - for param_name in params_required: - - param_required = params_required.get(param_name) - param_actual = params_actual.get(param_name) - - if isinstance(param_required, dict): - - param_incorrect = _incorrect_parameter(param_required, param_actual, param_name) - - if param_incorrect is not None: - return '{}.{}'.format(parent, param_incorrect) - - else: - - if param_required == 'datetime': - try: - Database.string_to_datetime(param_actual) - except: - return '{}.{}'.format(parent, param_name) - - type_pairs = [ - ('int', (int,)), - ('float', (float, int)), - ('bool', (bool,)), - ('string', (str, int)), - ('list', (list,)), - ] - - for str_type, actual_types in type_pairs: - if param_required == str_type and all(not isinstance(param_actual, t) - for t in actual_types): - return '{}.{}'.format(parent, param_name) - - return None - - -def _format_parameter(parameter): - """Format the output of a parameter check.""" - - parts = parameter.split('.') - inner = ['["{}"]'.format(x) for x in parts[2:]] - return parts[1] + ''.join(inner) - - -def check(request, **kwargs): - """Check if all required parameters are there.""" - - for location, params_required in kwargs.items(): - params_actual = getattr(request, 'params_{}'.format(location)) - - missing_parameter = _missing_parameter(params_required, params_actual) - if missing_parameter is not None: - raise exceptions.MissingParameterError(_format_parameter(missing_parameter), location) - - incorrect_parameter = _incorrect_parameter(params_required, params_actual) - if incorrect_parameter is not None: - raise exceptions.IncorrectParameterError(_format_parameter(incorrect_parameter), location) diff --git a/opendc-web/opendc-web-api/opendc/util/path_parser.py b/opendc-web/opendc-web-api/opendc/util/path_parser.py deleted file mode 100644 index c8452f20..00000000 --- a/opendc-web/opendc-web-api/opendc/util/path_parser.py +++ /dev/null @@ -1,36 +0,0 @@ -import json -import os - - -def parse(version, endpoint_path): - """Map an HTTP endpoint path to an API path""" - - # Get possible paths - with open(os.path.join(os.path.dirname(__file__), '..', 'api', '{}', 'paths.json').format(version)) as paths_file: - paths = json.load(paths_file) - - # Find API path that matches endpoint_path - endpoint_path_parts = endpoint_path.strip('/').split('/') - paths_parts = [x.strip('/').split('/') for x in paths if len(x.strip('/').split('/')) == len(endpoint_path_parts)] - path = None - - for path_parts in paths_parts: - found = True - for (endpoint_part, part) in zip(endpoint_path_parts, path_parts): - if not part.startswith('{') and endpoint_part != part: - found = False - break - if found: - path = path_parts - - if path is None: - return None - - # Extract path parameters - parameters = {} - - for (name, value) in zip(path, endpoint_path_parts): - if name.startswith('{'): - parameters[name.strip('{}')] = value - - return '{}/{}'.format(version, '/'.join(path)), parameters diff --git a/opendc-web/opendc-web-api/opendc/util/rest.py b/opendc-web/opendc-web-api/opendc/util/rest.py deleted file mode 100644 index c9e98295..00000000 --- a/opendc-web/opendc-web-api/opendc/util/rest.py +++ /dev/null @@ -1,141 +0,0 @@ -import importlib -import json -import os - -from oauth2client import client, crypt - -from opendc.util import exceptions, parameter_checker -from opendc.util.exceptions import ClientError - - -class Request: - """WebSocket message to REST request mapping.""" - def __init__(self, message=None): - """"Initialize a Request from a socket message.""" - - # Get the Request parameters from the message - - if message is None: - return - - try: - self.message = message - - self.id = message['id'] - - self.path = message['path'] - self.method = message['method'] - - self.params_body = message['parameters']['body'] - self.params_path = message['parameters']['path'] - self.params_query = message['parameters']['query'] - - self.token = message['token'] - - except KeyError as exception: - raise exceptions.MissingRequestParameterError(exception) - - # Parse the path and import the appropriate module - - try: - self.path = message['path'].strip('/') - - module_base = 'opendc.api.{}.endpoint' - module_path = self.path.replace('{', '').replace('}', '').replace('/', '.') - - self.module = importlib.import_module(module_base.format(module_path)) - except ImportError as e: - print(e) - raise exceptions.UnimplementedEndpointError('Unimplemented endpoint: {}.'.format(self.path)) - - # Check the method - - if self.method not in ['POST', 'GET', 'PUT', 'PATCH', 'DELETE']: - raise exceptions.UnsupportedMethodError('Non-rest method: {}'.format(self.method)) - - if not hasattr(self.module, self.method): - raise exceptions.UnsupportedMethodError('Unimplemented method at endpoint {}: {}'.format( - self.path, self.method)) - - # Verify the user - - if "OPENDC_FLASK_TESTING" in os.environ: - self.google_id = 'test' - return - - try: - self.google_id = self._verify_token(self.token) - except crypt.AppIdentityError as e: - raise exceptions.AuthorizationTokenError(e) - - def check_required_parameters(self, **kwargs): - """Raise an error if a parameter is missing or of the wrong type.""" - - try: - parameter_checker.check(self, **kwargs) - except exceptions.ParameterError as e: - raise ClientError(Response(400, str(e))) - - def process(self): - """Process the Request and return a Response.""" - - method = getattr(self.module, self.method) - - try: - response = method(self) - except ClientError as e: - e.response.id = self.id - return e.response - - response.id = self.id - - return response - - def to_JSON(self): - """Return a JSON representation of this Request""" - - self.message['id'] = 0 - self.message['token'] = None - - return json.dumps(self.message) - - @staticmethod - def _verify_token(token): - """Return the ID of the signed-in user. - - Or throw an Exception if the token is invalid. - """ - - try: - id_info = client.verify_id_token(token, os.environ['OPENDC_OAUTH_CLIENT_ID']) - except Exception as e: - print(e) - raise crypt.AppIdentityError('Exception caught trying to verify ID token: {}'.format(e)) - - if id_info['aud'] != os.environ['OPENDC_OAUTH_CLIENT_ID']: - raise crypt.AppIdentityError('Unrecognized client.') - - if id_info['iss'] not in ['accounts.google.com', 'https://accounts.google.com']: - raise crypt.AppIdentityError('Wrong issuer.') - - return id_info['sub'] - - -class Response: - """Response to websocket mapping""" - def __init__(self, status_code, status_description, content=None): - """Initialize a new Response.""" - - self.id = 0 - self.status = {'code': status_code, 'description': status_description} - self.content = content - - def to_JSON(self): - """"Return a JSON representation of this Response""" - - data = {'id': self.id, 'status': self.status} - - if self.content is not None: - data['content'] = self.content - - return json.dumps(data, default=str) |
