|
18 | 18 | - Security data should be in our DB. |
19 | 19 | - First cut: let's use CRUD. |
20 | 20 | - Having a dictionary of bools to specify checks needed will permit easily adding more later. |
| 21 | + |
| 22 | +## Current Implementation |
| 23 | + |
| 24 | +### Modules |
| 25 | + |
| 26 | +- `security/models.py` — `SecProtocol` and `ActionChecks` value objects. `ActionChecks` supports `login`, `valid_users`, `allowed_roles`, `api_key` / `valid_api_keys`, `pass_phrase`, and `codes`. New checks can be added here without touching call sites. |
| 27 | +- `security/manager.py` — in-memory registry of protocols, plus `is_permitted(...)` which decodes the `Authorization: Bearer <jwt>` header via `server.auth.authenticate_request` and runs the checks for the given feature/action. |
| 28 | +- `security/decorators.py` — `@require_protocol(feature, action)` for Flask routes. Reads the `Authorization` header, calls the manager, stashes the decision on `flask.g.security_result`, and either passes through, audit-logs, or returns 403 depending on env flags. |
| 29 | +- `security/security.py` — legacy-format seed records (`temp_recs`) and the `read()` bootstrap that loads them into the manager. This is what the app calls at startup. |
| 30 | + |
| 31 | +### Features with protocols today |
| 32 | + |
| 33 | +| Feature | READ | CREATE | UPDATE | DELETE | |
| 34 | +|-------------|----------------|-------------------------------|-------------------------------|-------------------------------| |
| 35 | +| `people` | open | login + `ejc369@nyu.edu` only | open | open | |
| 36 | +| `countries` | open | login + role `admin` | login + role `admin` | login + role `admin` | |
| 37 | + |
| 38 | +`countries` write actions are wired in `server/countries_endpoints.py` via `@require_protocol(...)` on `CountriesList.post`, `Country.put`, and `Country.delete`. Read endpoints are intentionally left open so unauthenticated browsing keeps working. Other features (states, cities) currently have no protocol record, which means they are open to all per the rule above. |
| 39 | + |
| 40 | +### Rollout / enforcement flags |
| 41 | + |
| 42 | +Both default to off, so adding a new protocol record does not change live behavior until an operator opts in. |
| 43 | + |
| 44 | +| Env var | Default | Effect | |
| 45 | +|--------------------------|---------|-----------------------------------------------------------------------------------------------------| |
| 46 | +| `SECURITY_ENFORCEMENT` | `false` | When `true`, the decorator returns HTTP 403 for any denied request. | |
| 47 | +| `SECURITY_AUDIT_ONLY` | `false` | When `true` (with enforcement on), denied requests still pass through but are logged at INFO level. | |
| 48 | + |
| 49 | +Audit-only mode is the recommended way to roll a new protocol out: turn enforcement on with audit-only, watch the logs (e.g. through `/dev/logs`) for unexpected denials, then drop audit-only. |
| 50 | + |
| 51 | +### Auth tokens |
| 52 | + |
| 53 | +`server/auth.py` issues HS256 JWTs with `sub` (user id) and `role` claims. Allowed roles are `admin` and `user`. The `JWT_SECRET` env var controls signing; the default `dev-jwt-secret` is for local use only. |
| 54 | + |
| 55 | +### Adding a security protocol to a new feature |
| 56 | + |
| 57 | +1. **Register the feature** by adding an entry to `temp_recs` in `security/security.py`, e.g.: |
| 58 | + |
| 59 | + ```python |
| 60 | + STATES = 'states' |
| 61 | + |
| 62 | + temp_recs = { |
| 63 | + ..., |
| 64 | + STATES: { |
| 65 | + CREATE: {CHECKS: {LOGIN: True, ALLOWED_ROLES: [ROLE_ADMIN]}}, |
| 66 | + UPDATE: {CHECKS: {LOGIN: True, ALLOWED_ROLES: [ROLE_ADMIN]}}, |
| 67 | + DELETE: {CHECKS: {LOGIN: True, ALLOWED_ROLES: [ROLE_ADMIN]}}, |
| 68 | + }, |
| 69 | + } |
| 70 | + ``` |
| 71 | + |
| 72 | +2. **Decorate the route** in the matching endpoint module: |
| 73 | + |
| 74 | + ```python |
| 75 | + from security import require_protocol |
| 76 | + |
| 77 | + @require_protocol("states", "create") |
| 78 | + @states_ns.expect(state_create_model) |
| 79 | + def post(self): |
| 80 | + ... |
| 81 | + ``` |
| 82 | + |
| 83 | + `@require_protocol` should be the outermost decorator so a 403 short-circuits before `@marshal_with` runs. |
| 84 | + |
| 85 | +3. **Add a test** that flips `SECURITY_ENFORCEMENT=true` (see `server/tests/test_countries_security.py` as a template) covering: no token → 403, wrong role → 403, admin token → 2xx, and the relevant GET still 200. |
| 86 | + |
| 87 | +No app-code changes are required beyond those three steps — `create_app()` already calls `security.security.read()` at startup, so the new entry is loaded automatically. |
0 commit comments