Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion .task/checksum/generate-ent-smart
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1e7267aa5256ff8b4c9800039c9f0bde
dd2da927942651c3f50a8e1ab9fea045
2 changes: 1 addition & 1 deletion .task/checksum/generate-graphql-smart
Original file line number Diff line number Diff line change
@@ -1 +1 @@
e54d7c4e359cb93c54182ce7d2a5cf5a
b15d09676260341056d596c1738c02d6
2 changes: 1 addition & 1 deletion .task/checksum/generate-openapi-smart
Original file line number Diff line number Diff line change
@@ -1 +1 @@
30a02216e08e66d20acf513f1022d1b
381fa11d6a440adc3c8edbd9f3b6311c
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,9 +192,11 @@ These are also created automatically when you setup the test user using `task cl
### OpenFGA Playground
You can load up a local openFGA environment with the compose setup in this
repository; `task fga:up` - this will launch an interactive playground where you
repository; `task docker:fga:up` - this will launch an interactive playground where you
can model permissions model(s) or changes to the models
If you have issues with CORS + loopback error, see this [issue](https://github.com/openfga/openfga/issues/338#issuecomment-3511304207) on openfga for a workaround.
### Creating a new Schema
To ease the effort required to add additional schemas into the system a
Expand Down
19 changes: 10 additions & 9 deletions Taskfile.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ includes:
env:
ATLAS_POSTGRES_DB_URI: "postgres:17-alpine"
TEST_DB_URL: "docker://postgres:17-alpine"
TEST_DB_CONTAINER_EXPIRY: "5" # in minutes
TEST_DB_CONTAINER_EXPIRY: "20" # in minutes
TEST_FGA_URL: "localhost:8080"
ENV: config
# go build
Expand Down Expand Up @@ -69,6 +69,7 @@ tasks:
deps:
- task: generate:core
- task: generate:catalog:smart
- task: fga:generate
cmds:
- task: generate:openapi:smart
- task: tidy
Expand Down Expand Up @@ -258,7 +259,7 @@ tasks:
desc: runs golangci-lint, the most annoying opinionated linter ever, for CI
## do not use --fix in CI
cmds:
- golangci-lint run --config=.golangci.yaml --verbose --concurrency 8 --verbose
- golangci-lint run --config=.golangci.yaml --verbose --verbose

go:lint:local:
desc: runs all the ci linter tasks in parallel for local linting equivalent to ci
Expand All @@ -271,20 +272,20 @@ tasks:
silent: true
desc: runs golangci-lint only on the graphapi package for CI, excluding generated code
cmds:
- golangci-lint run --config=.golangci.yaml --verbose --concurrency 8 $(go list ./... | grep internal/graphapi | grep -v generated | sed 's|github.com/theopenlane/core[^/]*/||' | sed 's|^|./|')
- golangci-lint run --config=.golangci.yaml --verbose $(go list ./... | grep internal/graphapi | grep -v generated | sed 's|github.com/theopenlane/core[^/]*/||' | sed 's|^|./|')

go:lint:ci:handlers:
silent: true
desc: runs golangci-lint only on the httpserve handlers package for CI
cmds:
- golangci-lint run --config=.golangci.yaml --verbose --concurrency 8 --verbose ./internal/httpserve/handlers/...
- golangci-lint run --config=.golangci.yaml --verbose --verbose ./internal/httpserve/handlers/...

go:lint:ci:no-api:
silent: true
desc: runs golangci-lint on all packages except graphapi and handlers for CI
cmds:
- |
golangci-lint run --config=.golangci.yaml --verbose --concurrency 8 $(
golangci-lint run --config=.golangci.yaml --verbose $(
go list ./... \
| grep -v /internal/graphapi \
| grep -v /internal/httpserve/handlers \
Expand Down Expand Up @@ -354,7 +355,7 @@ tasks:
GOFLAGS: "{{.GO_INLINE_FLAG}}"
aliases: ['testsum:ci']
cmds:
- go run gotest.tools/gotestsum@latest --junitfile junit.xml --junitfile-hide-empty-pkg --packages "./..." --format-hide-empty-pkg --format-icons hivis --rerun-fails=3 --rerun-fails-run-root-test --hide-summary output -- -p 20 -coverprofile=coverage.out -tags test
- go run gotest.tools/gotestsum@latest --junitfile junit.xml --junitfile-hide-empty-pkg --packages "./..." --format-hide-empty-pkg --format-icons hivis --rerun-fails=3 --rerun-fails-run-root-test --hide-summary output -- -p 20 -timeout 20m -coverprofile=coverage.out -tags test

go:testsum:ci:graph:
desc: runs tests only graph tests using gotestsum for CI
Expand All @@ -363,7 +364,7 @@ tasks:
GOFLAGS: "{{.GO_INLINE_FLAG}}"
aliases: ['testsum:ci:graph']
cmds:
- go run gotest.tools/gotestsum@latest --junitfile junit-graph.xml --junitfile-hide-empty-pkg --packages "github.com/theopenlane/core/internal/graphapi/" --format-hide-empty-pkg --format-icons hivis --rerun-fails=3 --rerun-fails-run-root-test --hide-summary output -- -p 20 -coverprofile=coverage-graph.out -tags test
- go run gotest.tools/gotestsum@latest --format standard-verbose --junitfile junit-graph.xml --junitfile-hide-empty-pkg --packages "github.com/theopenlane/core/internal/graphapi/" --format-hide-empty-pkg --rerun-fails=3 --rerun-fails-run-root-test --hide-summary output -- -p 20 -timeout 20m -coverprofile=coverage-graph.out -tags test

go:testsum:ci:handlers:
desc: runs tests only graph tests using gotestsum for CI
Expand All @@ -372,7 +373,7 @@ tasks:
GOFLAGS: "{{.GO_INLINE_FLAG}}"
aliases: ['testsum:ci:handlers']
cmds:
- go run gotest.tools/gotestsum@latest --junitfile junit-handlers.xml --junitfile-hide-empty-pkg --packages "github.com/theopenlane/core/internal/httpserve/handlers" --format-hide-empty-pkg --format-icons hivis --rerun-fails=3 --rerun-fails-run-root-test --hide-summary output -- -p 20 -coverprofile=coverage-handlers.out -tags test
- go run gotest.tools/gotestsum@latest --format standard-verbose --junitfile junit-handlers.xml --junitfile-hide-empty-pkg --packages "github.com/theopenlane/core/internal/httpserve/handlers" --format-hide-empty-pkg --rerun-fails=3 --rerun-fails-run-root-test --hide-summary output -- -p 20 -timeout 20m -coverprofile=coverage-handlers.out -tags test

go:testsum:ci:no-api:
desc: runs tests using gotestsum for CI
Expand All @@ -381,7 +382,7 @@ tasks:
GOFLAGS: "{{.GO_INLINE_FLAG}}"
aliases: ['testsum:ci:no-api', 'testsum:ci:no-graph', 'go:testsum:ci:no-graph']
cmds:
- go run gotest.tools/gotestsum@latest --junitfile junit-no-api.xml --junitfile-hide-empty-pkg --packages "$(go list ./... | grep -v /internal/graphapi | grep -v /internal/httpserve/handlers)" --format-hide-empty-pkg --format-icons hivis --rerun-fails=3 --rerun-fails-run-root-test --hide-summary output -- -p 20 -coverprofile=coverage-no-api.out -tags test
- go run gotest.tools/gotestsum@latest --format standard-verbose --junitfile junit-no-api.xml --junitfile-hide-empty-pkg --packages "$(go list ./... | grep -v /internal/graphapi | grep -v /internal/httpserve/handlers)" --format-hide-empty-pkg --rerun-fails=3 --rerun-fails-run-root-test --hide-summary output -- -p 20 -timeout 20m -coverprofile=coverage-no-api.out -tags test

go:testsum:ci:cover:
desc: pushes test results to buildkite using gotestsum
Expand Down
2 changes: 1 addition & 1 deletion cli/cmd/apitokens/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func init() {
createCmd.Flags().StringP("name", "n", "", "name of the api token token")
createCmd.Flags().StringP("description", "d", "", "description of the api token")
createCmd.Flags().DurationP("expiration", "e", 0, "duration of the api token to be valid, leave empty to never expire")
createCmd.Flags().StringSlice("scopes", []string{"read", "write"}, "scopes to associate with the api token")
createCmd.Flags().StringSlice("scopes", []string{"can_view", "can_edit"}, "scopes to associate with the api token"+scopeFlagConfig())
}

// createValidation validates the required fields for the command
Expand Down
36 changes: 36 additions & 0 deletions cli/cmd/apitokens/scopes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//go:build cli

package apitokens

import (
"fmt"
"sort"
"strings"

fgamodel "github.com/theopenlane/core/fga/model"
)

// scopeFlagConfig returns a description suffix listing available scopes.
func scopeFlagConfig() string {
scopes, err := fgamodel.RelationsForService()
if err != nil {
panic(fmt.Sprintf("failed to load service scopes: %v", err))
}

desc := fmt.Sprintf(" (available: %s)", strings.Join(scopes, ", "))

aliases := fgamodel.ScopeAliases()
if len(aliases) > 0 {
aliasPairs := make([]string, 0, len(aliases))

for alias, relation := range aliases {
aliasPairs = append(aliasPairs, fmt.Sprintf("%s->%s", alias, relation))
}

sort.Strings(aliasPairs)

desc = fmt.Sprintf("%s; aliases: %s", desc, strings.Join(aliasPairs, ", "))
}

return desc
}
2 changes: 1 addition & 1 deletion cli/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ require (
github.com/theopenlane/core/common v1.0.21
github.com/theopenlane/go-client v0.10.0
github.com/theopenlane/httpsling v0.3.0
github.com/theopenlane/iam v0.29.0
github.com/theopenlane/iam v0.30.1-0.20260519184713-f7e890743e3b
github.com/theopenlane/utils v0.7.0
golang.org/x/oauth2 v0.36.0
golang.org/x/term v0.43.0
Expand Down
3 changes: 1 addition & 2 deletions cli/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,7 @@ github.com/theopenlane/go-client v0.10.0 h1:gBEOEWXM3nP7VlXPcmgA/0RPWcvSz1XzFBwA
github.com/theopenlane/go-client v0.10.0/go.mod h1:usNDyObWwEJmNph2vAlsYfnx1q6jxXMwOKC6kQAesFo=
github.com/theopenlane/httpsling v0.3.0 h1:Bad0dGdqCqAB8UVDyVo+YCevzRvGHhmkK22F7T3pXtY=
github.com/theopenlane/httpsling v0.3.0/go.mod h1:iJc3XRLYTFIpfCnPpLZVMBP0xsWIPAb7ozARtQoclAE=
github.com/theopenlane/iam v0.29.0 h1:mmN5ZC5wfKHiMm69XpTfcqZl0IPMCFTEZuGHI7j2u4I=
github.com/theopenlane/iam v0.29.0/go.mod h1:WIWrlNu6gBNrEVNCtdAmGX7xrnv4+J2xPl0Frjw94rE=
github.com/theopenlane/iam v0.30.1-0.20260519184713-f7e890743e3b h1:coP8QFoYtwL03pj5d8EnPv8IIhZx4NiaBlHeYh8/uRI=
github.com/theopenlane/utils v0.7.0 h1:tSN9PBC8Ywn2As3TDW/1TAfWsVsodrccec40oAhiZgo=
github.com/theopenlane/utils v0.7.0/go.mod h1:7U9CDoVzCAFWw/JygR5ZhCKGwhHBnuJpK3Jgh1m59+w=
github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=
Expand Down
14 changes: 8 additions & 6 deletions common/enums/role.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,20 @@ import "io"
type Role string

var (
RoleOwner Role = "OWNER"
RoleAdmin Role = "ADMIN"
RoleMember Role = "MEMBER"
RoleUser Role = "USER"
RoleInvalid Role = "INVALID"
RoleOwner Role = "OWNER"
RoleAdmin Role = "ADMIN"
RoleSuperAdmin Role = "SUPER_ADMIN"
RoleMember Role = "MEMBER"
RoleAuditor Role = "AUDITOR"
RoleUser Role = "USER"
RoleInvalid Role = "INVALID"
)

// roleSchemaValues are the values exposed to ent schemas.
var roleSchemaValues = []Role{RoleAdmin, RoleMember}

// roleParseValues are all values accepted by ToRole.
var roleParseValues = []Role{RoleOwner, RoleAdmin, RoleMember, RoleUser}
var roleParseValues = []Role{RoleOwner, RoleAdmin, RoleSuperAdmin, RoleMember, RoleAuditor, RoleUser}

// Values returns a slice of strings that represents all the possible values of the Role enum.
// Possible default values are "ADMIN", "MEMBER"
Expand Down
1 change: 1 addition & 0 deletions common/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ require (
github.com/agnivade/levenshtein v1.2.1 // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/bmatcuk/doublestar v1.3.4 // indirect
github.com/brianvoe/gofakeit/v7 v7.15.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/fxamacker/cbor/v2 v2.9.2 // indirect
Expand Down
3 changes: 1 addition & 2 deletions common/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@ github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0=
github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
github.com/brianvoe/gofakeit/v7 v7.14.1 h1:a7fe3fonbj0cW3wgl5VwIKfZtiH9C3cLnwcIXWT7sow=
github.com/brianvoe/gofakeit/v7 v7.14.1/go.mod h1:QXuPeBw164PJCzCUZVmgpgHJ3Llj49jSLVkKPMtxtxA=
github.com/brianvoe/gofakeit/v7 v7.15.0 h1:kGLYAWN8tnmxq2PelKVK6zwpM7kMxdz9SGPH31mFkNs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand Down
2 changes: 2 additions & 0 deletions config/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ CORE_AUTHZ_CREDENTIALS_AUDIENCE=""
CORE_AUTHZ_CREDENTIALS_ISSUER=""
CORE_AUTHZ_CREDENTIALS_SCOPES=""
CORE_AUTHZ_MAXBATCHWRITESIZE="100"
CORE_AUTHZ_ENABLEPARENTCONTEXT=""
CORE_AUTHZ_PARENTCONTEXTSKIPKINDS=""
CORE_DB_DEBUG="false"
CORE_DB_DATABASENAME="openlane"
CORE_DB_DRIVERNAME="pgx"
Expand Down
10 changes: 10 additions & 0 deletions config/config-dev.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,16 @@ authz:
createnewmodel: true
credentials:
apitoken: "QKwHEmWX99RnFh28eSRJ3GWlfb2FQkL7toh1GJpzch1mMkVeMg"
parentcontextconditions:
- kind: group
name: public_group
context:
public: false

parentcontextskipkinds:
- organization
- user
- system

# session settings
sessions:
Expand Down
3 changes: 3 additions & 0 deletions config/config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,14 @@ authz:
issuer: ""
scopes: ""
enabled: true
enableparentcontext: false
hosturl: https://authz.theopenlane.io
maxbatchwritesize: 100
modelfile: fga/model/model.fga
modelid: ""
modulefile: ""
parentcontextconditions: []
parentcontextskipkinds: []
storeid: ""
storename: openlane
cloudflare:
Expand Down
13 changes: 13 additions & 0 deletions config/configmap-config-file.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,19 @@ data:
{{- if .Values.openlane.coreConfiguration.authz.maxbatchwritesize }}
maxbatchwritesize: {{ .Values.openlane.coreConfiguration.authz.maxbatchwritesize }}
{{- end }}
{{- if .Values.openlane.coreConfiguration.authz.enableparentcontext }}
enableparentcontext: {{ .Values.openlane.coreConfiguration.authz.enableparentcontext }}
{{- end }}
{{- $sliceValue := (.Values.openlane.coreConfiguration.authz.parentcontextskipkinds | default (list)) }}
{{- if gt (len $sliceValue) 0 }}
parentcontextskipkinds:
{{- toYaml $sliceValue | nindent 8 }}
{{- end }}
{{- $sliceValue := (.Values.openlane.coreConfiguration.authz.parentcontextconditions | default (list)) }}
{{- if gt (len $sliceValue) 0 }}
parentcontextconditions:
{{- toYaml $sliceValue | nindent 8 }}
{{- end }}
{{- end }}
{{- if .Values.openlane.coreConfiguration.db }}
db:
Expand Down
6 changes: 6 additions & 0 deletions config/helm-values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,12 @@ coreConfiguration:
scopes: "" # @schema type:string
# -- maximum number of writes per batch in a transaction
maxbatchwritesize: 100 # @schema type:integer; default:100
# -- disables the automatic addition of parent context tuples
enableparentcontext: false # @schema type:boolean
# -- entity kind names that should not have parent context tuples added
parentcontextskipkinds: []
# -- relationship conditions to apply on parent context tuples per entity kind
parentcontextconditions: []
# -- DB contains the database configuration for the ent client
db:
# -- debug enables printing the debug database logs
Expand Down
86 changes: 86 additions & 0 deletions db/MIGRATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Migration to v2 permissions

Migrate `parent` → `parent_context` for Organization Tuples

## Background

The FGA model is being updated to distinguish between structural parent ownership (e.g. a `control` owns a `control_implementation`) and organizational context (e.g. "this object belongs to organization X"). Previously both used the generic `parent` relation; the new model uses `parent_context` specifically for organization-scoped relations.

This allows permission rules to correctly differentiate between "inherit from owning object" and "inherit from org context", which is required for permission scopes and no longer requiring a user "owner" tuple for ownership, and instead can inherit from roles.

The exception being `files`, since files have a more complex relationship and are not always owned by orgs, and can also be owned by users, these are staying as `parent` even for `organization`.

### What to migrate

All tuples where:
- `relation = 'parent'`
- `_user LIKE 'organization:%'`
- `object_type != 'file'` (files use a separate ownership model)

### Steps

**1. Preview the rows that will be migrated**

```sql
SELECT
store,
object_type,
object_id,
'parent_context' AS relation,
_user,
user_type
FROM tuple
WHERE relation = 'parent'
AND _user LIKE 'organization:%'
AND object_type != 'file';
```

**2. Run the migration**

```sql
INSERT INTO tuple (store, object_type, object_id, relation, _user, user_type, ulid, inserted_at)
SELECT
store,
object_type,
object_id,
'parent_context',
_user,
user_type,
md5(store || object_type || object_id || 'parent_context' || _user),
NOW()
FROM tuple
WHERE relation = 'parent'
AND _user LIKE 'organization:%'
AND object_type != 'file'
ON CONFLICT (store, object_type, object_id, relation, _user) DO NOTHING;
```

The `ulid` is derived deterministically from the natural key so the insert is idempotent — safe to re-run.

**3. Verify**

Spot-check that `parent_context` rows now exist for the same objects that had `parent` rows:

```sql
SELECT object_type, COUNT(*)
FROM tuple
WHERE relation = 'parent_context'
AND _user LIKE 'organization:%'
GROUP BY object_type
ORDER BY object_type;
```

**4. Deploy the updated FGA model**

The new model must be deployed after the tuples are written. Deploying the model before the migration means objects will temporarily lose org-context permissions.

**5. (Optional) Clean up old `parent` tuples**

Once the new model is live and verified, the old `parent` + `organization:*` tuples are no longer used and can be deleted:

```sql
DELETE FROM tuple
WHERE relation = 'parent'
AND _user LIKE 'organization:%'
AND object_type != 'file';
```
Loading
Loading