- Different users have different permissions
- Do we need group permissions? (Maybe later.)
- We should be fine-grained about access... someone might be able, e.g., to update a record but not delete it.
- We must be able to easily add different types of security checks as required, e.g.,
- login
- auth key
- dual factor
- biometrics
- IP address
- If there is no security record for some feature, it is open to all.
- Security data should be in our DB.
- First cut: let's use CRUD.
- Having a dictionary of bools to specify checks needed will permit easily adding more later.
security/models.py—SecProtocolandActionChecksvalue objects.ActionCheckssupportslogin,valid_users,allowed_roles,api_key/valid_api_keys,pass_phrase, andcodes. New checks can be added here without touching call sites.security/manager.py— in-memory registry of protocols, plusis_permitted(...)which decodes theAuthorization: Bearer <jwt>header viaserver.auth.authenticate_requestand runs the checks for the given feature/action.security/decorators.py—@require_protocol(feature, action)for Flask routes. Reads theAuthorizationheader, calls the manager, stashes the decision onflask.g.security_result, and either passes through, audit-logs, or returns 403 depending on env flags.security/security.py— legacy-format seed records (temp_recs) and theread()bootstrap that loads them into the manager. This is what the app calls at startup.
| Feature | READ | CREATE | UPDATE | DELETE |
|---|---|---|---|---|
people |
open | login + ejc369@nyu.edu only |
open | open |
countries |
open | login + role admin |
login + role admin |
login + role admin |
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.
Both default to off, so adding a new protocol record does not change live behavior until an operator opts in.
| Env var | Default | Effect |
|---|---|---|
SECURITY_ENFORCEMENT |
false |
When true, the decorator returns HTTP 403 for any denied request. |
SECURITY_AUDIT_ONLY |
false |
When true (with enforcement on), denied requests still pass through but are logged at INFO level. |
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.
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.
-
Register the feature by adding an entry to
temp_recsinsecurity/security.py, e.g.:STATES = 'states' temp_recs = { ..., STATES: { CREATE: {CHECKS: {LOGIN: True, ALLOWED_ROLES: [ROLE_ADMIN]}}, UPDATE: {CHECKS: {LOGIN: True, ALLOWED_ROLES: [ROLE_ADMIN]}}, DELETE: {CHECKS: {LOGIN: True, ALLOWED_ROLES: [ROLE_ADMIN]}}, }, }
-
Decorate the route in the matching endpoint module:
from security import require_protocol @require_protocol("states", "create") @states_ns.expect(state_create_model) def post(self): ...
@require_protocolshould be the outermost decorator so a 403 short-circuits before@marshal_withruns. -
Add a test that flips
SECURITY_ENFORCEMENT=true(seeserver/tests/test_countries_security.pyas a template) covering: no token → 403, wrong role → 403, admin token → 2xx, and the relevant GET still 200.
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.