Skip to content

feat(api): per-user resource selection endpoint — Part of #586#981

Open
skypank-coder wants to merge 9 commits into
OWASP:mainfrom
skypank-coder:feat/586-resource-selection-api
Open

feat(api): per-user resource selection endpoint — Part of #586#981
skypank-coder wants to merge 9 commits into
OWASP:mainfrom
skypank-coder:feat/586-resource-selection-api

Conversation

@skypank-coder

Copy link
Copy Markdown
Contributor

What & why

Second increment of #586. Builds on PR1's user persistence to expose the
per-user resource selection over the API, so the frontend (PR4) and server-side
filtering (PR3) have a read/write surface. No new tables or migrations — pure
HTTP + validation over PR1's Node_collection methods.

Endpoints

/rest/v1/user/resources (canonical /rest/v1 surface):

  • GET{"selected": [...]} — the current user's saved standard names, sorted.
  • PUT {"selected": ["ASVS","CWE"]} → dedup + replace, persists via PR1's
    set_user_resource_selection, returns the stored list.

Gating

Requires login_required + is_login_enabled() + is_myopencre_enabled()
(per the #966 decision):

  • Login flag off → safe default {"selected": []}, 200, no auth, no writes.
  • MyOpenCRE capability off → same safe default; the gate short-circuits before
    any DB read/write.
  • Flag on + anonymous → 401.
  • Invalid PUT body (missing selected, not a list, empty/non-string entries) → 400.

Implemented via a generalized feature_enabled_or_default helper composed over
login_required (a generalization of PR1's flag-gating; PR1's behaviour is
unchanged).

OpenAPI

Endpoint added to the spec properly — two PathSpec entries +
@openapi_documented decorators, docs/api/openapi.yaml regenerated (not
hand-edited). The OpenAPI guardrail passes all four checks (documented views,
freshness, validity, route coverage).

Testing

12 new tests, verified on real Postgres (prod parity): flag-off default,
myopencre-off default + no-write, anonymous 401, dedup, replace against the live
UNIQUE(user_id, standard_name), and body-validation 400s. The myopencre-off
tests are mutation-proven — removing the capability from the gate makes them fail
(GET leaks the saved selection, PUT persists), restoring it makes them pass — so
they genuinely detect a bypassed gate rather than passing incidentally.

black clean; no new mypy errors vs. baseline.

Scope boundaries

No server-side filtering (PR3), no frontend (PR4), no new DB surface. Reuses
PR1's methods and login_required.

Files (4)

  • application/web/web_main.py — routes + gate + feature_enabled_or_default.
  • application/web/openapi_registry.py — two PathSpec entries.
  • docs/api/openapi.yaml — regenerated.
  • application/tests/user_resources_api_test.py — new, 12 tests.

skypank-coder and others added 7 commits July 9, 2026 20:18
…WASP#586)

Persist accounts on OIDC login and store a per-user resource (standard) selection. Additive to the existing session auth: the callback upserts the User row and sets session['user_id'] only when CRE_ENABLE_LOGIN is on; the existing session keys and 401 behaviour are unchanged.

- User(google_sub UNIQUE, email, display_name, created_at, last_seen_at)
- UserResourceSelection(user_id FK CASCADE, standard_name, UNIQUE(user_id, name))
- Node_collection: upsert_user / get_user_by_sub / get_user_resource_selection / set_user_resource_selection
- Alembic: dedicated merge of the two open heads + create tables (up/down verified on Postgres; guardrail green)
…int (OWASP#586)

Address PR review:
- upsert_user now mirrors add_node: catch IntegrityError, rollback, re-query
  by google_sub, update profile — safe under concurrent logins.
- Drop column-level unique=True on User.google_sub; keep only the named
  constraint so create_all matches the Alembic migration (no schema drift).
- Login callback skips persistence when the OIDC 'sub' claim is missing.
- Callback test asserts session['user_id'] equals the persisted User.id.
…OWASP#586)

Address PR review:
- Log only the exception class on login-persistence failure; the raw message
  can carry SQL parameters (email, OIDC sub).
- Add callback test: login enabled but token missing 'sub' creates no user
  row and leaves session['user_id'] unset.
@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@skypank-coder, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 15 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 7abce83a-1d0e-4c7e-a3b3-03597f7994fd

📥 Commits

Reviewing files that changed from the base of the PR and between 95f914c and 7c4fe2c.

📒 Files selected for processing (2)
  • application/web/openapi_registry.py
  • docs/api/openapi.yaml

Walkthrough

Adds persisted OIDC user identities and per-user resource selections, integrates persistence into the login callback, exposes feature-gated GET/PUT endpoints, documents them, and adds database, callback, and API tests.

Changes

User persistence and resource selection

Layer / File(s) Summary
User persistence and schema
migrations/versions/*, application/database/db.py, application/tests/user_model_test.py
Adds user and resource-selection tables, SQLAlchemy models, persistence helpers, uniqueness/cascade behavior, and persistence tests.
OIDC callback persistence
application/web/web_main.py, application/tests/web_main_test.py
Persists authenticated OIDC users when login is enabled and stores the persisted user ID in the session, with tests for missing claims and disabled login.
Resource-selection API and contract
application/web/web_main.py, application/web/openapi_registry.py, docs/api/openapi.yaml, application/tests/user_resources_api_test.py
Adds feature-gated GET/PUT routes, validation, OpenAPI definitions, documentation, and authentication/feature-flag behavior tests.

Estimated code review effort: 4 (Complex) | ~45 minutes

Suggested reviewers: pa04rth, paoga87, robvanderveer, northdpole

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: a per-user resource selection API endpoint.
Description check ✅ Passed The description accurately describes the new endpoint, gating, validation, and testing changes in the pull request.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
application/web/web_main.py (2)

1233-1246: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Consider disabling caching for per-user selection responses.

Neither GET nor PUT /rest/v1/user/resources sets Cache-Control, so a per-user list could be cached by an intermediary proxy or browser (e.g., shared-computer back/forward cache) and served to a different session.

Also applies to: 1249-1269

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@application/web/web_main.py` around lines 1233 - 1246, Disable caching for
both get_user_resources and the corresponding PUT user-resources handler by
adding an explicit no-cache/no-store Cache-Control response header to their JSON
responses. Ensure the header applies to authenticated per-user selections,
including the feature-disabled fallback where applicable.

846-882: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Tighten type hints for mypy compliance.

feature_enabled_or_default(is_enabled: Any, default_factory: Any) -> Any and _resolve_current_user(database): use Any/no annotations, unlike the fully-typed upsert_user/get_user_by_sub in db.py. As per coding guidelines, **/*.py should "Run make mypy for Python type checking" — loose Any typing here defeats that check for callable misuse (e.g. a non-callable default_factory).

def feature_enabled_or_default(
    is_enabled: Callable[[], bool], default_factory: Callable[[], Any]
) -> Callable[[Callable], Callable]:
    ...

def _resolve_current_user(database: "db.Node_collection") -> Optional["db.User"]:
    ...
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@application/web/web_main.py` around lines 846 - 882, Tighten the annotations
on feature_enabled_or_default by replacing Any with callable types for
is_enabled and default_factory and an appropriately typed decorator return,
importing the required typing symbols. Annotate _resolve_current_user’s database
parameter with db.Node_collection and its return value with Optional[db.User],
preserving the existing user lookup and creation behavior.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@application/database/db.py`:
- Around line 1101-1125: Update set_user_resource_selection to handle concurrent
writes using the same IntegrityError recovery pattern established by
upsert_user: catch the insert/commit failure, roll back the session, and retry
the replacement operation so concurrent PUTs do not surface an unhandled error.
Preserve deduplication and return the refreshed selection after a successful
commit.

In `@application/web/openapi_registry.py`:
- Around line 333-340: Update put_user_resources in
application/web/openapi_registry.py to configure PathSpec request-body
generation for a required JSON object containing a required selected array of
non-empty strings, then update docs/api/openapi.yaml at lines 516-533 with the
matching required application/json requestBody schema.

In `@application/web/web_main.py`:
- Around line 1256-1263: Normalize each string in selected by trimming leading
and trailing whitespace before passing it to set_user_resource_selection in
put_user_resources. Validate and persist the normalized values so equivalent
entries such as whitespace-padded and unpadded names deduplicate consistently.

---

Nitpick comments:
In `@application/web/web_main.py`:
- Around line 1233-1246: Disable caching for both get_user_resources and the
corresponding PUT user-resources handler by adding an explicit no-cache/no-store
Cache-Control response header to their JSON responses. Ensure the header applies
to authenticated per-user selections, including the feature-disabled fallback
where applicable.
- Around line 846-882: Tighten the annotations on feature_enabled_or_default by
replacing Any with callable types for is_enabled and default_factory and an
appropriately typed decorator return, importing the required typing symbols.
Annotate _resolve_current_user’s database parameter with db.Node_collection and
its return value with Optional[db.User], preserving the existing user lookup and
creation behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 688f202a-5958-4507-a9d8-43f0df37825c

📥 Commits

Reviewing files that changed from the base of the PR and between 6b6a5ac and 5156c51.

📒 Files selected for processing (9)
  • application/database/db.py
  • application/tests/user_model_test.py
  • application/tests/user_resources_api_test.py
  • application/tests/web_main_test.py
  • application/web/openapi_registry.py
  • application/web/web_main.py
  • docs/api/openapi.yaml
  • migrations/versions/a1b2c3d4e5f6_add_users_and_resource_selection.py
  • migrations/versions/f0e1d2c3b4a5_merge_heads_before_users.py

Comment thread application/database/db.py
Comment thread application/web/openapi_registry.py Outdated
Comment thread application/web/web_main.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
application/web/openapi_registry.py (1)

307-334: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Document the 401 response for both resource endpoints.

web_main.py returns 401 for anonymous requests when both feature flags are enabled, but these PathSpecs document only 200 and, for PUT, 400. Add a 401 response to both entries and keep docs/api/openapi.yaml synchronized so clients can discover the authentication failure.

Also applies to: 336-343

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@application/web/openapi_registry.py` around lines 307 - 334, Add a documented
401 response to both resource endpoint PathSpec entries, including the GET entry
identified by get_user_resources and the corresponding PUT entry, while
preserving their existing responses. Regenerate or update docs/api/openapi.yaml
so the OpenAPI specification remains synchronized and exposes the authentication
failure.
docs/api/openapi.yaml (2)

510-515: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Require selected in both response schemas.

The documented response is {"selected": [...]}, but omitting required: [selected] allows {} according to OpenAPI.

Proposed schema fix
                 type: object
+                required:
+                - selected
                 properties:
                   selected:

Apply this to both the GET and PUT response schemas.

Also applies to: 542-547

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/api/openapi.yaml` around lines 510 - 515, Add `required: [selected]` to
both GET and PUT response object schemas containing the `selected` property in
the OpenAPI definition, ensuring each documented response requires that field
while preserving its existing array-of-string type.

501-505: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Document the conditional 401 behavior.

The endpoint contract says anonymous users receive 401 when both login and MyOpenCRE flags are enabled, but neither operation documents that response. Clarify the GET description and add a 401 response to both GET and PUT; when either flag is disabled, document the empty-selection fallback instead.

Also applies to: 536-549

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/api/openapi.yaml` around lines 501 - 505, Update the selected-standards
GET and PUT OpenAPI definitions to document that authenticated access is
required when both login and MyOpenCRE are enabled, including a 401 response for
anonymous users. Clarify each GET/PUT description to state that the endpoint
returns an empty selection when either feature flag is disabled, and add the
corresponding 401 response entries to both operations.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@application/web/openapi_registry.py`:
- Around line 307-334: Add a documented 401 response to both resource endpoint
PathSpec entries, including the GET entry identified by get_user_resources and
the corresponding PUT entry, while preserving their existing responses.
Regenerate or update docs/api/openapi.yaml so the OpenAPI specification remains
synchronized and exposes the authentication failure.

In `@docs/api/openapi.yaml`:
- Around line 510-515: Add `required: [selected]` to both GET and PUT response
object schemas containing the `selected` property in the OpenAPI definition,
ensuring each documented response requires that field while preserving its
existing array-of-string type.
- Around line 501-505: Update the selected-standards GET and PUT OpenAPI
definitions to document that authenticated access is required when both login
and MyOpenCRE are enabled, including a 401 response for anonymous users. Clarify
each GET/PUT description to state that the endpoint returns an empty selection
when either feature flag is disabled, and add the corresponding 401 response
entries to both operations.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 55780973-4784-49cb-b6fd-8379a657de19

📥 Commits

Reviewing files that changed from the base of the PR and between 5156c51 and 95f914c.

📒 Files selected for processing (5)
  • application/database/db.py
  • application/tests/user_resources_api_test.py
  • application/web/openapi_registry.py
  • application/web/web_main.py
  • docs/api/openapi.yaml
🚧 Files skipped from review as they are similar to previous changes (3)
  • application/tests/user_resources_api_test.py
  • application/web/web_main.py
  • application/database/db.py

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant