Skip to content

Conversation

@bwalsh
Copy link

@bwalsh bwalsh commented Aug 4, 2025

New Features

  • Introduced Role-Based Access Control (RBAC) to indexd.
    • Added support for enforcing authorization on database operations via Arborist (adds RBAC to db operations).
    • Integrated a cached call to Arborist to reduce authorization lookup overhead (add cached call to Arborist).
    • Added configuration flag to enable or disable RBAC enforcement (Adds RBAC config).

Breaking Changes

  • None. The RBAC feature is gated by a configuration flag and maintains backward compatibility when disabled.

Bug Fixes

  • Improved error messaging for unauthorized access and token validation (Improve error handling).

Improvements

  • Added developer documentation for RBAC configuration and usage (developer documentation).
  • Added test coverage for RBAC behavior (test RBAC).

Dependency updates

  • None.

Deployment changes

  • New optional configuration setting: RBAC. When set to True, RBAC enforcement is active for protected records.
  • Arborist must be reachable by the indexd service for RBAC to function properly.

@bwalsh
Copy link
Author

bwalsh commented Aug 4, 2025

Not yet addressed:

  • Ensure ARE_RECORDS_DISCOVERABLE, GLOBAL_DISCOVERY_AUTHZ See discussion
  • Add a corresponding feature flag to helm chart

@Avantol13 - could you review and comment


🧾 User Story 1: Control Whether Records Are Discoverable

Title: Configurable Discovery of Indexd Records

As a platform operator,
I want to control whether indexd records are discoverable at all via a config flag,
So that I can prevent users from listing or retrieving records unless explicitly permitted.

Acceptance Criteria

  • Given ARE_RECORDS_DISCOVERABLE=False, when a client sends a request (with valid token):
    • indexd returns a 403 Forbidden for all reads with id e.g. GET /index/<did>
      • should only 403 in situations where this record itself would've been filtered out.
    • indexd filters out all records without read permission e.g. GET /index
  • Given ARE_RECORDS_DISCOVERABLE=True, the RBAC rules are ignored
  • This behavior is documented in indexd_settings.py and the README with a description of impact on runtime behavior.

🧾 User Story 2: Global Discovery Authorization Control

Title: Global Discovery Authz for Indexd Records, Support Discovery Access Independent from File Access

As a system administrator,
I want to configure a global authorization group for reading/discovering indexd records,
So that discovery can be gated separately from file access and we can support user registration workflows.

As a data commons architect,
I want to decouple discovery access (e.g. listing/searching records) from access to the underlying files,
So that I can implement workflows like "register to see what’s available", then "apply for access to download".

Acceptance Criteria

Assuming ARE_RECORDS_DISCOVERABLE=False

  • If GLOBAL_DISCOVERY_AUTHZ=None, then RBAC will use record-level authz fields are used to authorize GET requests to records. ie then record-level authz continues to govern access to records.
  • If GLOBAL_DISCOVERY_AUTHZ is set and if a user has permissions to the resource set in GLOBAL_DISCOVERY_AUTHZ, then RBAC will ignore filters for record-level authz fields and return all records.
  • Behavior is clearly documented, including the override effect of GLOBAL_DISCOVERY_AUTHZ.

📌 Configuration Summary

# Whether any records are discoverable at all
ARE_RECORDS_DISCOVERABLE = True  # default: True

# Override per-record authz for GET/read
# Only applies to record discovery (not file access)
# If None, use per-record `authz`
GLOBAL_DISCOVERY_AUTHZ = ["/indexd/discovery"]

@Avantol13
Copy link
Contributor

In general the comments above look good, thanks for all the detail.

This part:

indexd returns a 403 Forbidden for all reads with id e.g. GET /index/

I think needs to actually behave similar to READ filtering based on config. In other words, if you request a did and you do have access to authz, this should return 200. If you request a did and do you have access to the global authz that's configured, this should return 200. Basically this should only 403 in situations where this record itself would've been filtered out.

resources = self.arborist.auth_mapping()
return resources

@timed_cache(1800) # Cache for 30 minutes (typical JWT expiration time)
Copy link
Contributor

Choose a reason for hiding this comment

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

We can't hard-code this because we absolutely cannot have a response cached beyond the expiration. This has to be dynamic based on the expiration of the token. Our security is heavily reliant on the guarantee that the expiration ensures no access beyond that

Copy link
Author

@bwalsh bwalsh Aug 12, 2025

Choose a reason for hiding this comment

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

@Avantol13

Re. requirements

has to be dynamic based on the expiration of the token

Understood. At the same time, previous feedback stated:

Arborist allows no token to be sent on purpose, it allows assignment of anonymous access.

Additionally, AFAIK, no validation of the token occurs now in indexd. ie no calls to authutils.token.validate_jwt()

So, if there is a token:

  • 🆕 we can check to ensure it has not expired, use expiry time as ttl
  • already being used as a cache key

If there is no token:

  • 🆕 use maximum_ttl_seconds as ttl
  • 🆕 add authentication header to cache key (for basic and no auth)

Other:

  • 🆕 clean up any unused cache entries

Copy link
Author

Choose a reason for hiding this comment

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

done

@bwalsh
Copy link
Author

bwalsh commented Aug 13, 2025

In general the comments above look good, thanks for all the detail.

This part:

indexd returns a 403 Forbidden for all reads with id e.g. GET /index/

I think needs to actually behave similar to READ filtering based on config. In other words, if you request a did and you do have access to authz, this should return 200. If you request a did and do you have access to the global authz that's configured, this should return 200. Basically this should only 403 in situations where this record itself would've been filtered out.

Thanks. I edited the comment above

@bwalsh bwalsh requested a review from Avantol13 August 14, 2025 03:47
@bwalsh
Copy link
Author

bwalsh commented Aug 14, 2025

  • squash commits

@bwalsh
Copy link
Author

bwalsh commented Sep 23, 2025

@Avantol13 I've addressed all PR items. Please see #405 (comment) for a followup question.

Copy link
Contributor

@Avantol13 Avantol13 left a comment

Choose a reason for hiding this comment

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

re: db driver and how to centrally organize things. Here's my current thinking:

We should, theoretically, be able to move all the new code we need with stateful decisions out of the db driver b/c nothing really needs the db.

Here's my idea:

Put the authz check for discovery in a similar authorize decorator to this

def authorize(*p):
, maybe call it authorize_discovery and add that decorator everywhere you need. The logic in there should look like this:

  • if is_discovery_enabled (check config only)
    • Get user's authz (perhaps cache, could even use flask's per-request cache flask.g if that somehow simplifies - I know that won't save beyond the request)
    • if config GLOBAL_AUTHZ set
      • Check if the GLOBAL_authz is in the user's authz
    • if config GLOBAL not set
      • Check if user's authz contains records authz (this will require making a db call based on the request's ID)

done. Now we have appropriately denied access pre-blueprint logic with this decorator.

Within the blueprints that need the logic for filtering, now we can implement a shared set of util functions.

Before making a db query:

 * if is_discovery_enabled (check config only)
      * get user authz (perhaps from cache)
      * don't worry about GLOBAL AUTHZ at all b/c we already handled that in the new decorator of this endpoint, so we only get here if they are authorized
      * Add filter for records to db query based on user authz

This way, the stateful logic stays out of the db driver and in the request handling (which is where it should be) and we have minimized duplication of code as much as possible.

What do you think?


return result

def calculate_ttl(now, token) -> int | None:
Copy link
Contributor

Choose a reason for hiding this comment

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

I feel we might be overcomplicating this cache. Can't we simply keep the exp in the cache itself and instead of doing ttl math, just invalidate entries before now?

Copy link
Author

Choose a reason for hiding this comment

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

@Avantol13 - I might be missing what you are saying here. simply keep the exp in the cache
could you expand a bit more?

Copy link
Contributor

Choose a reason for hiding this comment

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

don't calculate ttl, just put the token and exp in the cache and instead of calculating it every time you check the cache, just get the entries with exp that isn't past now

Copy link
Contributor

Choose a reason for hiding this comment

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

it would simplify a lot of this code imo

@bwalsh
Copy link
Author

bwalsh commented Oct 6, 2025

Using ContextVar to decouple the web tier from the database driver

See https://github.com/ohsu-comp-bio/indexd/blob/672f16b469298d27bb0782627ad1df05ebd65efb/indexd/auth/discovery_context.py

Why this pattern

Current request-scoped values (Bearer token, Arborist resources) are collected in the web tier but consumed in the data tier (for row-level security, audit columns, tenancy filters, per-request logging, etc.). Passing these values explicitly through every call (blueprints → services → repositories) scatters boilerplate and causes wide signature churn.

contextvars.ContextVar provides implicit, request-scoped propagation without coupling the DB code to Flask. It gives you:

Separation of concerns: web code gathers state once; DB/ORM code reads it when needed.

  • Zero signature churn: no need to thread updated parameters through every function.

  • Async/greenlet safety: unlike threading.local() and many ad-hoc globals, ContextVar is designed for async tasks and framework context switches.

  • Testability: tests can set/reset context in one place without building fake requests.

  • Framework-agnostic DB layer: the driver/ORM code depends only on ContextVar, not on flask.request, flask.g, or app globals.

Copy link
Contributor

@Avantol13 Avantol13 left a comment

Choose a reason for hiding this comment

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

I took a look at the contextvar design. I appreciate the considerations and do see where you're coming from on some of the points, but I still don't agree with the approach.

The main thing I'm trying to ensure is appropriate separation between the web and data layer. We don't want explicit or implicit coupling. The data layer should not need to understand web layer. The endpoint handling the incoming request should be translating what it can and interacting with data through an explicit, well-defined interface, not relying on an implicit context being available. The contextvar would be a sort of hidden dependency in the data layer, which complicates this data layer abstraction.

I do understand and appreciate the attempt to reduce churn, but the implicit coupling imo is not worth the drawbacks.

I'm open to counters to the idea I had, but I really want to land on a solution that doesn't (explicitly or implicitly) couple the data layer to the web request context.


return result

def calculate_ttl(now, token) -> int | None:
Copy link
Contributor

Choose a reason for hiding this comment

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

don't calculate ttl, just put the token and exp in the cache and instead of calculating it every time you check the cache, just get the entries with exp that isn't past now


return result

def calculate_ttl(now, token) -> int | None:
Copy link
Contributor

Choose a reason for hiding this comment

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

it would simplify a lot of this code imo

@bwalsh
Copy link
Author

bwalsh commented Oct 7, 2025

Clarification question:

How will a web tier decorator will retrieve the record's authz? There are several possible database calls, each with their own set of possible parameters. Unclear how to a decorator would manage this. Assuming it could, wouldn't that double the db IO count for successful queries (once in the decorator + once in the driver)?

from above

image

@Avantol13
Copy link
Contributor

Clarification question:

How will a web tier decorator will retrieve the record's authz? There are several possible database calls, each with their own set of possible parameters. Unclear how to a decorator would manage this. Assuming it could, wouldn't that double the db IO count for successful queries (once in the decorator + once in the driver)?

from above

image

The decorator would need to use the driver to interact as the data access layer (DAL) based on the content of the request. e.g. take GUID, ask DAL for record, get GUID's authz from record. In the backwards-compatible case (where discovery is open) then all this logic should get skipped. In the new case, this does mean that you db hit to get the authz.

I'm open to reconsidering the decorator as the approach (this was just a cursory idea) so long as we figure out a way that the separation b/t web / DAL exists appropriately, but note that the decorator is just a function wrapping the decorated function, so you can pass in variables. e.g. theoretically you could get the index record from the db in the decorator logic (to get authz) and then pass the whole record into the endpoint function to avoid a double db hit (and in the case where we don't use this feature, just pass none and then hit the db if you don't have the record when you need it).

@bwalsh
Copy link
Author

bwalsh commented Oct 7, 2025

API Endpoints Documentation

A preliminary list of blueprint endpoints that are impacted
by discovery. Ordered by db driver and method.

indexd/index/drivers/alchemy.py

  • ids()
    - Blueprint: indexd.blueprint.indexd

    • Method: GET
    • URL: /index/
    • Method: POST
    • URL: /index/records
      - Blueprint: indexd.blueprint.admin
    • Method: GET
    • URL: /admin/index/records
      - Blueprint: indexd.blueprint.drs
    • Method: GET
    • URL: /ga4gh/drs/v1/objects
  • get_urls()

    • Blueprint: indexd.blueprint.indexd
      • Method: GET
      • URL: /index/urls
    • Blueprint: indexd.blueprint.admin
      • Method: GET
      • URL: /admin/index/urls
    • Blueprint: indexd.blueprint.drs
      • Method: GET
      • URL: /ga4gh/drs/v1/urls
  • get_all_versions()

    • Blueprint: indexd.blueprint.indexd
      • Method: GET
      • URL: /index/versions/<record_id>
    • Blueprint: indexd.blueprint.admin
      • Method: GET
      • URL: /admin/index/versions/<record_id>
    • Blueprint: indexd.blueprint.drs
      • Method: GET
      • URL: /ga4gh/drs/v1/objects/<record_id>/versions
  • get_latest_version()

    • Blueprint: indexd.blueprint.indexd

      • Method: GET
      • URL: /index/latest_version/<record_id>
    • Blueprint: indexd.blueprint.admin

      • Method: GET
      • URL: /admin/index/latest_version/<record_id>
    • Blueprint: indexd.blueprint.drs

      • Method: GET
      • URL: /ga4gh/drs/v1/objects/<record_id>/latest_version
  • get()

    • Blueprint: indexd.blueprint.indexd

      • Method: GET
      • URL: /index/<record_id>
    • Blueprint: indexd.blueprint.admin

      • Method: GET
      • URL: /admin/index/<record_id>
    • Blueprint: indexd.blueprint.drs

      • Method: GET
      • URL: /ga4gh/drs/v1/objects/<record_id>
  • get_with_nonstrict_prefix()

    • Blueprint: indexd.blueprint.indexd

      • Method: GET
      • URL: /index/prefix/
    • Blueprint: indexd.blueprint.admin

      • Method: GET
      • URL: /admin/index/prefix/
    • Blueprint: indexd.blueprint.drs

      • Method: GET
      • URL: /ga4gh/drs/v1/objects/prefix/
  • get_by_alias()

    • Blueprint: indexd.blueprint.indexd

      • Method: GET
      • URL: /index/alias/
    • Blueprint: indexd.blueprint.admin

      • Method: GET
      • URL: /admin/index/alias/
    • Blueprint: indexd.blueprint.drs

      • Method: GET
      • URL: /ga4gh/drs/v1/objects/alias/
  • get_aliases_for_did()

    • Blueprint: indexd.blueprint.indexd

      • Method: GET
      • URL: /index/aliases/
    • Blueprint: indexd.blueprint.admin

      • Method: GET
      • URL: /admin/index/aliases/
    • Blueprint: indexd.blueprint.drs

      • Method: GET
      • URL: /ga4gh/drs/v1/objects/aliases/
  • get_all_versions()

    • Blueprint: indexd.blueprint.indexd

      • Method: GET
      • URL: /index/versions/<record_id>
    • Blueprint: indexd.blueprint.admin

      • Method: GET
      • URL: /admin/index/versions/<record_id>
    • Blueprint: indexd.blueprint.drs

      • Method: GET
      • URL: /ga4gh/drs/v1/objects/<record_id>/versions

indexd/index/drivers/query/urls.py:

  • query_urls()

    • Blueprint: indexd.blueprint.indexd

      • Method: GET
      • URL: /index/urls
    • Blueprint: indexd.blueprint.admin

      • Method: GET
      • URL: /admin/index/urls
    • Blueprint: indexd.blueprint.drs

      • Method: GET
      • URL: /ga4gh/drs/v1/urls
  • query_metadata_by_key()

    • Blueprint: indexd.blueprint.indexd

      • Method: GET
      • URL: /index/metadata/query
    • Blueprint: indexd.blueprint.admin

      • Method: GET
      • URL: /admin/index/metadata/query
    • Blueprint: indexd.blueprint.drs

      • Method: GET
      • URL: /ga4gh/drs/v1/metadata/query

@bwalsh
Copy link
Author

bwalsh commented Oct 7, 2025

sequenceDiagram
autonumber
actor Client
participant Flask as Flask App
participant Hook as "before_request: ensure_auth_context()"
participant Arborist as "Arborist (auth service)"
participant Ctx as "ContextVar<request_ctx>"
participant Repo as "Driver.method()"
participant Decorator as "@authorize_discovery"
participant DB as "Indexd DB"

Client->>Flask: HTTP GET /ids …
note over Flask,Hook: middleware, called before all blueprints
Flask->>Hook: Trigger before_request
Hook->>Flask: Read config\nARE_RECORDS_DISCOVERABLE, GLOBAL_DISCOVERY_AUTHZ
alt are_records_discoverable == false
  Hook->>Arborist: auth.get_authorized_resources()
  Arborist-->>Hook: {resource -> [permissions]}
  Hook->>Hook: filter for read@indexd|*\ncompute can_user_discover
else are_records_discoverable == true
  Hook->>Hook: can_user_discover = true\nauthorized_resources = []
end
Hook->>Ctx: request_ctx.set({\n  are_records_discoverable,\n  can_user_discover,\n  authorized_resources\n})

note over Flask: call specific endpoint e.g. `/index`
Flask->>Decorator: call Driver.ids(...)
note over Decorator,Repo: Inject can_user_discover, authorized_resources only if args missing/None
Decorator->>Ctx: get_auth_context()
Decorator->>Repo: ids(...,\n  can_user_discover, authorized_resources)

Repo->>DB: query with discovery rules\n(ACL/authz filters)
DB-->>Repo: results
Repo-->>Flask: IDs payload
Flask-->>Client: 200 OK (JSON)
note over Ctx: Framework-agnostic state\n(no Flask import in Driver)

Loading

Request hits Flask → ensure_auth_context seeds a ContextVar with discovery flags. The @authorize_discovery decorator then pulls can_user_discover and authorized_resources and injects them into wrapped function, for example ids() , so the repo/DB layer stays Flask-agnostic and only consumes plain function parameters.

@bwalsh
Copy link
Author

bwalsh commented Oct 7, 2025

Thanks for the thoughtful review, @Avantol13 I’ve aligned the behavior and added a small infrastructure layer to make it reliable and testable.

What changed (referencing commit 2064504)

  • Discovery context + decorator. Introduced a ContextVar-backed discovery_context and an @authorize_discovery decorator that injects can_user_discover and authorized_resources into the driver entry points. This keeps the repo/DB code Flask-agnostic and avoids depending on flask.request while still giving us request-scoped auth state. See commit “adds authorize_discovery decorator” (206450476c81bec42cff222702093c2a3583eb50). ([GitHub])

  • Plain-parameter injection. The affected driver functions (e.g., ids(...) see above) were updated to accept plain old parameters can_user_discover and authorized_resources which the decorator injects when not explicitly provided by the caller.

  • This commit includes indexd/auth/discovery_context.py and related wiring plus unit tests to verify the injection semantics. ([GitHub])

Why this addresses the concern

The updated logic and the discovery context make that true for both list/search and direct-DID paths, while keeping the enforcement code decoupled from Flask and easy to unit test. ([GitHub])

Impact on blueprints & compatibility

  • No blueprint rework required. The change is wired at the app edge (before_request seeding the context) and at the repository boundary via the decorator. Blueprint routes/endpoints remain untouched.

  • Preserves existing functionality. This is additive and non-breaking; no API or schema changes.

  • Existing tests continue to pass. The current test suite runs without modification; new unit tests cover the decorator/context behavior.

Follow-ups

  • I’ll open a small PR in gen3-helm to surface ARE_RECORDS_DISCOVERABLE and GLOBAL_DISCOVERY_AUTHZ as explicit Helm values and update docs accordingly (not done yet; tracked as a follow-up from the earlier discussion). ([GitHub][2])

If anything looks off in the commit, I’m happy to tweak. Thanks again for the clear guidance, this made the behavior much more predictable.

Copy link
Contributor

@Avantol13 Avantol13 left a comment

Choose a reason for hiding this comment

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

Alright, here's where I'm at:

  • I am happy with the general adoption of the idea of the separating web/data
  • The implementation details using this contextvar approach could be simplified

The key items to solve still (let me know if I'm missing anything):

We don't want to hit the data layer twice for indexd records when checking their authz for discovery.

Solution: Use flask.g to store the record (already handles all the context properly per request) after hitting DAL in request decorator by extracting GUID from request in web layer and hitting existing DAL to get a record.

We don't want to hit arborist to get resources twice.

Solution: Use the cache that was built for persisting this - don't bother with any context, that's what the cache is for.

With the above: I don't think we need any home-grown parameter injection or manual context management at all. Sure we have to update existing blueprints to add a new decorator, I'm fine with that.

if any(resource in authorized_resources for resource in global_discovery_authz):
can_user_discover = True

# Remove global discovery authz resources from authorized_resources to avoid confusion
Copy link
Contributor

Choose a reason for hiding this comment

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

ooh, don't do this. It's possible for people for authorize data the same way they authorize discovery, in which case we would want the resource(s) in there

Copy link
Contributor

Choose a reason for hiding this comment

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

leave the authorized resources untouched, as they should represent the users full authz

Copy link
Author

Choose a reason for hiding this comment

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

Agreed. Will do.


from indexd import auth

discovery_context = ContextVar("discovery_context", default={})
Copy link
Contributor

Choose a reason for hiding this comment

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

We should just use the web framework's context capabilities already built-in. I'm worried about edge cases around this global.

https://flask.palletsprojects.com/en/stable/appcontext/

use flask.g instead: https://flask.palletsprojects.com/en/stable/appcontext/#storing-data

Copy link
Contributor

Choose a reason for hiding this comment

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

It's already built for this exact purpose and manages the clearing of the context as part of the general framework

Copy link
Author

Choose a reason for hiding this comment

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

Agreed, ensure_auth_context will no longer be called. Note that a "context" var holder is no longer as we are passing the variables explicitly

The main thing I'm trying to ensure is appropriate separation between the web and data layer. We don't want explicit or implicit coupling. The data layer should not need to understand web layer. The endpoint handling the incoming request should be translating what it can and interacting with data through an explicit, well-defined interface, not relying on an implicit context being available


set_auth_context(are_records_discoverable=are_records_discoverable,
can_user_discover=can_user_discover,
authorized_resources=authorized_resources)
Copy link
Contributor

Choose a reason for hiding this comment

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

isn't this duplicating some of the logic in the caching? There's got to be a cleaner way to do all this.

Flask → ensure_auth_context seeds a ContextVar with discovery flags

We don't really need the context to contain discovery flags, we should just appropriately react if they don't have access by intercepting the request - I think we may be overcomplicating things with the context now that I'm thinking more.

We don't need any context persisted other than ideally the user's authorization resources (so we don't have to hit arborist twice). We already have a cache for that information. So we just need to ensure that we use the cache in the inception of the request where we make the call and then we don't need any of this context.

And we may need the interaction with arborist to happen in a decorator capturing the request (not running before_request). We could keep the global config check before_request, but if there's a global authz then we have to handle that later with user info, so it might be better to actually not use before_request.

so I think the original idea still stands: #405 (review)

and to address this: #405 (comment)

You can use flask.g to store the record after interaction with the DAL and check for that later in the endpoints

Copy link
Author

Choose a reason for hiding this comment

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

Agreed, web tier will resolve config and authorized_resources on demand via auth_context() and pass to db layer. See latest change to get_all_index_record_versions:

Updates the get_all_index_record_versions view to call auth_context() and pass can_user_discover and authorized_resources to the index driver’s get_all_versions() method.

- Only inject for parameters actually present in the target signature.
- Only fill when the argument is missing or None (does not override explicit values).
"""
sig = inspect.signature(func)
Copy link
Contributor

Choose a reason for hiding this comment

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

This all scares me a bit. I don't really want to add home-grown injection that relies on downstream functions to know to use these parameter names. I would rather rely on the web framework's context management (even if that logic ends up in the endpoints) and a pattern of decorators (which already exists).

Then, in endpoints where you need to handle some per request context that may or may not exist, do:

indexd_record = flask.g.indexd_record or get_record()

Copy link
Author

@bwalsh bwalsh Oct 17, 2025

Choose a reason for hiding this comment

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

Agreed, ensure_auth_context will no longer be called.

deprecated methods (ensure_auth_context, authorize_discovery) removed.

@bwalsh
Copy link
Author

bwalsh commented Oct 14, 2025

Summary of the changes made in commit 0d7f5d706334ff9b5023f4bce39dfb3b7e7b7afd :

A single change was made to a single blueprint method,get_all_index_record_versions, to verify the desired changes.

If viable, this pattern would be repeated in all the blueprints listed above and deprecated methods (ensure_auth_context, authorize_discovery) removed.

These changes refactor how authorization context is handled and propagated. Instead of relying on global or context-setting side effects, the relevant authorization variables are now explicitly returned and passed through function calls, improving code clarity and maintainability. The changes also update method signatures to accept authorization parameters, and remove an authorization decorator in favor of direct parameter handling.


1. Auth Context Refactor

  • Introduced a new function: auth_context() in indexd/auth/discovery_context.py
    • Returns a tuple: (are_records_discoverable, can_user_discover, authorized_resources) instead of directly setting context.
    • The previous logic in ensure_auth_context() was split—now ensure_auth_context() calls auth_context() and sets context using its return values.

2. Blueprint Updates

  • In indexd/index/blueprint.py:
    • Imports and uses the new auth_context() function.
    • Updates the get_all_index_record_versions view to call auth_context() and pass can_user_discover and authorized_resources to the index driver’s get_all_versions() method.

3. Index Driver Changes

  • In indexd/index/drivers/alchemy.py:

    • The get_all_versions method signature now explicitly accepts can_user_discover and authorized_resources parameters (defaulting to True and None).
    • Removes the @authorize_discovery decorator from get_all_versions.
    • Updates _enforce_record_authz to check for authorized_resources before enforcing authorization.
  • In indexd/index/drivers/single_table_alchemy.py:

    • Updates get_all_versions method to accept can_user_discover and authorized_resources parameters, to comply with the changed method signature. Note, this is the first change to this module, as it was not impacted by other design iterations above

@Avantol13
Copy link
Contributor

Hey @bwalsh, per

If viable, this pattern would be repeated in all the blueprints listed above and deprecated methods (ensure_auth_context, authorize_discovery) removed.

Could you go ahead and make all those necessary changes so we can see the overall new approach without it being mixed in with the old approach? We'll have the git history if we need to refer back or revert, but it's hard to understand the implications without making the full change.

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.

2 participants