Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/api/handlers/admin/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import api.handlers.admin.projects
import api.handlers.admin.users
import api.handlers.admin.clusters
import api.handlers.admin.global_tokens

85 changes: 85 additions & 0 deletions src/api/handlers/admin/global_tokens.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import uuid

from flask import g, abort, request
from flask_restx import Resource, fields
from pyinfraboxutils.ibflask import OK
from pyinfraboxutils.ibrestplus import api
from pyinfraboxutils.token import encode_global_token

global_token_create_model = api.model('GlobalTokenCreate', {
'description': fields.String(required=True),
'scope_push': fields.Boolean(required=False, default=False),
'scope_pull': fields.Boolean(required=False, default=True),
})

@api.route('/api/v1/admin/global-tokens', doc=False)
class GlobalTokens(Resource):

def get(self):
"""List all global tokens (admin only)"""
tokens = g.db.execute_many_dict('''
SELECT id, description, scope_push, scope_pull
FROM global_token
ORDER BY description
''')
return tokens

@api.expect(global_token_create_model, validate=True)
def post(self):
"""Create a new global token (admin only)"""
if g.token['user']['role'] != 'admin':
abort(403, "creating global tokens is only allowed for admin users")

body = request.get_json()
token_id = str(uuid.uuid4())
description = body['description']
scope_push = body.get('scope_push', False)
scope_pull = body.get('scope_pull', True)

g.db.execute('''
INSERT INTO global_token (id, description, scope_push, scope_pull)
VALUES (%s, %s, %s, %s)
''', [token_id, description, scope_push, scope_pull])
g.db.commit()

token = encode_global_token(token_id)

return {
'id': token_id,
'token': token,
'description': description,
'scope_push': scope_push,
'scope_pull': scope_pull,
}


@api.route('/api/v1/admin/global-tokens/<token_id>', doc=False)
class GlobalToken(Resource):

def get(self, token_id):
"""Get a specific global token (admin only)"""
token = g.db.execute_one_dict('''
SELECT id, description, scope_push, scope_pull
FROM global_token
WHERE id = %s
''', [token_id])

if not token:
abort(404, "Global token not found")

return token

def delete(self, token_id):
"""Delete a global token (admin only)"""
if g.token['user']['role'] != 'admin':
abort(403, "deleting global tokens is only allowed for admin users")

num = g.db.execute('''
DELETE FROM global_token WHERE id = %s
''', [token_id])
g.db.commit()

if num == 0:
abort(404, "Global token not found")

return OK("OK")
7 changes: 7 additions & 0 deletions src/db/migrations/00045.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
CREATE TABLE "global_token" (
id uuid DEFAULT gen_random_uuid() NOT NULL,
description VARCHAR(255) NOT NULL,
scope_push BOOLEAN DEFAULT FALSE NOT NULL,
scope_pull BOOLEAN DEFAULT TRUE NOT NULL,
PRIMARY KEY (id)
);
26 changes: 25 additions & 1 deletion src/openpolicyagent/policies/admin.rego
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package infrabox
# HTTP API request
import input as api

user_roles = {"user": 10, "devops": 20, "admin": 30}
user_roles = {"viewer": 15, "user": 10, "devops": 20, "admin": 30}

default authz = false

Expand All @@ -20,6 +20,30 @@ allow {
user_roles[api.token.user.role] >= 20
}

# Allow viewer role GET access to all admin endpoints
allow {
api.method = "GET"
api.token.type = "user"
api.token.user.role = "viewer"
}

# Allow global token (viewer) GET access to all admin endpoints
allow {
api.method = "GET"
api.token.type = "global"
api.token.user.role = "viewer"
}

# Allow admin access to manage global tokens
allow {
api.path[0] = "api"
api.path[1] = "v1"
api.path[2] = "admin"
api.path[3] = "global-tokens"
api.token.type = "user"
user_roles[api.token.user.role] >= 30
}


# Allow GET access to /api/v1/admin/clusters for users logged in
allow {
Expand Down
45 changes: 45 additions & 0 deletions src/openpolicyagent/policies/projects_projects.rego
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,48 @@ allow {
api.token.type = "user"
projects_projects_owner([api.token.user.id, project])
}

# Allow global token (viewer) GET access to all projects list
allow {
api.method = "GET"
api.path = ["api", "v1", "projects"]
api.token.type = "global"
}

# Allow global token (viewer) GET access to specific project by id
allow {
api.method = "GET"
api.path = ["api", "v1", "projects", project]
api.token.type = "global"
}

# Allow global token (viewer) GET access to project by name
allow {
api.method = "GET"
array.slice(api.path, 0, 4) = ["api", "v1", "projects", "name"]
api.token.type = "global"
}

# Allow viewer user role GET access to all projects list
allow {
api.method = "GET"
api.path = ["api", "v1", "projects"]
api.token.type = "user"
api.token.user.role = "viewer"
}

# Allow viewer user role GET access to specific project by id
allow {
api.method = "GET"
api.path = ["api", "v1", "projects", project]
api.token.type = "user"
api.token.user.role = "viewer"
}

# Allow viewer user role GET access to project by name
allow {
api.method = "GET"
array.slice(api.path, 0, 4) = ["api", "v1", "projects", "name"]
api.token.type = "user"
api.token.user.role = "viewer"
}
29 changes: 29 additions & 0 deletions src/pyinfraboxutils/ibflask.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,10 @@ def normalize_token(token):
if not validate_project_token(token):
return None

# Validate global token
elif token["type"] == "global":
return validate_global_token(token)

return token

def enrich_job_token(token):
Expand Down Expand Up @@ -230,6 +234,31 @@ def validate_user_token(token):
token['user']['role'] = u[1]
return token

def validate_global_token(token):
if not ("id" in token and validate_uuid(token['id'])):
return None

r = g.db.execute_one('''
SELECT id, description, scope_push, scope_pull FROM global_token
WHERE id = %s
''', [token['id']])
if not r:
logger.warn('global token not valid')
return None

token['global_token'] = {
'id': r[0],
'description': r[1],
'scope_push': r[2],
'scope_pull': r[3],
}
# Global tokens act as viewer role
token['user'] = {
'id': None,
'role': 'viewer',
}
return token

def validate_project_token(token):
if not ("project" in token and "id" in token['project'] and validate_uuid(token['project']['id'])
and "id" in token and validate_uuid(token['id'])):
Expand Down
9 changes: 9 additions & 0 deletions src/pyinfraboxutils/token.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ def encode_project_token(token_id, project_id, name):

return jwt.encode(data, key=s.read(), algorithm='RS256')

def encode_global_token(token_id):
with open(private_key_path) as s:
data = {
'id': token_id,
'type': 'global'
}

return jwt.encode(data, key=s.read(), algorithm='RS256')

def encode_job_token(job_id):
with open(private_key_path) as s:
data = {
Expand Down
Loading