Skip to content

feat(organization): add suspension support#3007

Open
Piskoo wants to merge 2 commits intochainloop-dev:mainfrom
Piskoo:pfm-5391
Open

feat(organization): add suspension support#3007
Piskoo wants to merge 2 commits intochainloop-dev:mainfrom
Piskoo:pfm-5391

Conversation

@Piskoo
Copy link
Copy Markdown
Collaborator

@Piskoo Piskoo commented Apr 8, 2026

Summary

Added a suspended field to the Organization schema and a suspension middleware that enforces read-only mode when an org is suspended. The middleware classifies operations using ServerOperationsMap policy actions — read/list actions are allowed, everything else is blocked by default. An exempt regex covers empty-policy reads that lack action metadata and self-service writes so users can still leave or delete a suspended org.

Examples

Write fails

$ chainloop att init --project myproject --workflow suspend
WRN API contacted in insecure mode
WRN User-attended mode detected. This is intended for local testing only. For CI/CD or automated workflows, please use an API token.
This command will run against the organization "myorg"
Please confirm to continue y/N
y
ERR organization is suspended, read-only access only
exit status 1

$ chainloop wf contract create --name suspended
WRN API contacted in insecure mode
ERR organization is suspended, read-only access only
exit status 1

$ chainloop org api-token create --name suspended
WRN API contacted in insecure mode
ERR organization is suspended, read-only access only
exit status 1

Read goes through

$ chainloop wf contract describe --name vulnerabilities -o schema
WRN API contacted in insecure mode
apiVersion: chainloop.dev/v1
kind: Contract
metadata:
    name: vulnerabilities
spec:
    policies:
        materials:
            - ref: vulnerabilities

$ chainloop org member ls
WRN API contacted in insecure mode
┌──────────────────────────────────────┬───────────────────────┬────────┬─────────────────────┐
│ ID                                   │ EMAIL                 │ ROLE   │ JOINED AT           │
├──────────────────────────────────────┼───────────────────────┼────────┼─────────────────────┤
│ e0bc184d-4455-425f-9179-3cb276745329 │ john@chainloop.local  │ member │ 12 Dec 25 10:58 UTC │
├──────────────────────────────────────┼───────────────────────┼────────┼─────────────────────┤
│ c7740e0e-1a0c-467b-8bde-036b4d753bf7 │ sarah@chainloop.local │ owner  │ 16 Jun 25 09:09 UTC │
└──────────────────────────────────────┴───────────────────────┴────────┴─────────────────────┘
INF Showing [1-2] out of 2

Closes #3005

Piskoo added 2 commits April 8, 2026 14:34
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 April 8, 2026 13:43
Copy link
Copy Markdown

@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 26 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/pkg/authz/authz.go">

<violation number="1" location="app/controlplane/pkg/authz/authz.go:500">
P2: `regexp.MatchString` recompiles the pattern on every call. Since this runs in a request-hot loop over all `ServerOperationsMap` keys, precompile the regex patterns once at package init. Currently every OrgMetrics API call (and any unmapped operation) triggers ~55 regex compilations.

Consider extracting regex-pattern keys into a separate `map[*regexp.Regexp][]*Policy` built at init time, or precompile them with `regexp.MustCompile` in a package-level variable, similar to how `suspensionExemptRegexp` is handled in the suspension middleware.</violation>
</file>

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


// Second pass: regex match
for k, policies := range ServerOperationsMap {
found, _ := regexp.MatchString(k, apiOperation)
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Apr 8, 2026

Choose a reason for hiding this comment

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

P2: regexp.MatchString recompiles the pattern on every call. Since this runs in a request-hot loop over all ServerOperationsMap keys, precompile the regex patterns once at package init. Currently every OrgMetrics API call (and any unmapped operation) triggers ~55 regex compilations.

Consider extracting regex-pattern keys into a separate map[*regexp.Regexp][]*Policy built at init time, or precompile them with regexp.MustCompile in a package-level variable, similar to how suspensionExemptRegexp is handled in the suspension middleware.

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

<comment>`regexp.MatchString` recompiles the pattern on every call. Since this runs in a request-hot loop over all `ServerOperationsMap` keys, precompile the regex patterns once at package init. Currently every OrgMetrics API call (and any unmapped operation) triggers ~55 regex compilations.

Consider extracting regex-pattern keys into a separate `map[*regexp.Regexp][]*Policy` built at init time, or precompile them with `regexp.MustCompile` in a package-level variable, similar to how `suspensionExemptRegexp` is handled in the suspension middleware.</comment>

<file context>
@@ -475,3 +480,28 @@ func (Role) Values() (roles []string) {
+
+	// Second pass: regex match
+	for k, policies := range ServerOperationsMap {
+		found, _ := regexp.MatchString(k, apiOperation)
+		if found {
+			return policies, nil
</file context>
Fix with Cubic

// This covers two cases:
// - Empty-policy reads: operations mapped with {} in ServerOperationsMap that are actually reads
// - Self-service writes: operations the user needs to leave/delete even when suspended
var suspensionExemptRegexp = regexp.MustCompile(`controlplane.v1.CASCredentialsService/Get|controlplane.v1.UserService/(ListMemberships|SetCurrentMembership|DeleteMembership)|controlplane.v1.GroupService/(List|Get|ListMembers|ListProjects|ListPendingInvitations)|controlplane.v1.AuthService/DeleteAccount|controlplane.v1.OrganizationService/Delete$`)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is fragile and difficult to maintain if we add more endpoints. What if instead we extend the endpoint info in authz.go to add whether an endpoint is mutating or not? This would require adding all endpoints there though (default to mutating=true for maximum security if it's not found)
Another option is trying to get the metadata from the proto definitions, but I don't think Kratos exposes that.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nevermind, I've just seen that logic below. Thanks

@migmartri
Copy link
Copy Markdown
Member

I have mixed feelings about this feature; I’m not sure why we need to support read-only.

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.

Add organization suspension support

3 participants