Skip to content

Rely on per-entity filtering for listing instead of list-level gates#2523

Merged
p-hoffmann merged 1 commit into
webapi-3.0from
p-hoffmann/authz-listing-corrections
Jun 25, 2026
Merged

Rely on per-entity filtering for listing instead of list-level gates#2523
p-hoffmann merged 1 commit into
webapi-3.0from
p-hoffmann/authz-listing-corrections

Conversation

@p-hoffmann

@p-hoffmann p-hoffmann commented Jun 23, 2026

Copy link
Copy Markdown
Member

Why

WebAPI authorizes every request through a security principal (authenticated user or the built-in anonymous principal), deciding access from permission grants + per-entity filtering. #2521 hardened authorization and kept that model. #2521 gated list endpoints behind read/write permissions, when listing should stay open and have its contents filtered per principal (which several lists already did). This PR removes those list gates and relies on filtering, and removes a handful of isAuthenticated() checks that cut against the model. #2521's genuine endpoint protections are kept.

Kept from #2521 (these endpoints were genuinely open before — leave gated)

  • Source-scoped CDM data reads (vocabulary, evidence, cdmresults) → read:source / hasSourceAccess
  • Person-level data (person profile, cohort-sample) → source + cohort grants
  • Feature-extraction → read:feature-analysis
  • Admin operations (user/role/permission mgmt + LDAP import, source mgmt, cache, statistics, tag management) → admin:*

The fix: listing is open, contents are filtered

Content is protected one of two ways:

  • Filtered (open list, rows trimmed to grants) — the 7 artifact lists + /source/sources. 4 already did this (cohort-definition, concept-set, cohort-characterization, pathway); this PR adds IR, feature-analysis, reusable, and the source list. /byTags list-by-tag queries are opened too.
  • Gated with a new list permission (migration V2.99.0005, also in the B3.0.0 baseline), granted to the built-in public role, where there's no per-entity scope to filter — user/role/permission registries, jobs, tools, and the DDL/SqlRender utilities (which read no source data). Authenticated users can list; anonymous → 403.

Secondary cleanups

  • Removed the isAuthenticated() gates (getCurrentUser, generation reads, notifications) — generation reads keep their in-body entity+source checks.
  • Renamed the anonymous-access toggle security.anonymousAccess.enabledsecurity.allowAnonymousAccess (default true serves token-less requests as the anonymous principal; false requires a valid JWT for every endpoint outside the bootstrap allow-list).

Other fixes

  • Cohort-sample IDOR — require cohort + source access and bind the sample to the cohort/source in the path.

@p-hoffmann p-hoffmann force-pushed the p-hoffmann/authz-listing-corrections branch from 7dd1ed5 to 10dfc9a Compare June 23, 2026 14:09
@p-hoffmann p-hoffmann marked this pull request as draft June 23, 2026 14:18
@p-hoffmann p-hoffmann force-pushed the p-hoffmann/authz-listing-corrections branch from 10dfc9a to 82ca796 Compare June 23, 2026 16:04
@chrisknoll

chrisknoll commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

I have concerns about read:platform. I understand what you're doing where you're tyring to restrict listing of permissions, groups, users, i am 1) not sure we need to gate that (what's the implication of not having that restricted?) and 2) calling it a 'platform reading' type of permission is awkward (which is a symptom of why we neglected to gate these types of operations).

The second issue is that the tag management stuff that was under admin:tags got mapped over to read:platform. I don't think that's correct. Admin doesn't necessarily mean you have god level permissions to things: in our environment we would allow users to create their own tags and assign them to things they own. But that may be a restricted operation (as, only certain users shoudl be able to assign and create tags). Therefore, we put admin:tags and operations related to tags requires that permission. I have to check further, but it may be the case that owners can assign tags to their own assets without admin:tag....

@p-hoffmann p-hoffmann force-pushed the p-hoffmann/authz-listing-corrections branch 2 times, most recently from 0fcdcb2 to d0eaf61 Compare June 24, 2026 08:03
@p-hoffmann p-hoffmann marked this pull request as ready for review June 24, 2026 14:15
@p-hoffmann p-hoffmann requested a review from chrisknoll June 24, 2026 14:15
--
-- Placed above the 3.0.0 baseline so it runs on fresh installs (migrations below the baseline
-- are folded into it and skipped).
INSERT INTO ${ohdsiSchema}.sec_permission (value, description)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This migration should be put as V2.99.0005 so that it will be considred 'transitional migration leading up to the 3.0 version. The version you have specified here makes it look like there is a V3.0.1 of webapi, but there's not.

The idea for making all the migrations leading up the the 3.0 baseline is everything we're cahing from 2.15 to the 3.0 release goes in migration scripts V2.99.xxxx so that it will be present when we do the 3.0 baseline script. So please put this script into V2.99.0005, and add this permission to the 3.0 baseline script.

Future migrations leading up to the 3.0 release should be put into V2.99

-- Placed above the 3.0.0 baseline so it runs on fresh installs (migrations below the baseline
-- are folded into it and skipped).
INSERT INTO ${ohdsiSchema}.sec_permission (value, description)
SELECT 'list', 'List platform reference data (users, roles, permissions, jobs, tools) and use DDL/SqlRender utilities'

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

IN addition, the other examples of inserting permissoins is like this:

insert into ${ohdsiSchema}.sec_permission(id, value, description)
select nextval('${ohdsiSchema}.sec_permission_sequence'), value, description
FROM (
	VALUES
	('*', 'All Permissions'),
	('admin', 'All Admin Permissions'),
	('admin:source', 'Manage Sources'),

This example above is used to insert a bunch of permissions, but the point is we use nextval() to use the sequence, and also, we shoudl not be using 'where not exists'. If the permission actually exists, we want to throw an error in the migration because it is not expected.

# false – default-deny: every endpoint outside the login allow-list
# requires authentication. Use when every client logs in.
anonymousAccess:
enabled: true

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think that it's not a bad option to disable anonymous access to webapi, so if you want, you can restore this setting, and we can put in the JWT servlet filter chain that if this is not enabled, we reject the request if it doesn't have a valid JWT.

I wonder if this is better under security.xxx configuration options vs putting this at the top-level of the application.yaml.

WebAPI authorizes every request through a security principal (authenticated user or
the built-in anonymous principal), deciding access from permission grants + per-entity
filtering. #2521 hardened authorization and kept that model. It gated list endpoints
behind read/write permissions, when listing should stay open and have its contents
filtered per principal (which several lists already did). This change removes those
list gates and relies on filtering, and removes a handful of isAuthenticated() checks
that cut against the model. #2521's genuine endpoint protections are kept.

- Lists: drop the read/write @PreAuthorize gates on artifact lists (cohort, concept
  set, characterization, pathway, feature-analysis, IR, reusable) and the four /byTags
  queries. Listing is open; contents are filtered per-entity.
- Filtering: implement the missing per-entity filtering on the IR, feature-analysis
  and reusable lists, and filter /source/sources by source access.
- 'list' permission: add a 'list' permission (transitional migration V2.99.0005, also
  added to the B3.0.0 baseline) granted to the built-in public role, gating the
  surfaces with no per-entity scope to filter -- the user/role/permission registries,
  jobs, tools, and the DDL/SqlRender utilities. Authenticated users (public) can list;
  anonymous/ungranted -> 403.
- Source data gates kept from #2521: vocabulary, evidence, cdmresults stay on
  read:source / hasSourceAccess. DDL and SqlRender read no source data, so they move
  off read:source to 'list'. Feature-extraction stays on read:feature-analysis.
- Anonymous-access toggle: rename security.anonymousAccess.enabled to
  security.allowAnonymousAccess (default true serves token-less requests as the
  anonymous principal; false requires a valid JWT for every endpoint outside the
  bootstrap allow-list). SecurityIT covers the disabled path.
- Remove the isAuthenticated() gates (getCurrentUser, CC/pathway generation reads,
  notifications); generation reads keep their in-body entity+source checks.
- Cohort-sample person data: require both cohort and source access (was source-only)
  and bind the sample to the cohort/source in the path (closes an IDOR).
- BouncyCastle provider: register it once on the JVM via java.security.Security
  (guarded by getProvider) in EncryptorUtils.buildStringEncryptor, instead of
  constructing a fresh BouncyCastleProvider per call.
- Guardrail tests: allow-list the open-listing endpoints; assert anonymous is denied on
  the gated surfaces and a reader is allowed; repurpose AnonymousAccessIT for the
  enabled path and SecurityIT for the disabled path.

Verified: SecurityIT, EndpointAuthCoverageIT, AuthorizationAccessIT, AnonymousAccessIT,
AuthorizationPermissionStringsIT and SourceAccessIT all pass.
@p-hoffmann p-hoffmann force-pushed the p-hoffmann/authz-listing-corrections branch from d0eaf61 to fb971b8 Compare June 24, 2026 23:49
@chrisknoll

Copy link
Copy Markdown
Collaborator

@p-hoffmann , thank you for updates, I think this is good enough to merge in. We can always revisit the list functions if it needs to be more refined, and I appreciate granting it to the public role by default.

I have work on a api-key branch so when this gets merged in, i will be able to merge my branch and add that function next.

@chrisknoll chrisknoll dismissed their stale review June 25, 2026 00:26

Changes look good

@p-hoffmann p-hoffmann merged commit 6c1d300 into webapi-3.0 Jun 25, 2026
6 checks passed
@chrisknoll chrisknoll mentioned this pull request Jun 25, 2026
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