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
21 changes: 21 additions & 0 deletions docs/ENTERPRISE_SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,27 @@ plus a database cross-check; HS256/none are rejected.
> This also fixes a latent multi-replica bug where a per-process random secret
> made tokens unverifiable across manager replicas/restarts.

### Scope-based authorization (roles are scope bundles)

Authorization decisions are made on **scopes only** — never role names. A user's
role is a convenience bundle that the manager **expands into concrete
`resource:action` scopes at token issuance**, emitting them in the token's
`scope` claim. Middleware checks the `scope` claim; `global_role`/`team_roles`
remain on the token for audit and per-team membership checks.

| Global role | Scope bundle (summary) |
|--------------|-------------------------------------------------------------------|
| SystemAdmin | every `*:write` / `*:admin` scope + `admin:super` + all `*:read` |
| OrgAdmin | `servers:write` `teams:write` `time:write` `dhcp:write` + reads |
| UserManager | `users:write` + reads |
| Viewer | `*:read` only |

Endpoints declare the scope they need (e.g. `@requires_scope('servers:write')`);
the super-admin bypass for team/zone access checks the `admin:super` scope, not
the `SystemAdmin` role name. Bundle definitions live in one place
(`app/services/scopes.py`), so entitlements are auditable and adjustable
centrally.

---

## 2. Tenant isolation
Expand Down
14 changes: 7 additions & 7 deletions manager/backend/app/blueprints/dhcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from flask import Blueprint, request, jsonify, current_app
from app.middleware.auth import token_required, get_current_user
from app.middleware.rbac import requires_role, check_team_access
from app.middleware.rbac import requires_scope, check_team_access
from app.utils.decorators import validate_json, audit_log
from datetime import datetime

Expand Down Expand Up @@ -96,7 +96,7 @@ def list_dhcp_pools():

@dhcp_bp.route('/api/v1/dhcp/pools', methods=['POST'])
@token_required
@requires_role('SystemAdmin', 'OrgAdmin')
@requires_scope('dhcp:write')
@validate_json('name', 'network', 'rangeStart', 'rangeEnd')
@audit_log('dhcp_pool_created')
def create_dhcp_pool():
Expand Down Expand Up @@ -205,7 +205,7 @@ def get_dhcp_pool(pool_id):

@dhcp_bp.route('/api/v1/dhcp/pools/<int:pool_id>', methods=['PUT'])
@token_required
@requires_role('SystemAdmin', 'OrgAdmin')
@requires_scope('dhcp:write')
@audit_log('dhcp_pool_updated')
def update_dhcp_pool(pool_id):
"""Update DHCP pool configuration."""
Expand Down Expand Up @@ -255,7 +255,7 @@ def update_dhcp_pool(pool_id):

@dhcp_bp.route('/api/v1/dhcp/pools/<int:pool_id>', methods=['DELETE'])
@token_required
@requires_role('SystemAdmin')
@requires_scope('dhcp:admin')
@audit_log('dhcp_pool_deleted')
def delete_dhcp_pool(pool_id):
"""Delete DHCP pool and all associated leases/reservations."""
Expand Down Expand Up @@ -327,7 +327,7 @@ def list_pool_leases(pool_id):

@dhcp_bp.route('/api/v1/dhcp/pools/<int:pool_id>/leases/<int:lease_id>', methods=['DELETE'])
@token_required
@requires_role('SystemAdmin', 'OrgAdmin')
@requires_scope('dhcp:write')
@audit_log('dhcp_lease_released')
def release_lease(pool_id, lease_id):
"""Manually release a DHCP lease."""
Expand Down Expand Up @@ -388,7 +388,7 @@ def list_reservations():

@dhcp_bp.route('/api/v1/dhcp/reservations', methods=['POST'])
@token_required
@requires_role('SystemAdmin', 'OrgAdmin')
@requires_scope('dhcp:write')
@validate_json('poolId', 'macAddress', 'ipAddress')
@audit_log('dhcp_reservation_created')
def create_reservation():
Expand Down Expand Up @@ -435,7 +435,7 @@ def create_reservation():

@dhcp_bp.route('/api/v1/dhcp/reservations/<int:reservation_id>', methods=['DELETE'])
@token_required
@requires_role('SystemAdmin', 'OrgAdmin')
@requires_scope('dhcp:write')
@audit_log('dhcp_reservation_deleted')
def delete_reservation(reservation_id):
"""Delete a DHCP reservation."""
Expand Down
14 changes: 7 additions & 7 deletions manager/backend/app/blueprints/dns_servers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@
from app.services.auth_service import AuthService
from app.services.config_service import ConfigService
from app.middleware.auth import token_required, server_token_required, get_current_server
from app.middleware.rbac import requires_role
from app.middleware.rbac import requires_scope
from app.utils.decorators import validate_json, audit_log

dns_servers_bp = Blueprint('dns_servers', __name__)


@dns_servers_bp.route('/api/v1/dns-servers', methods=['GET'])
@token_required
@requires_role('SystemAdmin', 'OrgAdmin')
@requires_scope('servers:write')
def list_dns_servers():
"""
List all DNS servers.
Expand Down Expand Up @@ -55,7 +55,7 @@ def list_dns_servers():

@dns_servers_bp.route('/api/v1/dns-servers', methods=['POST'])
@token_required
@requires_role('SystemAdmin', 'OrgAdmin')
@requires_scope('servers:write')
@validate_json('name')
@audit_log('dns_server_created')
def create_dns_server():
Expand Down Expand Up @@ -89,7 +89,7 @@ def create_dns_server():

@dns_servers_bp.route('/api/v1/dns-servers/<int:server_id>', methods=['GET'])
@token_required
@requires_role('SystemAdmin', 'OrgAdmin')
@requires_scope('servers:write')
def get_dns_server(server_id):
"""Get DNS server details."""
server = JoinKeyService.get_server_by_id(server_id)
Expand All @@ -102,7 +102,7 @@ def get_dns_server(server_id):

@dns_servers_bp.route('/api/v1/dns-servers/<int:server_id>', methods=['DELETE'])
@token_required
@requires_role('SystemAdmin')
@requires_scope('servers:admin')
@audit_log('dns_server_deleted')
def delete_dns_server(server_id):
"""Delete DNS server."""
Expand All @@ -123,7 +123,7 @@ def delete_dns_server(server_id):

@dns_servers_bp.route('/api/v1/dns-servers/<int:server_id>/regenerate-key', methods=['POST'])
@token_required
@requires_role('SystemAdmin')
@requires_scope('servers:admin')
@audit_log('dns_server_key_regenerated')
def regenerate_join_key(server_id):
"""
Expand Down Expand Up @@ -308,7 +308,7 @@ def refresh_server_token(server_id):

@dns_servers_bp.route('/api/v1/dns-servers/<int:server_id>/metrics', methods=['GET'])
@token_required
@requires_role('SystemAdmin', 'OrgAdmin')
@requires_scope('servers:write')
def get_dns_server_metrics(server_id):
"""
Get historical metrics for DNS server.
Expand Down
10 changes: 5 additions & 5 deletions manager/backend/app/blueprints/ioc_feeds.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from typing import Optional
from flask import Blueprint, request, jsonify, current_app
from app.middleware.auth import token_required
from app.middleware.rbac import requires_system_admin, requires_role
from app.middleware.rbac import requires_scope
from app.utils.decorators import validate_json, audit_log

ioc_feeds_bp = Blueprint('ioc_feeds', __name__)
Expand Down Expand Up @@ -131,7 +131,7 @@ def list_ioc_feeds():

@ioc_feeds_bp.route('/api/v1/ioc-feeds', methods=['POST'])
@token_required
@requires_system_admin
@requires_scope('ioc:admin')
@validate_json('name', 'url', 'feed_type')
@_check_ioc_flag_and_license()
@audit_log('ioc_feed_created')
Expand Down Expand Up @@ -221,7 +221,7 @@ def get_ioc_feed(feed_id):

@ioc_feeds_bp.route('/api/v1/ioc-feeds/<int:feed_id>', methods=['PUT'])
@token_required
@requires_system_admin
@requires_scope('ioc:admin')
@_check_ioc_flag_and_license()
@audit_log('ioc_feed_updated')
def update_ioc_feed(feed_id):
Expand Down Expand Up @@ -271,7 +271,7 @@ def update_ioc_feed(feed_id):

@ioc_feeds_bp.route('/api/v1/ioc-feeds/<int:feed_id>', methods=['DELETE'])
@token_required
@requires_system_admin
@requires_scope('ioc:admin')
@_check_ioc_flag_and_license()
@audit_log('ioc_feed_deleted')
def delete_ioc_feed(feed_id):
Expand All @@ -292,7 +292,7 @@ def delete_ioc_feed(feed_id):

@ioc_feeds_bp.route('/api/v1/ioc-feeds/<int:feed_id>/sync', methods=['POST'])
@token_required
@requires_system_admin
@requires_scope('ioc:admin')
@_check_ioc_flag_and_license()
@audit_log('ioc_feed_sync_triggered')
def trigger_ioc_feed_sync(feed_id):
Expand Down
6 changes: 3 additions & 3 deletions manager/backend/app/blueprints/teams.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from flask import Blueprint, request, jsonify, current_app
from app.middleware.auth import token_required, get_current_user
from app.middleware.rbac import requires_role, requires_team_role, can_access_team, filter_teams_by_access
from app.middleware.rbac import requires_scope, requires_team_role, can_access_team, filter_teams_by_access
from app.utils.decorators import validate_json, audit_log
from app.utils.validators import validate_team_role

Expand Down Expand Up @@ -58,7 +58,7 @@ def list_teams():

@teams_bp.route('/api/v1/teams', methods=['POST'])
@token_required
@requires_role('SystemAdmin', 'OrgAdmin')
@requires_scope('teams:write')
@validate_json('name')
@audit_log('team_created')
def create_team():
Expand Down Expand Up @@ -170,7 +170,7 @@ def update_team(team_id):

@teams_bp.route('/api/v1/teams/<int:team_id>', methods=['DELETE'])
@token_required
@requires_role('SystemAdmin', 'OrgAdmin')
@requires_scope('teams:write')
@audit_log('team_deleted')
def delete_team(team_id):
"""Delete team and all memberships."""
Expand Down
10 changes: 5 additions & 5 deletions manager/backend/app/blueprints/time.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from flask import Blueprint, request, jsonify, current_app
from app.middleware.auth import token_required, get_current_user
from app.middleware.rbac import requires_role
from app.middleware.rbac import requires_scope
from app.utils.decorators import validate_json, audit_log
from datetime import datetime, timedelta

Expand Down Expand Up @@ -88,7 +88,7 @@ def list_time_servers():

@time_bp.route('/api/v1/time/servers', methods=['POST'])
@token_required
@requires_role('SystemAdmin', 'OrgAdmin')
@requires_scope('time:write')
@validate_json('name', 'serverUrl', 'protocol')
@audit_log('time_server_created')
def create_time_server():
Expand Down Expand Up @@ -203,7 +203,7 @@ def get_time_server(server_id):

@time_bp.route('/api/v1/time/servers/<int:server_id>', methods=['PUT'])
@token_required
@requires_role('SystemAdmin', 'OrgAdmin')
@requires_scope('time:write')
@audit_log('time_server_updated')
def update_time_server(server_id):
"""Update time server configuration."""
Expand Down Expand Up @@ -236,7 +236,7 @@ def update_time_server(server_id):

@time_bp.route('/api/v1/time/servers/<int:server_id>', methods=['DELETE'])
@token_required
@requires_role('SystemAdmin')
@requires_scope('time:admin')
@audit_log('time_server_deleted')
def delete_time_server(server_id):
"""Delete a time server."""
Expand Down Expand Up @@ -321,7 +321,7 @@ def get_time_status():

@time_bp.route('/api/v1/time/sync', methods=['POST'])
@token_required
@requires_role('SystemAdmin', 'OrgAdmin')
@requires_scope('time:write')
@audit_log('time_sync_triggered')
def trigger_time_sync():
"""
Expand Down
12 changes: 6 additions & 6 deletions manager/backend/app/blueprints/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from flask import Blueprint, request, jsonify, current_app
from app.services.auth_service import AuthService
from app.middleware.auth import token_required
from app.middleware.rbac import requires_role
from app.middleware.rbac import requires_scope
from app.utils.decorators import validate_json, audit_log
from app.utils.validators import validate_email, validate_username, validate_global_role

Expand All @@ -15,7 +15,7 @@

@users_bp.route('/api/v1/users', methods=['GET'])
@token_required
@requires_role('SystemAdmin', 'UserManager')
@requires_scope('users:write')
def list_users():
"""
List all users.
Expand Down Expand Up @@ -67,7 +67,7 @@ def list_users():

@users_bp.route('/api/v1/users', methods=['POST'])
@token_required
@requires_role('SystemAdmin', 'UserManager')
@requires_scope('users:write')
@validate_json('username', 'email', 'password', 'global_role')
@audit_log('user_created')
def create_user():
Expand Down Expand Up @@ -139,7 +139,7 @@ def create_user():

@users_bp.route('/api/v1/users/<int:user_id>', methods=['GET'])
@token_required
@requires_role('SystemAdmin', 'UserManager')
@requires_scope('users:write')
def get_user(user_id):
"""Get user by ID."""
db = current_app.db
Expand Down Expand Up @@ -178,7 +178,7 @@ def get_user(user_id):

@users_bp.route('/api/v1/users/<int:user_id>', methods=['PUT'])
@token_required
@requires_role('SystemAdmin', 'UserManager')
@requires_scope('users:write')
@audit_log('user_updated')
def update_user(user_id):
"""
Expand Down Expand Up @@ -228,7 +228,7 @@ def update_user(user_id):

@users_bp.route('/api/v1/users/<int:user_id>', methods=['DELETE'])
@token_required
@requires_role('SystemAdmin')
@requires_scope('users:admin')
@audit_log('user_deleted')
def delete_user(user_id):
"""
Expand Down
2 changes: 2 additions & 0 deletions manager/backend/app/middleware/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ def decorated(*args, **kwargs):
'user_id': payload.get('user_id'),
'server_id': payload.get('server_id'),
'username': payload.get('username'),
'scope': payload.get('scope', ''),
'global_role': payload.get('global_role'),
'team_roles': payload.get('team_roles', {}),
'token_type': payload.get('type')
Expand Down Expand Up @@ -132,6 +133,7 @@ def decorated(*args, **kwargs):
g.current_user = {
'user_id': payload.get('user_id'),
'username': payload.get('username'),
'scope': payload.get('scope', ''),
'global_role': payload.get('global_role'),
'team_roles': payload.get('team_roles', {}),
'token_type': payload.get('type')
Expand Down
Loading