summaryrefslogtreecommitdiff
path: root/opendc-web
diff options
context:
space:
mode:
Diffstat (limited to 'opendc-web')
-rw-r--r--opendc-web/opendc-web-api/conftest.py15
-rw-r--r--opendc-web/opendc-web-api/opendc/api/jobs.py6
-rw-r--r--opendc-web/opendc-web-api/opendc/api/portfolios.py7
-rw-r--r--opendc-web/opendc-web-api/opendc/api/scenarios.py8
-rw-r--r--opendc-web/opendc-web-api/opendc/api/topologies.py8
-rw-r--r--opendc-web/opendc-web-api/opendc/exts.py36
-rw-r--r--opendc-web/opendc-web-api/static/schema.yml1
7 files changed, 69 insertions, 12 deletions
diff --git a/opendc-web/opendc-web-api/conftest.py b/opendc-web/opendc-web-api/conftest.py
index 430262f1..958a5894 100644
--- a/opendc-web/opendc-web-api/conftest.py
+++ b/opendc-web/opendc-web-api/conftest.py
@@ -8,7 +8,7 @@ from flask import _request_ctx_stack, g
from opendc.database import Database
-def decorator(f):
+def requires_auth_mock(f):
@wraps(f)
def decorated_function(*args, **kwargs):
_request_ctx_stack.top.current_user = {'sub': 'test'}
@@ -16,13 +16,24 @@ def decorator(f):
return decorated_function
+def requires_scope_mock(required_scope):
+ def decorator(f):
+ @wraps(f)
+ def decorated_function(*args, **kwargs):
+ return f(*args, **kwargs)
+ return decorated_function
+ return decorator
+
+
@pytest.fixture
def client():
"""Returns a Flask API client to interact with."""
# Disable authorization for test API endpoints
from opendc import exts
- exts.requires_auth = decorator
+ exts.requires_auth = requires_auth_mock
+ exts.requires_scope = requires_scope_mock
+ exts.has_scope = lambda x: False
from app import create_app
diff --git a/opendc-web/opendc-web-api/opendc/api/jobs.py b/opendc-web/opendc-web-api/opendc/api/jobs.py
index 5feaea16..6fb0522b 100644
--- a/opendc-web/opendc-web-api/opendc/api/jobs.py
+++ b/opendc-web/opendc-web-api/opendc/api/jobs.py
@@ -22,7 +22,7 @@ from flask_restful import Resource
from marshmallow import fields, Schema, validate
from werkzeug.exceptions import BadRequest, Conflict
-from opendc.exts import requires_auth
+from opendc.exts import requires_auth, requires_scope
from opendc.models.scenario import Scenario
@@ -54,7 +54,7 @@ class JobList(Resource):
"""
Resource representing the list of available jobs.
"""
- method_decorators = [requires_auth]
+ method_decorators = [requires_auth, requires_scope('runner')]
def get(self):
"""Get all available jobs."""
@@ -67,7 +67,7 @@ class Job(Resource):
"""
Resource representing a single job.
"""
- method_decorators = [requires_auth]
+ method_decorators = [requires_auth, requires_scope('runner')]
def get(self, job_id):
"""Get the details of a single job."""
diff --git a/opendc-web/opendc-web-api/opendc/api/portfolios.py b/opendc-web/opendc-web-api/opendc/api/portfolios.py
index 84ec466c..eea82289 100644
--- a/opendc-web/opendc-web-api/opendc/api/portfolios.py
+++ b/opendc-web/opendc-web-api/opendc/api/portfolios.py
@@ -22,7 +22,7 @@ from flask import request
from flask_restful import Resource
from marshmallow import Schema, fields
-from opendc.exts import requires_auth, current_user
+from opendc.exts import requires_auth, current_user, has_scope
from opendc.models.portfolio import Portfolio as PortfolioModel, PortfolioSchema
from opendc.models.project import Project
from opendc.models.scenario import ScenarioSchema, Scenario
@@ -42,7 +42,10 @@ class Portfolio(Resource):
portfolio = PortfolioModel.from_id(portfolio_id)
portfolio.check_exists()
- portfolio.check_user_access(current_user['sub'], False)
+
+ # Users with scope runner can access all portfolios
+ if not has_scope('runner'):
+ portfolio.check_user_access(current_user['sub'], False)
data = PortfolioSchema().dump(portfolio.obj)
return {'data': data}
diff --git a/opendc-web/opendc-web-api/opendc/api/scenarios.py b/opendc-web/opendc-web-api/opendc/api/scenarios.py
index 234bdec1..eacb0b49 100644
--- a/opendc-web/opendc-web-api/opendc/api/scenarios.py
+++ b/opendc-web/opendc-web-api/opendc/api/scenarios.py
@@ -24,7 +24,7 @@ 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
+from opendc.exts import current_user, requires_auth, has_scope
class Scenario(Resource):
@@ -37,7 +37,11 @@ class Scenario(Resource):
"""Get scenario by identifier."""
scenario = ScenarioModel.from_id(scenario_id)
scenario.check_exists()
- scenario.check_user_access(current_user['sub'], False)
+
+ # Users with scope runner can access all scenarios
+ if not has_scope('runner'):
+ scenario.check_user_access(current_user['sub'], False)
+
data = ScenarioSchema().dump(scenario.obj)
return {'data': data}
diff --git a/opendc-web/opendc-web-api/opendc/api/topologies.py b/opendc-web/opendc-web-api/opendc/api/topologies.py
index a2d3f41a..c0b2e7ee 100644
--- a/opendc-web/opendc-web-api/opendc/api/topologies.py
+++ b/opendc-web/opendc-web-api/opendc/api/topologies.py
@@ -26,7 +26,7 @@ from marshmallow import Schema, fields
from opendc.models.project import Project
from opendc.models.topology import Topology as TopologyModel, TopologySchema
-from opendc.exts import current_user, requires_auth
+from opendc.exts import current_user, requires_auth, has_scope
class Topology(Resource):
@@ -41,7 +41,11 @@ class Topology(Resource):
"""
topology = TopologyModel.from_id(topology_id)
topology.check_exists()
- topology.check_user_access(current_user['sub'], False)
+
+ # Users with scope runner can access all topologies
+ if not has_scope('runner'):
+ topology.check_user_access(current_user['sub'], False)
+
data = TopologySchema().dump(topology.obj)
return {'data': data}
diff --git a/opendc-web/opendc-web-api/opendc/exts.py b/opendc-web/opendc-web-api/opendc/exts.py
index d24f7197..17dacd5e 100644
--- a/opendc-web/opendc-web-api/opendc/exts.py
+++ b/opendc-web/opendc-web-api/opendc/exts.py
@@ -2,10 +2,11 @@ import os
from functools import wraps
from flask import g, _request_ctx_stack
+from jose import jwt
from werkzeug.local import LocalProxy
from opendc.database import Database
-from opendc.auth import AuthContext, AsymmetricJwtAlgorithm, get_token
+from opendc.auth import AuthContext, AsymmetricJwtAlgorithm, get_token, AuthError
def get_db():
@@ -56,3 +57,36 @@ def requires_auth(f):
current_user = LocalProxy(lambda: getattr(_request_ctx_stack.top, 'current_user', None))
+
+
+def has_scope(required_scope):
+ """Determines if the required scope is present in the Access Token
+ Args:
+ required_scope (str): The scope required to access the resource
+ """
+ token = get_token()
+ unverified_claims = jwt.get_unverified_claims(token)
+ if unverified_claims.get("scope"):
+ token_scopes = unverified_claims["scope"].split()
+ for token_scope in token_scopes:
+ if token_scope == required_scope:
+ return True
+ return False
+
+
+def requires_scope(required_scope):
+ """Determines if the required scope is present in the Access Token
+ Args:
+ required_scope (str): The scope required to access the resource
+ """
+ def decorator(f):
+ @wraps(f)
+ def decorated(*args, **kwargs):
+ if not has_scope(required_scope):
+ raise AuthError({
+ "code": "Unauthorized",
+ "description": "You don't have access to this resource"
+ }, 403)
+ return f(*args, **kwargs)
+ return decorated
+ return decorator
diff --git a/opendc-web/opendc-web-api/static/schema.yml b/opendc-web/opendc-web-api/static/schema.yml
index 5a8c6825..6a07ae52 100644
--- a/opendc-web/opendc-web-api/static/schema.yml
+++ b/opendc-web/opendc-web-api/static/schema.yml
@@ -1180,6 +1180,7 @@ components:
tokenUrl: https://opendc.eu.auth0.com/oauth/token
scopes:
openid: Grants access to user_id
+ runner: Grants access to runner jobs
schemas:
Unauthorized:
type: object