Flagsmith / Flagsmith/flagsmith

Spike: Map and enforce scopes based on permissions

Open
#7,035 0 comments 0 reactions 1 assignee Claimed by @Zaimwa9 View on GitHub
Dominant language
Python
Stars
6.6k
Forks
567
Avg merge
1d 13h
Merged PRs (30d)
121

Description

### Context

We need a scope layer for OAuth tokens without duplicating the permission system.

We map scopes to existing RBAC permissions.

A new `OAuthScopePermission` class checks whether the token's scopes cover the view's required RBAC permission. Unmapped permissions are denied by default

### Enforcement rule
Effective permissions is the intersection between the user permissions and scopes granted

### What to do
- Define the `SCOPE_PERMISSION_MAP`:
```python
SCOPE_PERMISSION_MAP = {
"flag:read": {VIEW_PROJECT, VIEW_FEATURE_STATE, VIEW_ENVIRONMENT},
"flag:write": {CREATE_FEATURE, UPDATE_FEATURE_STATE, DELETE_FEATURE},
# ...
}
```
- Build `OAuthScopePermission` class:
- If request is not an OAuth token → return `True` (no-op for session users)
- If the view has no declared RBAC permission → deny (default deny)
- Look up which scopes cover the required RBAC permission → check the token has one
- Add `OAuthScopePermission` to the permission classes:
```python
permission_classes = [IsAuthenticated, OAuthScopePermission, ProjectPermission]
```
- Define scope list in DOT settings (`OAUTH2_PROVIDER["SCOPES"]`)

### Scope-to-permission mapping

| Scope | RBAC Permissions | MCP Endpoints |
|---|---|---|
| `organisation:read` | *(org membership)* | `GET /organisations/`, `/organisations/{id}/groups/`, `/organisations/{id}/invites/` |
| `organisation:write` | `CREATE_PROJECT`, `MANAGE_USER_GROUPS` | `POST /organisations/{id}/invites/` |
| `project:read` | `VIEW_PROJECT`, `MANAGE_TAGS` | `GET /organisations/{id}/projects/`, `/projects/{id}/`, `/projects/{id}/environments/` |
| `project:write` | `CREATE_ENVIRONMENT` | `PUT /projects/{id}/` |
| `environment:read` | `VIEW_ENVIRONMENT` | `GET /environments/` |
| `environment:write` | `MANAGE_SEGMENT_OVERRIDES` | |
| `flag:read` | `VIEW_PROJECT`, `VIEW_ENVIRONMENT` | `GET /features/`, `/features/{id}/`, `/evaluation-data/`, `/code-references/`, `/feature-external-resources/`, `/mv-options/`, `/versions/`, `/featurestates/`, `/change-requests/`, `/list-change-requests/` |
| `flag:write` | `CREATE_FEATURE`, `EDIT_FEATURE`, `DELETE_FEATURE`, `UPDATE_FEATURE_STATE`, `CREATE_CHANGE_REQUEST`, `APPROVE_CHANGE_REQUEST`, `MANAGE_PROJECT_LEVEL_CHANGE_REQUESTS`, `APPROVE_PROJECT_LEVEL_CHANGE_REQUESTS`, `CREATE_PROJECT_LEVEL_CHANGE_REQUESTS` | `POST/PUT/DELETE` on features, MV options, versions, feature states, change requests |
| `segment:read` | `VIEW_PROJECT` | `GET /segments/`, `/segments/{id}/` |
| `segment:write` | `MANAGE_SEGMENTS` | `POST /segments/`, `PUT /segments/{id}/` |
| `identity:read` | `VIEW_IDENTITIES` | |
| `identity:write` | `MANAGE_IDENTITIES` | |
| `release_pipeline:read` | `VIEW_PROJECT` | `GET /release-pipelines/`, `/release-pipelines/{id}/` |
| `release_pipeline:write` | *(project admin)* | `POST /release-pipelines/{id}/add-feature/` |
| `feature_health:read` | `VIEW_PROJECT` | `GET /feature-health/events/` |
| `audit_log:read` | `VIEW_AUDIT_LOG` | |
| `webhook:read` | *(env/project membership)* | |
| `webhook:write` | *(env/project admin)* | |
| `role:read` | *(org admin)* | |
| `role:write` | *(org admin)* | |
| `integration:read` | *(project/env membership)* | |
| `integration:write` | *(project/env admin)* | |

```python
SCOPE_PERMISSION_MAP = {
# Organisation
"organisation:read": set(), # org membership check
"organisation:write": {CREATE_PROJECT, MANAGE_USER_GROUPS},
# Project
"project:read": {VIEW_PROJECT, MANAGE_TAGS},
"project:write": {CREATE_ENVIRONMENT},
# Environment
"environment:read": {VIEW_ENVIRONMENT},
"environment:write": {MANAGE_SEGMENT_OVERRIDES},
# Flags (includes change requests)
"flag:read": {VIEW_PROJECT, VIEW_ENVIRONMENT},
"flag:write": {
CREATE_FEATURE, EDIT_FEATURE, DELETE_FEATURE, UPDATE_FEATURE_STATE,
CREATE_CHANGE_REQUEST, APPROVE_CHANGE_REQUEST,
MANAGE_PROJECT_LEVEL_CHANGE_REQUESTS, APPROVE_PROJECT_LEVEL_CHANGE_REQUESTS,
CREATE_PROJECT_LEVEL_CHANGE_REQUESTS,
},
# Segments
"segment:read": {VIEW_PROJECT},
"segment:write": {MANAGE_SEGMENTS},
# Identities
"identity:read": {VIEW_IDENTITIES},
"identity:write": {MANAGE_IDENTITIES},
# Release pipelines
"release_pipeline:read": {VIEW_PROJECT},
"release_pipeline:write": set(), # project admin check
# Feature health
"feature_health:read": {VIEW_PROJECT},
# Audit log
"audit_log:read": {VIEW_AUDIT_LOG},
# Webhooks
"webhook:read": set(), # env/project membership check
"webhook:write": set(), # env/project admin check
# Roles
"role:read": set(), # org admin check
"role:write": set(), # org admin check
# Integrations
"integration:read": set(), # project/env membership check
"integration:write": set(), # project/env admin check
}
```

### Definition of done
- [ ] Scope enforcement active for OAuth tokens
- [ ] Session users unaffected
- [ ] Unmapped RBAC permissions are denied for OAuth tokens
- [ ] A token with the relevant scopes can access the resources (e.g `flag:read` can read features but not create them)
- [ ] A token without the required scope gets `403 insufficient_scope`

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.