Skip to content

Commit 0e0a4d7

Browse files
authored
Permissions in GraphQL API (#581)
## Summary Add permission management to the GraphQL API, enabling the frontend to query and mutate authorization data alongside transit entities. This is the first step toward sunsetting the separate admin REST API — all permission reads and writes are now expressible through GraphQL. Also decouples `azchecker.Checker` from its `auth0` and `fga` dependencies, making it pluggable for different auth backends. ### New GraphQL types - `Tenant` — represents a tenant organization with `id`, `name`, `groups`, and `permissions` - `Group` — represents a group within a tenant with `id`, `name`, `tenant`, `feeds`, and `permissions` - `Permissions` — generic permissions type reused across all entity types, containing `actions`, `subjects`, `parent`, and `children` - `PermissionSubject` — describes a user/group with a relationship to an entity - `PermissionRef` — lightweight reference to a parent/child entity in the auth hierarchy ### New queries - `tenants` — list tenants accessible to the current user (returns empty list when auth not configured) - `groups` — list groups accessible to the current user (returns empty list when auth not configured) - `permissions` field added to `Feed` and `FeedVersion` types (nullable, returns null when auth not configured) ### New mutations - `permission_add(type, id, input)` — add a permission to any entity type - `permission_remove(type, id, input)` — remove a permission from any entity type - `permission_set_parent(type, id, input)` — set an entity's parent in the auth hierarchy - `tenant_save(id, input)` — update a tenant's name - `tenant_create_group(id, input)` — create a group within a tenant - `group_save(id, input)` — update a group's name ### Checker decoupling - `NewCheckerFromConfig` now accepts `UserProvider` and `FGAProvider` as parameters instead of constructing them internally - Removed `auth0` and `fga` imports from `azchecker/checker.go` — deployments inject their own providers - `CheckerConfig` reduced to just `GlobalAdmin` — all provider config is handled by callers - Read-only `Checker` implementations (without `PermissionManager` or `AdminManager`) are supported: permission fields return null, admin mutations return errors, queries/data path works normally - `AdminManager` interface documented with guidance that `UserProvider` implementations are responsible for user visibility scoping - `EKLookup` exported for callers that need to resolve symbolic tuple names to DB IDs (e.g., test setup) ### Implementation details - All permission resolvers use `cfg.Checker` from context, type-asserting to `PermissionManager` or `AdminManager` as needed - Read-only `permissions` field on Feed/FeedVersion returns `null` when `PermissionManager` is not configured (no error) - `tenants` and `groups` queries return empty lists when unconfigured, matching the nullable pattern - Mutation resolvers validate string enum inputs (`type`, `relation`) via `ObjectTypeString`/`RelationString` - `Group.feeds` resolves full `Feed` entities via `cfg.Finder.FindFeeds` (not just ObjectRef) - `loaders.go` nil-guard added to prevent panic when Finder is not configured - `"group"` added as alias for `"org"` in `ObjectType_value` — GraphQL API exposes "group" while FGA model uses "org" ### JWT hardening - Empty user ID check rejects tokens with missing subject or email (when `useEmailAsId` is set) - OIDC `WithSkipJWKValidation()` option for providers with incorrectly encoded x5t values ### Tests - 8 test functions with real in-memory OpenFGA (not mocks): tenant queries, group queries, feed/feed_version permissions, all mutations, invalid input handling, nil-config graceful degradation, unauthorized user access, mutation round-trip (add/verify/remove/verify) - Admin mutation tests use transaction rollback to avoid persisting test data - JWT middleware tests for empty subject and missing email edge cases - Tests require `TL_TEST_FGA_ENDPOINT` and test database ### Breaking changes - `NewCheckerFromConfig` signature changed: `(ctx, cfg, db) -> (*Checker, error)` is now `(cfg, userClient, fgaClient, db) -> *Checker` - Downstream callers (e.g., tlv2-apps) need to construct their own `UserProvider` and `FGAProvider` and pass them in ### Existing REST API - The admin REST API in `azchecker/server.go` is unchanged — same endpoints, same response shapes ## Test plan - Run `go test -run TestPermissionResolver -v ./server/gql/` — all 8 test functions pass - Run `go test -run TestNewJWTHandler -v ./server/auth/mw/jwtcheck/` — JWT tests pass - Run full test suite `go test ./server/gql/...` with test DB to verify no regressions on existing resolvers - Verify `go build ./server/...` compiles cleanly - Manual: query `{ feeds { permissions { actions } } }` with and without auth configured
1 parent 201120b commit 0e0a4d7

22 files changed

Lines changed: 4714 additions & 384 deletions

ext/bestpractices/flex_location_group_empty.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,3 @@ func (e *FlexLocationGroupEmptyCheck) Validate(ent tt.Entity) []error {
5454

5555
return nil
5656
}
57-

gqlgen.yml

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,10 +120,30 @@ models:
120120
Timezone:
121121
model:
122122
- "github.com/interline-io/transitland-lib/tt.Timezone"
123-
Feed:
123+
Tenant:
124+
fields:
125+
groups:
126+
resolver: true
127+
permissions:
128+
resolver: true
129+
Group:
130+
fields:
131+
tenant:
132+
resolver: true
133+
feeds:
134+
resolver: true
135+
permissions:
136+
resolver: true
137+
Feed:
124138
fields:
125139
onestop_id:
126140
fieldName: FeedID
141+
permissions:
142+
resolver: true
143+
FeedVersion:
144+
fields:
145+
permissions:
146+
resolver: true
127147
# Force resolvers
128148
StopObservation:
129149
extraFields:

gtfs/location_group.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,3 @@ func (ent *LocationGroup) Filename() string {
2626
func (ent *LocationGroup) TableName() string {
2727
return "gtfs_location_groups"
2828
}
29-

0 commit comments

Comments
 (0)