JWT Token Organization Object Embedding#328
Conversation
There was a problem hiding this comment.
Pull request overview
Updates the cvmanager JWT cvmanager_data.organizations claim format to embed organization identifiers and metadata (id, name, email), then refactors the Intersection API and webapp to consume the new structure (notably enabling API queries to work from decoded org objects rather than org-name joins).
Changes:
- Webapp AuthToken typing + auth parsing updated to use
org_name(and accept new org fields). - Intersection API permission/token plumbing refactored so
getQualifiedOrgList(...)returnsOrganizationobjects and repositories accept org entities for authorization queries. - Keycloak custom user provider and extensive test updates to emit/validate the new token organization payload shape.
Reviewed changes
Copilot reviewed 34 out of 35 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| webapp/src/models/AuthToken.d.ts | Updates JWT token typing to the new organizations payload structure (id/name/email). |
| webapp/src/apis/auth-api.ts | Adjusts auth parsing to read org_name from the updated token. |
| webapp/src/apis/auth-api.test.ts | Updates auth parsing tests to match the new token organization object shape. |
| services/intersection-api/api/src/test/java/us/dot/its/jpo/ode/api/services/UserManagementServiceTest.java | Updates mocks/expectations for qualified org lists now returning Organization objects. |
| services/intersection-api/api/src/test/java/us/dot/its/jpo/ode/api/services/ScmsHealthServiceTest.java | Updates fixtures to create orgs with email/id inputs. |
| services/intersection-api/api/src/test/java/us/dot/its/jpo/ode/api/services/RsuManagementServiceTest.java | Updates tests for org-entity-based authorization and revised org-add/remove behaviors. |
| services/intersection-api/api/src/test/java/us/dot/its/jpo/ode/api/services/PermissionServiceTest.java | Refactors permission tests for new repository methods and org-entity qualified lists. |
| services/intersection-api/api/src/test/java/us/dot/its/jpo/ode/api/services/AdminIntersectionServiceTest.java | Refactors integration tests to mock permission/token org lists using embedded org objects. |
| services/intersection-api/api/src/test/java/us/dot/its/jpo/ode/api/models/keycloak/CvManagerAuthTokenTest.java | Updates token parsing tests for org_id/org_name/org_email and org-entity return types. |
| services/intersection-api/api/src/test/java/us/dot/its/jpo/ode/api/fixtures/TestFixtures.java | Extends org fixture creation to include id and email. |
| services/intersection-api/api/src/test/java/us/dot/its/jpo/ode/api/controllers/users/UserControllerTest.java | Updates controller tests to use hasRoleInOrgNames transition method. |
| services/intersection-api/api/src/test/java/us/dot/its/jpo/ode/api/controllers/devices/RsuControllerTest.java | Updates controller tests to use hasRoleInOrgNames transition method. |
| services/intersection-api/api/src/test/java/us/dot/its/jpo/ode/api/controllers/admin/AdminIntersectionControllerTest.java | Updates controller tests to work with qualified org lists as Organization objects. |
| services/intersection-api/api/src/main/java/us/dot/its/jpo/ode/api/services/UserManagementService.java | Refactors allowed selections and org-change handling to operate on authorized Organization entities. |
| services/intersection-api/api/src/main/java/us/dot/its/jpo/ode/api/services/RsuManagementService.java | Refactors RSU org-change handling to use authorized Organization entities from the token. |
| services/intersection-api/api/src/main/java/us/dot/its/jpo/ode/api/services/PermissionService.java | Changes permission checks to flow Organization entities through repository queries; adds transitional hasRoleInOrgNames. |
| services/intersection-api/api/src/main/java/us/dot/its/jpo/ode/api/services/AdminIntersectionService.java | Adjusts allowed selections to return org names while using org entities for RSU lookups. |
| services/intersection-api/api/src/main/java/us/dot/its/jpo/ode/api/repositories/UserRepository.java | Refactors org-scoped user existence checks to use organization entities. |
| services/intersection-api/api/src/main/java/us/dot/its/jpo/ode/api/repositories/SnmpCredentialRepository.java | Refactors credential-org checks to use organization entities. |
| services/intersection-api/api/src/main/java/us/dot/its/jpo/ode/api/repositories/RsuRepository.java | Refactors RSU-org existence and “allowed RSU IPs” query to accept organization entities. |
| services/intersection-api/api/src/main/java/us/dot/its/jpo/ode/api/repositories/RsuCredentialRepository.java | Refactors credential-org checks to use organization entities. |
| services/intersection-api/api/src/main/java/us/dot/its/jpo/ode/api/repositories/OrganizationRepository.java | Removes org-name-only helper query in favor of entity retrieval. |
| services/intersection-api/api/src/main/java/us/dot/its/jpo/ode/api/repositories/IntersectionRepository.java | Replaces explicit “exists by id/org names” query with derived method naming. |
| services/intersection-api/api/src/main/java/us/dot/its/jpo/ode/api/models/postgres/tables/Organization.java | Adds Hibernate-safe equals/hashCode based on id. |
| services/intersection-api/api/src/main/java/us/dot/its/jpo/ode/api/models/keycloak/CvManagerAuthToken.java | Updates token parsing to build Organization objects from JWT org entries; returns org entities for qualified lists. |
| services/intersection-api/api/src/main/java/us/dot/its/jpo/ode/api/controllers/users/UserController.java | Switches org qualification check to transitional hasRoleInOrgNames. |
| services/intersection-api/api/src/main/java/us/dot/its/jpo/ode/api/controllers/devices/RsuController.java | Switches org qualification check to transitional hasRoleInOrgNames. |
| services/intersection-api/api/src/main/java/us/dot/its/jpo/ode/api/controllers/admin/AdminIntersectionController.java | Updates operator-qualified org enforcement to derive name sets from org entities. |
| resources/keycloak/realm.json | Updates realm export content (now includes federated users/credentials structure). |
| resources/keycloak/custom-user-provider/src/test/java/com/cvmanager/auth/provider/user/pojos/UserObjectTest.java | Updates user-provider tests to parse/emit new org JSON payload (id/name/email/role). |
| resources/keycloak/custom-user-provider/src/test/java/com/cvmanager/auth/provider/user/pojos/OrganizationObjectTest.java | Updates org object parsing/serialization tests for new org payload fields. |
| resources/keycloak/custom-user-provider/src/test/java/com/cvmanager/auth/provider/user/CustomUserStorageProviderTest.java | Updates SQL expectations and org JSON aggregation for id/name/email. |
| resources/keycloak/custom-user-provider/src/test/java/com/cvmanager/auth/provider/mapper/CustomProtocolMapperTest.java | Updates protocol mapper test data to the new org JSON structure. |
| resources/keycloak/custom-user-provider/src/main/java/com/cvmanager/auth/provider/user/pojos/OrganizationObject.java | Updates org POJO serialization/deserialization to org_id/org_name/org_email. |
| resources/keycloak/custom-user-provider/src/main/java/com/cvmanager/auth/provider/user/CustomUserStorageProvider.java | Updates SQL JSON aggregation to embed org_id/org_name/org_email into token claim. |
Comments suppressed due to low confidence (4)
services/intersection-api/api/src/main/java/us/dot/its/jpo/ode/api/services/UserManagementService.java:209
organizations_to_modifyupdates roles without verifying that the target organization is withinauthorizedOrgs. A non-superuser with ADMIN in one org could modify a user’s role in another org they aren’t authorized for (as long as the relationship exists). Add the same authorization filtering/check used for removals before allowing modifications, and return a 403-style error when unauthorized.
if (patch.getOrganizationsToModify() != null && !patch.getOrganizationsToModify().isEmpty()) {
for (UserOrganizationDto org : patch.getOrganizationsToModify()) {
userOrganizationRepository.findByUserAndOrganization_Name(
user,
org.getOrganization()).ifPresent(userOrg -> {
Role role = roleRepository.findByNameIgnoreCase(org.getRole())
.orElseThrow(
() -> new IllegalArgumentException(
"Role not found: " + org.getRole()));
userOrg.setRole(role);
userOrganizationRepository.save(userOrg);
});
}
services/intersection-api/api/src/main/java/us/dot/its/jpo/ode/api/services/RsuManagementService.java:262
- When an org is missing or unauthorized, this code throws
IllegalArgumentException, which the API maps to HTTP 400. Previously these paths returned 403 (unauthorized) or 400/404 (not found). Conflating “not found” and “not authorized” into a 400 can break clients and makes error semantics misleading; consider throwingAccessDeniedExceptionfor unauthorized and a not-found/ResponseStatusExceptionfor missing organizations.
for (String orgName : patch.getOrganizationsToAdd()) {
Organization org = authorizedOrgs.stream()
.filter(o -> o.getName().equals(orgName))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException(
"Organization not found or user not authorized for: " + orgName));
resources/keycloak/realm.json:567
- This realm export now includes
federatedUsersentries with password credential blobs (secretData/credentialData) and environment-specific IDs/timestamps. Even if hashed, committing credential material makes the repo harder to audit and can cause brittle local-dev imports when IDs differ. Consider stripping federated user credential data fromrealm.json(keep only required realm/client configuration) or regenerating a minimal realm export intended for version control.
"federatedUsers": [
{
"id": "f:60b8a4e7-d427-4316-9ec0-cb8a6eeb34bd:fc3d8729-8526-4aaa-805b-d64bf3b93860",
"attributes": {
"fedNotBefore": ["0"]
},
"credentials": [
{
"id": "64f66e4b-868d-4805-835d-dba761c6e32b",
"type": "password",
"userLabel": "My password",
"createdDate": 1746774661287,
"secretData": "{\"value\":\"KXOpj1vu3jv+nf8Qvs7WJbd1NgZcOPg3ilp99kpW43U=\",\"salt\":\"+mN1WmKPXqvvdXKlhZF0LQ==\",\"additionalParameters\":{}}",
"credentialData": "{\"hashIterations\":5,\"algorithm\":\"argon2\",\"additionalParameters\":{\"hashLength\":[\"32\"],\"memory\":[\"7168\"],\"type\":[\"id\"],\"version\":[\"1.3\"],\"parallelism\":[\"1\"]}}"
}
],
"notBefore": 0,
"groups": []
},
{
"id": "f:60b8a4e7-d427-4316-9ec0-cb8a6eeb34bd:9f0db92b-b703-40ae-a7e4-d9d61fd28f10",
"attributes": {
"ENABLED": ["true"]
},
"credentials": [
{
"id": "91837b06-6983-4602-821e-66410b8480b1",
"type": "password",
"userLabel": "My password",
"createdDate": 1778705478359,
"secretData": "{\"value\":\"5l7SNdx83IUexqSWbrocbxjCCh56/kYDTOTe22JQtJQ=\",\"salt\":\"dJ+UD5QKi7PrGmeO9L6Gew==\",\"additionalParameters\":{}}",
"credentialData": "{\"hashIterations\":5,\"algorithm\":\"argon2\",\"additionalParameters\":{\"hashLength\":[\"32\"],\"memory\":[\"7168\"],\"type\":[\"id\"],\"version\":[\"1.3\"],\"parallelism\":[\"1\"]}}"
}
],
"notBefore": 0,
"groups": []
}
services/intersection-api/api/src/main/java/us/dot/its/jpo/ode/api/services/UserManagementService.java:165
- Unauthorized organization adds now throw
IllegalArgumentException(mapped to HTTP 400) rather than an authorization exception (403). If the user is authenticated but not allowed to add a user to the requested org, the response should be a forbidden-style error (e.g.,AccessDeniedException) to preserve correct auth semantics and avoid breaking API clients.
Organization organization = authorizedOrgs.stream()
.filter(o -> o.getName().equals(org.getOrganization()))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException(
"Organization not found or user not authorized for: " + org.getOrganization()));
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
PR Details
Description
Previously, Keycloak was embedding organization name and role pairs into the JWT token cvmanager_data claim. Now, the organization ID and email are also included.
This update enables the Intersection API to generate postgres Organization objects from the decoded JWT token and use them in queries, reducing joins and complexity throughout. The webapp has also been updated to process the new token format (the python api does not read org permissions from the JWT).
New Token Format:
{ ... "cvmanager_data": { "super_user": "0", "organizations": [ { "org_id": 1, "org_name": "Test Org", "org_email": "example@gmail.com", "role": "admin" }, } } }How Has This Been Tested?
This was tested by making manual requests to the admin endpoints through postman and
Types of changes
Checklist: