Skip to content

feat(api-token): allow org-level tokens to manage project-scoped tokens#2811

Open
Piskoo wants to merge 4 commits intochainloop-dev:mainfrom
Piskoo:pfm-4759
Open

feat(api-token): allow org-level tokens to manage project-scoped tokens#2811
Piskoo wants to merge 4 commits intochainloop-dev:mainfrom
Piskoo:pfm-4759

Conversation

@Piskoo
Copy link
Collaborator

@Piskoo Piskoo commented Mar 3, 2026

Org-level API tokens can now create, list, and revoke project-scoped API tokens.

🟢 Create project-scoped token with org-scoped token

$ chainloop org api-token create --name testorgcreate --project myproject --token <new org token>
WRN API contacted in insecure mode
INF API token provided to the command line
┌──────────────────────────────────────┬───────────────┬───────────────────┬─────────────┬─────────────────────┬────────────┬────────────┬──────────────┐
│ ID                                   │ NAME          │ SCOPE             │ DESCRIPTION │ CREATED AT          │ EXPIRES AT │ REVOKED AT │ LAST USED AT │
├──────────────────────────────────────┼───────────────┼───────────────────┼─────────────┼─────────────────────┼────────────┼────────────┼──────────────┤
│ c2a8395c-c42e-41c2-bbf9-521dbe7955d5 │ testorgcreate │ project/myproject │             │ 03 Mar 26 11:06 UTC │            │            │              │
└──────────────────────────────────────┴───────────────┴───────────────────┴─────────────┴─────────────────────┴────────────┴──────────────┴──────────────┘

Save the following token since it will not printed again:

<project token>

🔴 Create project-scoped token with project-scoped token

$ chainloop org api-token create --name testorgcreate --project myproject --token <project token>
WRN API contacted in insecure mode
INF API token provided to the command line
ERR operation not allowed
exit status 1

🔴 Create org-scoped token with project-scoped token

$ chainloop org api-token create --name testorgcreate --token <project token>
WRN API contacted in insecure mode
INF API token provided to the command line
ERR operation not allowed
exit status 1

🟢 List tokens with org-scoped token

$ chainloop org api-token ls --token <org token>
WRN API contacted in insecure mode
INF API token provided to the command line
┌──────────────────────────────────────┬───────────────┬───────────────────┬─────────────┬─────────────────────┬────────────┬────────────┬─────────────────────┐
│ ID                                   │ NAME          │ SCOPE             │ DESCRIPTION │ CREATED AT          │ EXPIRES AT │ REVOKED AT │ LAST USED AT        │
├──────────────────────────────────────┼───────────────┼───────────────────┼─────────────┼─────────────────────┼────────────┼────────────┼─────────────────────┤
│ c2a8395c-c42e-41c2-bbf9-521dbe7955d5 │ testorgcreate │ project/myproject │             │ 03 Mar 26 11:06 UTC │            │            │ 03 Mar 26 11:07 UTC │
└──────────────────────────────────────┴───────────────┴───────────────────┴─────────────┴─────────────────────┴────────────┴────────────┴─────────────────────┘

🔴 List tokens with project-scoped token

$ chainloop org api-token ls --token <project token>
WRN API contacted in insecure mode
INF API token provided to the command line
ERR operation not allowed
exit status 1

🔴 Revoke project-scoped token with project-scoped token

$ chainloop org api-token revoke --id c2a8395c-c42e-41c2-bbf9-521dbe7955d5 --token <project token>
WRN API contacted in insecure mode
INF API token provided to the command line
ERR operation not allowed
exit status 1

🟢 Revoke project-scoped token with org-scoped token

$ chainloop org api-token revoke --id c2a8395c-c42e-41c2-bbf9-521dbe7955d5 --token <org token>
WRN API contacted in insecure mode
INF API token provided to the command line
INF API token revoked!

🟢 Create project-scoped token with org-scoped token that was created in the past (List and revoke also works)

$ chainloop org api-token create --name witholdtoken --project myproject --token <org2 token>
WRN API contacted in insecure mode
INF API token provided to the command line
┌──────────────────────────────────────┬──────────────┬───────────────────┬─────────────┬─────────────────────┬────────────┬────────────┬──────────────┐
│ ID                                   │ NAME         │ SCOPE             │ DESCRIPTION │ CREATED AT          │ EXPIRES AT │ REVOKED AT │ LAST USED AT │
├──────────────────────────────────────┼──────────────┼───────────────────┼─────────────┼─────────────────────┼────────────┼────────────┼──────────────┤
│ 13db0c20-044e-4d64-aaee-4c97a6d80ef4 │ witholdtoken │ project/myproject │             │ 03 Mar 26 11:16 UTC │            │            │              │
└──────────────────────────────────────┴──────────────┴───────────────────┴─────────────┴─────────────────────┴────────────┴────────────┴──────────────┘

Save the following token since it will not printed again:

<project token>

Closes #2808

Piskoo added 2 commits March 3, 2026 12:19
Signed-off-by: Sylwester Piskozub <sylwesterpiskozub@gmail.com>
Signed-off-by: Sylwester Piskozub <sylwesterpiskozub@gmail.com>
@Piskoo Piskoo marked this pull request as ready for review March 3, 2026 11:37
@Piskoo Piskoo requested review from jiparis and migmartri March 3, 2026 11:37
Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 5 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="app/controlplane/internal/service/apitoken_test.go">

<violation number="1" location="app/controlplane/internal/service/apitoken_test.go:70">
P2: This test duplicates the service's authorization logic inline rather than calling `svc.List()`. It will pass even if the actual `List` method is broken or its authorization logic changes, providing false confidence. The same issue applies to `TestAPITokenService_Revoke_OrgTokenCannotRevokeOrgTokens` below. Consider using mocks for `APITokenUseCase` (e.g., via an interface or testify mock) so the tests exercise the real service methods end-to-end.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

ctx := context.Background()
ctx = entities.WithCurrentAPIToken(ctx, tc.token)

scope := mapTokenScope(pb.APITokenServiceListRequest_SCOPE_UNSPECIFIED)
Copy link

@cubic-dev-ai cubic-dev-ai bot Mar 3, 2026

Choose a reason for hiding this comment

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

P2: This test duplicates the service's authorization logic inline rather than calling svc.List(). It will pass even if the actual List method is broken or its authorization logic changes, providing false confidence. The same issue applies to TestAPITokenService_Revoke_OrgTokenCannotRevokeOrgTokens below. Consider using mocks for APITokenUseCase (e.g., via an interface or testify mock) so the tests exercise the real service methods end-to-end.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/controlplane/internal/service/apitoken_test.go, line 70:

<comment>This test duplicates the service's authorization logic inline rather than calling `svc.List()`. It will pass even if the actual `List` method is broken or its authorization logic changes, providing false confidence. The same issue applies to `TestAPITokenService_Revoke_OrgTokenCannotRevokeOrgTokens` below. Consider using mocks for `APITokenUseCase` (e.g., via an interface or testify mock) so the tests exercise the real service methods end-to-end.</comment>

<file context>
@@ -0,0 +1,132 @@
+			ctx := context.Background()
+			ctx = entities.WithCurrentAPIToken(ctx, tc.token)
+
+			scope := mapTokenScope(pb.APITokenServiceListRequest_SCOPE_UNSPECIFIED)
+			if token := entities.CurrentAPIToken(ctx); token != nil && token.ProjectID == nil {
+				scope = biz.APITokenScopeProject
</file context>
Fix with Cubic

Signed-off-by: Sylwester Piskozub <sylwesterpiskozub@gmail.com>
Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="app/controlplane/pkg/biz/apitoken_integration_test.go">

<violation number="1" location="app/controlplane/pkg/biz/apitoken_integration_test.go:153">
P2: Unsafe `append` may mutate `s.APIToken.DefaultAuthzPolicies`. In Go, `append` reuses the underlying array when capacity permits, which would silently corrupt the shared `DefaultAuthzPolicies` field. The production code already has a safe helper `withOrgLevelPolicies` (apitoken.go:47) — consider using it here, or copy the slice first.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Signed-off-by: Sylwester Piskozub <sylwesterpiskozub@gmail.com>
}

// Org-level API tokens can only create project-scoped tokens
if token := entities.CurrentAPIToken(ctx); token != nil && token.ProjectID == nil {
Copy link
Member

Choose a reason for hiding this comment

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

how come we didn't need to change anything in the middleware or in authz.go?


// OrgLevelAPITokenPolicies are additional policies granted only to org-level tokens.
// They allow managing project-scoped tokens.
var OrgLevelAPITokenPolicies = []*authz.Policy{
Copy link
Member

Choose a reason for hiding this comment

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

why is this var exposed?

}

// withOrgLevelPolicies returns a new slice with OrgLevelAPITokenPolicies appended.
func withOrgLevelPolicies(policies []*authz.Policy) []*authz.Policy {
Copy link
Member

Choose a reason for hiding this comment

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

this doesn't look that useful?


// Org-level tokens additionally get project-level API token management policies
if projectID == nil && orgUUID != nil {
policies = withOrgLevelPolicies(policies)
Copy link
Member

Choose a reason for hiding this comment

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

I'd personally just add the policies here, being very explicit saying that org level API tokens can manage project tokens.

// OrgLevelAPITokenPolicies are additional policies granted only to org-level tokens.
// They allow managing project-scoped tokens.
var OrgLevelAPITokenPolicies = []*authz.Policy{
authz.PolicyAPITokenCreate,
Copy link
Member

Choose a reason for hiding this comment

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

I'd call this PolicyProjectLevelAPITokenCreate since it's only project level tokens

}

// Org-level API tokens can only create project-scoped tokens
if token := entities.CurrentAPIToken(ctx); token != nil && token.ProjectID == nil {
Copy link
Member

Choose a reason for hiding this comment

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

this is something that could be done with Enforce otherwise the policies are worthless no?

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.

feat: create project-level token from org api token

2 participants