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")
24 changes: 16 additions & 8 deletions src/api/handlers/projects/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,22 @@ def get(self):
'''
Returns user's projects
'''
projects = g.db.execute_many_dict("""
SELECT p.id, p.name, p.type, p.public, co.role AS userrole
FROM project p
INNER JOIN collaborator co
ON co.project_id = p.id
AND %s = co.user_id
ORDER BY p.name
""", [g.token['user']['id']])
# Global tokens can see all projects
if g.token and g.token.get('type') == 'global':
projects = g.db.execute_many_dict("""
SELECT p.id, p.name, p.type, p.public
FROM project p
ORDER BY p.name
""")
else:
projects = g.db.execute_many_dict("""
SELECT p.id, p.name, p.type, p.public, co.role AS userrole
FROM project p
INNER JOIN collaborator co
ON co.project_id = p.id
AND %s = co.user_id
ORDER BY p.name
""", [g.token['user']['id']])

return projects

Expand Down
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
159 changes: 159 additions & 0 deletions src/openpolicyagent/policies/global_viewer_test.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package infrabox

# ─── helpers ───────────────────────────────────────────────────────────────────

global_viewer_token = {
"type": "global",
"user": {"id": null, "role": "viewer"},
"global_token": {"id": "00000000-0000-0000-0000-000000000001",
"scope_push": false, "scope_pull": true}
}

user_viewer_token = {
"type": "user",
"user": {"id": "00000000-0000-0000-0000-000000000002", "role": "viewer"}
}

# ─── global token: read-only allowed ───────────────────────────────────────────

test_global_viewer_can_get_admin_users {
authz with input as {
"method": "GET",
"path": ["api", "v1", "admin", "users"],
"token": global_viewer_token
}
}

test_global_viewer_can_get_admin_clusters {
authz with input as {
"method": "GET",
"path": ["api", "v1", "admin", "clusters"],
"token": global_viewer_token
}
}

test_global_viewer_can_get_projects_list {
authz with input as {
"method": "GET",
"path": ["api", "v1", "projects"],
"token": global_viewer_token
}
}

test_global_viewer_can_get_project_by_id {
authz with input as {
"method": "GET",
"path": ["api", "v1", "projects", "00000000-0000-0000-0000-000000000099"],
"token": global_viewer_token
}
}

# ─── global token: write MUST be denied ────────────────────────────────────────

test_global_viewer_cannot_post_projects {
not authz with input as {
"method": "POST",
"path": ["api", "v1", "projects"],
"token": global_viewer_token
}
}

test_global_viewer_cannot_delete_project {
not authz with input as {
"method": "DELETE",
"path": ["api", "v1", "projects", "00000000-0000-0000-0000-000000000099"],
"token": global_viewer_token
}
}

test_global_viewer_cannot_post_admin_users {
not authz with input as {
"method": "POST",
"path": ["api", "v1", "admin", "users"],
"token": global_viewer_token
}
}

test_global_viewer_cannot_delete_admin_clusters {
not authz with input as {
"method": "DELETE",
"path": ["api", "v1", "admin", "clusters"],
"token": global_viewer_token
}
}

# ─── user viewer role: read-only allowed ───────────────────────────────────────

test_user_viewer_can_get_admin_users {
authz with input as {
"method": "GET",
"path": ["api", "v1", "admin", "users"],
"token": user_viewer_token
}
}

test_user_viewer_can_get_projects_list {
authz with input as {
"method": "GET",
"path": ["api", "v1", "projects"],
"token": user_viewer_token
}
}

# ─── user viewer role: write MUST be denied ────────────────────────────────────

test_user_viewer_cannot_post_admin_users {
not authz with input as {
"method": "POST",
"path": ["api", "v1", "admin", "users"],
"token": user_viewer_token
}
}

test_user_viewer_cannot_post_projects {
not authz with input as {
"method": "POST",
"path": ["api", "v1", "projects"],
"token": user_viewer_token
}
}

test_regular_user_can_post_projects {
authz with input as {
"method": "POST",
"path": ["api", "v1", "projects"],
"token": {
"type": "user",
"user": {"id": "00000000-0000-0000-0000-000000000004", "role": "user"}
}
}
}

test_user_viewer_cannot_delete_project {
not authz with input as {
"method": "DELETE",
"path": ["api", "v1", "projects", "00000000-0000-0000-0000-000000000099"],
"token": user_viewer_token
}
}

# ─── global-tokens endpoint: only admin can manage ─────────────────────────────

test_global_viewer_cannot_post_global_tokens {
not authz with input as {
"method": "POST",
"path": ["api", "v1", "admin", "global-tokens"],
"token": global_viewer_token
}
}

test_admin_can_post_global_tokens {
authz with input as {
"method": "POST",
"path": ["api", "v1", "admin", "global-tokens"],
"token": {
"type": "user",
"user": {"id": "00000000-0000-0000-0000-000000000003", "role": "admin"}
}
}
}
46 changes: 46 additions & 0 deletions src/openpolicyagent/policies/projects_projects.rego
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ allow {
api.method = "POST"
api.path = ["api", "v1", "projects"]
api.token.type = "user"
api.token.user.role != "viewer"
}

allow {
Expand Down Expand Up @@ -95,3 +96,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"
}
Loading
Loading