From c96f5fb47aad345eee3c6946391fb3bb1db4cac1 Mon Sep 17 00:00:00 2001 From: Miguel Martinez Date: Tue, 17 Feb 2026 11:29:55 +0100 Subject: [PATCH 1/3] feat(api-tokens): replace include_revoked boolean with StatusFilter enum Introduce a StatusFilter enum in APITokenServiceListRequest with three values: ACTIVE (default), REVOKED, and ALL. The previous boolean include_revoked field is marked deprecated. The biz layer APITokenListFilters gains a StatusFilter typed field, replacing IncludeRevoked bool. The data layer query is updated to apply the appropriate Ent predicate per filter value. The CLI gains a --status flag accepting active/revoked/all. The --all flag is deprecated and maps to --status all at runtime. Closes #2745 Signed-off-by: Miguel Martinez --- app/cli/cmd/organization_apitoken_list.go | 29 +++- app/cli/pkg/action/apitoken_list.go | 17 +- .../api/controlplane/v1/api_token.pb.go | 148 +++++++++++++----- .../api/controlplane/v1/api_token.proto | 17 +- .../api/controlplane/v1/api_token_grpc.pb.go | 2 +- .../gen/frontend/controlplane/v1/api_token.ts | 67 +++++++- ...APITokenServiceListRequest.jsonschema.json | 40 +++++ ....v1.APITokenServiceListRequest.schema.json | 40 +++++ app/controlplane/internal/service/apitoken.go | 16 +- app/controlplane/pkg/biz/apitoken.go | 23 ++- .../pkg/biz/apitoken_integration_test.go | 25 ++- app/controlplane/pkg/data/apitoken.go | 9 +- 12 files changed, 371 insertions(+), 62 deletions(-) diff --git a/app/cli/cmd/organization_apitoken_list.go b/app/cli/cmd/organization_apitoken_list.go index aa62c4086..18d1d0b8e 100644 --- a/app/cli/cmd/organization_apitoken_list.go +++ b/app/cli/cmd/organization_apitoken_list.go @@ -1,5 +1,5 @@ // -// Copyright 2023-2025 The Chainloop Authors. +// Copyright 2023-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -28,6 +28,7 @@ import ( func newAPITokenListCmd() *cobra.Command { var ( includeRevoked bool + statusFilter string project string scope string ) @@ -37,6 +38,12 @@ func newAPITokenListCmd() *cobra.Command { "global", } + var availableStatusFilters = []string{ + "active", + "revoked", + "all", + } + cmd := &cobra.Command{ Use: "list", Aliases: []string{"ls"}, @@ -46,10 +53,22 @@ func newAPITokenListCmd() *cobra.Command { return fmt.Errorf("invalid scope %q, please chose one of: %v", scope, availableScopes) } + if statusFilter != "" && !slices.Contains(availableStatusFilters, statusFilter) { + return fmt.Errorf("invalid status %q, please choose one of: %v", statusFilter, availableStatusFilters) + } + return nil }, RunE: func(cmd *cobra.Command, args []string) error { - res, err := action.NewAPITokenList(ActionOpts).Run(context.Background(), includeRevoked, project, scope) + // --all is deprecated: map it to --status all + if includeRevoked { + cmd.PrintErr("Warning: --all is deprecated, use --status all instead\n") + if statusFilter == "" { + statusFilter = "all" + } + } + + res, err := action.NewAPITokenList(ActionOpts).Run(context.Background(), statusFilter, project, scope) if err != nil { return fmt.Errorf("listing API tokens: %w", err) } @@ -58,7 +77,11 @@ func newAPITokenListCmd() *cobra.Command { }, } - cmd.Flags().BoolVarP(&includeRevoked, "all", "a", false, "show all API tokens including revoked ones") + cmd.Flags().BoolVarP(&includeRevoked, "all", "a", false, "Deprecated: use --status all instead") + if err := cmd.Flags().MarkDeprecated("all", "use --status all instead"); err != nil { + panic(err) + } + cmd.Flags().StringVar(&statusFilter, "status", "", fmt.Sprintf("filter by token status, available values: %v", availableStatusFilters)) cmd.Flags().StringVarP(&project, "project", "p", "", "filter by project name") cmd.Flags().StringVarP(&scope, "scope", "s", "", fmt.Sprintf("filter by scope, available scopes: %v", availableScopes)) return cmd diff --git a/app/cli/pkg/action/apitoken_list.go b/app/cli/pkg/action/apitoken_list.go index 489d53245..93500ca3c 100644 --- a/app/cli/pkg/action/apitoken_list.go +++ b/app/cli/pkg/action/apitoken_list.go @@ -1,5 +1,5 @@ // -// Copyright 2024-2025 The Chainloop Authors. +// Copyright 2024-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -30,10 +30,10 @@ func NewAPITokenList(cfg *ActionsOpts) *APITokenList { return &APITokenList{cfg} } -func (action *APITokenList) Run(ctx context.Context, includeRevoked bool, project string, scope string) ([]*APITokenItem, error) { +func (action *APITokenList) Run(ctx context.Context, statusFilter string, project string, scope string) ([]*APITokenItem, error) { client := pb.NewAPITokenServiceClient(action.cfg.CPConnection) - req := &pb.APITokenServiceListRequest{IncludeRevoked: includeRevoked} + req := &pb.APITokenServiceListRequest{StatusFilter: mapStatusFilter(statusFilter)} if project != "" { req.Project = &pb.IdentityReference{Name: &project} } @@ -65,3 +65,14 @@ func mapScope(scope string) pb.APITokenServiceListRequest_Scope { return pb.APITokenServiceListRequest_SCOPE_UNSPECIFIED } } + +func mapStatusFilter(status string) pb.APITokenServiceListRequest_StatusFilter { + switch status { + case "revoked": + return pb.APITokenServiceListRequest_STATUS_FILTER_REVOKED + case "all": + return pb.APITokenServiceListRequest_STATUS_FILTER_ALL + default: + return pb.APITokenServiceListRequest_STATUS_FILTER_ACTIVE + } +} diff --git a/app/controlplane/api/controlplane/v1/api_token.pb.go b/app/controlplane/api/controlplane/v1/api_token.pb.go index aaff44993..e5f82494f 100644 --- a/app/controlplane/api/controlplane/v1/api_token.pb.go +++ b/app/controlplane/api/controlplane/v1/api_token.pb.go @@ -1,5 +1,5 @@ // -// Copyright 2024-2025 The Chainloop Authors. +// Copyright 2024-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -87,6 +87,58 @@ func (APITokenServiceListRequest_Scope) EnumDescriptor() ([]byte, []int) { return file_controlplane_v1_api_token_proto_rawDescGZIP(), []int{4, 0} } +type APITokenServiceListRequest_StatusFilter int32 + +const ( + // Only active (non-revoked) tokens. This is the default. + APITokenServiceListRequest_STATUS_FILTER_ACTIVE APITokenServiceListRequest_StatusFilter = 0 + // Only revoked tokens. + APITokenServiceListRequest_STATUS_FILTER_REVOKED APITokenServiceListRequest_StatusFilter = 1 + // All tokens regardless of revocation status. + APITokenServiceListRequest_STATUS_FILTER_ALL APITokenServiceListRequest_StatusFilter = 2 +) + +// Enum value maps for APITokenServiceListRequest_StatusFilter. +var ( + APITokenServiceListRequest_StatusFilter_name = map[int32]string{ + 0: "STATUS_FILTER_ACTIVE", + 1: "STATUS_FILTER_REVOKED", + 2: "STATUS_FILTER_ALL", + } + APITokenServiceListRequest_StatusFilter_value = map[string]int32{ + "STATUS_FILTER_ACTIVE": 0, + "STATUS_FILTER_REVOKED": 1, + "STATUS_FILTER_ALL": 2, + } +) + +func (x APITokenServiceListRequest_StatusFilter) Enum() *APITokenServiceListRequest_StatusFilter { + p := new(APITokenServiceListRequest_StatusFilter) + *p = x + return p +} + +func (x APITokenServiceListRequest_StatusFilter) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (APITokenServiceListRequest_StatusFilter) Descriptor() protoreflect.EnumDescriptor { + return file_controlplane_v1_api_token_proto_enumTypes[1].Descriptor() +} + +func (APITokenServiceListRequest_StatusFilter) Type() protoreflect.EnumType { + return &file_controlplane_v1_api_token_proto_enumTypes[1] +} + +func (x APITokenServiceListRequest_StatusFilter) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use APITokenServiceListRequest_StatusFilter.Descriptor instead. +func (APITokenServiceListRequest_StatusFilter) EnumDescriptor() ([]byte, []int) { + return file_controlplane_v1_api_token_proto_rawDescGZIP(), []int{4, 1} +} + type APITokenServiceCreateRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` @@ -281,12 +333,17 @@ func (*APITokenServiceRevokeResponse) Descriptor() ([]byte, []int) { } type APITokenServiceListRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - IncludeRevoked bool `protobuf:"varint,1,opt,name=include_revoked,json=includeRevoked,proto3" json:"include_revoked,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + // Deprecated: use status_filter instead. + // + // Deprecated: Marked as deprecated in controlplane/v1/api_token.proto. + IncludeRevoked bool `protobuf:"varint,1,opt,name=include_revoked,json=includeRevoked,proto3" json:"include_revoked,omitempty"` // optional project reference to filter by Project *IdentityReference `protobuf:"bytes,4,opt,name=project,proto3" json:"project,omitempty"` // filter by the scope of the token - Scope APITokenServiceListRequest_Scope `protobuf:"varint,2,opt,name=scope,proto3,enum=controlplane.v1.APITokenServiceListRequest_Scope" json:"scope,omitempty"` + Scope APITokenServiceListRequest_Scope `protobuf:"varint,2,opt,name=scope,proto3,enum=controlplane.v1.APITokenServiceListRequest_Scope" json:"scope,omitempty"` + // Filter tokens by their revocation status. Defaults to STATUS_FILTER_ACTIVE. + StatusFilter APITokenServiceListRequest_StatusFilter `protobuf:"varint,5,opt,name=status_filter,json=statusFilter,proto3,enum=controlplane.v1.APITokenServiceListRequest_StatusFilter" json:"status_filter,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -321,6 +378,7 @@ func (*APITokenServiceListRequest) Descriptor() ([]byte, []int) { return file_controlplane_v1_api_token_proto_rawDescGZIP(), []int{4} } +// Deprecated: Marked as deprecated in controlplane/v1/api_token.proto. func (x *APITokenServiceListRequest) GetIncludeRevoked() bool { if x != nil { return x.IncludeRevoked @@ -342,6 +400,13 @@ func (x *APITokenServiceListRequest) GetScope() APITokenServiceListRequest_Scope return APITokenServiceListRequest_SCOPE_UNSPECIFIED } +func (x *APITokenServiceListRequest) GetStatusFilter() APITokenServiceListRequest_StatusFilter { + if x != nil { + return x.StatusFilter + } + return APITokenServiceListRequest_STATUS_FILTER_ACTIVE +} + type APITokenServiceListResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Result []*APITokenItem `protobuf:"bytes,1,rep,name=result,proto3" json:"result,omitempty"` @@ -458,15 +523,20 @@ const file_controlplane_v1_api_token_proto_rawDesc = "" + "\x03jwt\x18\x02 \x01(\tR\x03jwt\"8\n" + "\x1cAPITokenServiceRevokeRequest\x12\x18\n" + "\x02id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x02id\"\x1f\n" + - "\x1dAPITokenServiceRevokeResponse\"\x91\x02\n" + - "\x1aAPITokenServiceListRequest\x12'\n" + - "\x0finclude_revoked\x18\x01 \x01(\bR\x0eincludeRevoked\x12<\n" + + "\x1dAPITokenServiceRevokeResponse\"\xd0\x03\n" + + "\x1aAPITokenServiceListRequest\x12+\n" + + "\x0finclude_revoked\x18\x01 \x01(\bB\x02\x18\x01R\x0eincludeRevoked\x12<\n" + "\aproject\x18\x04 \x01(\v2\".controlplane.v1.IdentityReferenceR\aproject\x12G\n" + - "\x05scope\x18\x02 \x01(\x0e21.controlplane.v1.APITokenServiceListRequest.ScopeR\x05scope\"C\n" + + "\x05scope\x18\x02 \x01(\x0e21.controlplane.v1.APITokenServiceListRequest.ScopeR\x05scope\x12]\n" + + "\rstatus_filter\x18\x05 \x01(\x0e28.controlplane.v1.APITokenServiceListRequest.StatusFilterR\fstatusFilter\"C\n" + "\x05Scope\x12\x15\n" + "\x11SCOPE_UNSPECIFIED\x10\x00\x12\x11\n" + "\rSCOPE_PROJECT\x10\x01\x12\x10\n" + - "\fSCOPE_GLOBAL\x10\x02\"T\n" + + "\fSCOPE_GLOBAL\x10\x02\"Z\n" + + "\fStatusFilter\x12\x18\n" + + "\x14STATUS_FILTER_ACTIVE\x10\x00\x12\x19\n" + + "\x15STATUS_FILTER_REVOKED\x10\x01\x12\x15\n" + + "\x11STATUS_FILTER_ALL\x10\x02\"T\n" + "\x1bAPITokenServiceListResponse\x125\n" + "\x06result\x18\x01 \x03(\v2\x1d.controlplane.v1.APITokenItemR\x06result2\xc6\x02\n" + "\x0fAPITokenService\x12g\n" + @@ -486,40 +556,42 @@ func file_controlplane_v1_api_token_proto_rawDescGZIP() []byte { return file_controlplane_v1_api_token_proto_rawDescData } -var file_controlplane_v1_api_token_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_controlplane_v1_api_token_proto_enumTypes = make([]protoimpl.EnumInfo, 2) var file_controlplane_v1_api_token_proto_msgTypes = make([]protoimpl.MessageInfo, 7) var file_controlplane_v1_api_token_proto_goTypes = []any{ (APITokenServiceListRequest_Scope)(0), // 0: controlplane.v1.APITokenServiceListRequest.Scope - (*APITokenServiceCreateRequest)(nil), // 1: controlplane.v1.APITokenServiceCreateRequest - (*APITokenServiceCreateResponse)(nil), // 2: controlplane.v1.APITokenServiceCreateResponse - (*APITokenServiceRevokeRequest)(nil), // 3: controlplane.v1.APITokenServiceRevokeRequest - (*APITokenServiceRevokeResponse)(nil), // 4: controlplane.v1.APITokenServiceRevokeResponse - (*APITokenServiceListRequest)(nil), // 5: controlplane.v1.APITokenServiceListRequest - (*APITokenServiceListResponse)(nil), // 6: controlplane.v1.APITokenServiceListResponse - (*APITokenServiceCreateResponse_APITokenFull)(nil), // 7: controlplane.v1.APITokenServiceCreateResponse.APITokenFull - (*IdentityReference)(nil), // 8: controlplane.v1.IdentityReference - (*durationpb.Duration)(nil), // 9: google.protobuf.Duration - (*APITokenItem)(nil), // 10: controlplane.v1.APITokenItem + (APITokenServiceListRequest_StatusFilter)(0), // 1: controlplane.v1.APITokenServiceListRequest.StatusFilter + (*APITokenServiceCreateRequest)(nil), // 2: controlplane.v1.APITokenServiceCreateRequest + (*APITokenServiceCreateResponse)(nil), // 3: controlplane.v1.APITokenServiceCreateResponse + (*APITokenServiceRevokeRequest)(nil), // 4: controlplane.v1.APITokenServiceRevokeRequest + (*APITokenServiceRevokeResponse)(nil), // 5: controlplane.v1.APITokenServiceRevokeResponse + (*APITokenServiceListRequest)(nil), // 6: controlplane.v1.APITokenServiceListRequest + (*APITokenServiceListResponse)(nil), // 7: controlplane.v1.APITokenServiceListResponse + (*APITokenServiceCreateResponse_APITokenFull)(nil), // 8: controlplane.v1.APITokenServiceCreateResponse.APITokenFull + (*IdentityReference)(nil), // 9: controlplane.v1.IdentityReference + (*durationpb.Duration)(nil), // 10: google.protobuf.Duration + (*APITokenItem)(nil), // 11: controlplane.v1.APITokenItem } var file_controlplane_v1_api_token_proto_depIdxs = []int32{ - 8, // 0: controlplane.v1.APITokenServiceCreateRequest.project_reference:type_name -> controlplane.v1.IdentityReference - 9, // 1: controlplane.v1.APITokenServiceCreateRequest.expires_in:type_name -> google.protobuf.Duration - 7, // 2: controlplane.v1.APITokenServiceCreateResponse.result:type_name -> controlplane.v1.APITokenServiceCreateResponse.APITokenFull - 8, // 3: controlplane.v1.APITokenServiceListRequest.project:type_name -> controlplane.v1.IdentityReference + 9, // 0: controlplane.v1.APITokenServiceCreateRequest.project_reference:type_name -> controlplane.v1.IdentityReference + 10, // 1: controlplane.v1.APITokenServiceCreateRequest.expires_in:type_name -> google.protobuf.Duration + 8, // 2: controlplane.v1.APITokenServiceCreateResponse.result:type_name -> controlplane.v1.APITokenServiceCreateResponse.APITokenFull + 9, // 3: controlplane.v1.APITokenServiceListRequest.project:type_name -> controlplane.v1.IdentityReference 0, // 4: controlplane.v1.APITokenServiceListRequest.scope:type_name -> controlplane.v1.APITokenServiceListRequest.Scope - 10, // 5: controlplane.v1.APITokenServiceListResponse.result:type_name -> controlplane.v1.APITokenItem - 10, // 6: controlplane.v1.APITokenServiceCreateResponse.APITokenFull.item:type_name -> controlplane.v1.APITokenItem - 1, // 7: controlplane.v1.APITokenService.Create:input_type -> controlplane.v1.APITokenServiceCreateRequest - 5, // 8: controlplane.v1.APITokenService.List:input_type -> controlplane.v1.APITokenServiceListRequest - 3, // 9: controlplane.v1.APITokenService.Revoke:input_type -> controlplane.v1.APITokenServiceRevokeRequest - 2, // 10: controlplane.v1.APITokenService.Create:output_type -> controlplane.v1.APITokenServiceCreateResponse - 6, // 11: controlplane.v1.APITokenService.List:output_type -> controlplane.v1.APITokenServiceListResponse - 4, // 12: controlplane.v1.APITokenService.Revoke:output_type -> controlplane.v1.APITokenServiceRevokeResponse - 10, // [10:13] is the sub-list for method output_type - 7, // [7:10] is the sub-list for method input_type - 7, // [7:7] is the sub-list for extension type_name - 7, // [7:7] is the sub-list for extension extendee - 0, // [0:7] is the sub-list for field type_name + 1, // 5: controlplane.v1.APITokenServiceListRequest.status_filter:type_name -> controlplane.v1.APITokenServiceListRequest.StatusFilter + 11, // 6: controlplane.v1.APITokenServiceListResponse.result:type_name -> controlplane.v1.APITokenItem + 11, // 7: controlplane.v1.APITokenServiceCreateResponse.APITokenFull.item:type_name -> controlplane.v1.APITokenItem + 2, // 8: controlplane.v1.APITokenService.Create:input_type -> controlplane.v1.APITokenServiceCreateRequest + 6, // 9: controlplane.v1.APITokenService.List:input_type -> controlplane.v1.APITokenServiceListRequest + 4, // 10: controlplane.v1.APITokenService.Revoke:input_type -> controlplane.v1.APITokenServiceRevokeRequest + 3, // 11: controlplane.v1.APITokenService.Create:output_type -> controlplane.v1.APITokenServiceCreateResponse + 7, // 12: controlplane.v1.APITokenService.List:output_type -> controlplane.v1.APITokenServiceListResponse + 5, // 13: controlplane.v1.APITokenService.Revoke:output_type -> controlplane.v1.APITokenServiceRevokeResponse + 11, // [11:14] is the sub-list for method output_type + 8, // [8:11] is the sub-list for method input_type + 8, // [8:8] is the sub-list for extension type_name + 8, // [8:8] is the sub-list for extension extendee + 0, // [0:8] is the sub-list for field type_name } func init() { file_controlplane_v1_api_token_proto_init() } @@ -535,7 +607,7 @@ func file_controlplane_v1_api_token_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_controlplane_v1_api_token_proto_rawDesc), len(file_controlplane_v1_api_token_proto_rawDesc)), - NumEnums: 1, + NumEnums: 2, NumMessages: 7, NumExtensions: 0, NumServices: 1, diff --git a/app/controlplane/api/controlplane/v1/api_token.proto b/app/controlplane/api/controlplane/v1/api_token.proto index 609162da0..729487974 100644 --- a/app/controlplane/api/controlplane/v1/api_token.proto +++ b/app/controlplane/api/controlplane/v1/api_token.proto @@ -1,5 +1,5 @@ // -// Copyright 2024-2025 The Chainloop Authors. +// Copyright 2024-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -55,7 +55,8 @@ message APITokenServiceRevokeRequest { message APITokenServiceRevokeResponse {} message APITokenServiceListRequest { - bool include_revoked = 1; + // Deprecated: use status_filter instead. + bool include_revoked = 1 [deprecated = true]; // optional project reference to filter by IdentityReference project = 4; @@ -68,6 +69,18 @@ message APITokenServiceListRequest { // filter by the scope of the token Scope scope = 2; + + enum StatusFilter { + // Only active (non-revoked) tokens. This is the default. + STATUS_FILTER_ACTIVE = 0; + // Only revoked tokens. + STATUS_FILTER_REVOKED = 1; + // All tokens regardless of revocation status. + STATUS_FILTER_ALL = 2; + } + + // Filter tokens by their revocation status. Defaults to STATUS_FILTER_ACTIVE. + StatusFilter status_filter = 5; } message APITokenServiceListResponse { diff --git a/app/controlplane/api/controlplane/v1/api_token_grpc.pb.go b/app/controlplane/api/controlplane/v1/api_token_grpc.pb.go index a7791b48e..5600537b4 100644 --- a/app/controlplane/api/controlplane/v1/api_token_grpc.pb.go +++ b/app/controlplane/api/controlplane/v1/api_token_grpc.pb.go @@ -1,5 +1,5 @@ // -// Copyright 2024-2025 The Chainloop Authors. +// Copyright 2024-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/app/controlplane/api/gen/frontend/controlplane/v1/api_token.ts b/app/controlplane/api/gen/frontend/controlplane/v1/api_token.ts index 0fdbca493..2580ace40 100644 --- a/app/controlplane/api/gen/frontend/controlplane/v1/api_token.ts +++ b/app/controlplane/api/gen/frontend/controlplane/v1/api_token.ts @@ -35,11 +35,18 @@ export interface APITokenServiceRevokeResponse { } export interface APITokenServiceListRequest { + /** + * Deprecated: use status_filter instead. + * + * @deprecated + */ includeRevoked: boolean; /** optional project reference to filter by */ project?: IdentityReference; /** filter by the scope of the token */ scope: APITokenServiceListRequest_Scope; + /** Filter tokens by their revocation status. Defaults to STATUS_FILTER_ACTIVE. */ + statusFilter: APITokenServiceListRequest_StatusFilter; } export enum APITokenServiceListRequest_Scope { @@ -81,6 +88,48 @@ export function aPITokenServiceListRequest_ScopeToJSON(object: APITokenServiceLi } } +export enum APITokenServiceListRequest_StatusFilter { + /** STATUS_FILTER_ACTIVE - Only active (non-revoked) tokens. This is the default. */ + STATUS_FILTER_ACTIVE = 0, + /** STATUS_FILTER_REVOKED - Only revoked tokens. */ + STATUS_FILTER_REVOKED = 1, + /** STATUS_FILTER_ALL - All tokens regardless of revocation status. */ + STATUS_FILTER_ALL = 2, + UNRECOGNIZED = -1, +} + +export function aPITokenServiceListRequest_StatusFilterFromJSON(object: any): APITokenServiceListRequest_StatusFilter { + switch (object) { + case 0: + case "STATUS_FILTER_ACTIVE": + return APITokenServiceListRequest_StatusFilter.STATUS_FILTER_ACTIVE; + case 1: + case "STATUS_FILTER_REVOKED": + return APITokenServiceListRequest_StatusFilter.STATUS_FILTER_REVOKED; + case 2: + case "STATUS_FILTER_ALL": + return APITokenServiceListRequest_StatusFilter.STATUS_FILTER_ALL; + case -1: + case "UNRECOGNIZED": + default: + return APITokenServiceListRequest_StatusFilter.UNRECOGNIZED; + } +} + +export function aPITokenServiceListRequest_StatusFilterToJSON(object: APITokenServiceListRequest_StatusFilter): string { + switch (object) { + case APITokenServiceListRequest_StatusFilter.STATUS_FILTER_ACTIVE: + return "STATUS_FILTER_ACTIVE"; + case APITokenServiceListRequest_StatusFilter.STATUS_FILTER_REVOKED: + return "STATUS_FILTER_REVOKED"; + case APITokenServiceListRequest_StatusFilter.STATUS_FILTER_ALL: + return "STATUS_FILTER_ALL"; + case APITokenServiceListRequest_StatusFilter.UNRECOGNIZED: + default: + return "UNRECOGNIZED"; + } +} + export interface APITokenServiceListResponse { result: APITokenItem[]; } @@ -433,7 +482,7 @@ export const APITokenServiceRevokeResponse = { }; function createBaseAPITokenServiceListRequest(): APITokenServiceListRequest { - return { includeRevoked: false, project: undefined, scope: 0 }; + return { includeRevoked: false, project: undefined, scope: 0, statusFilter: 0 }; } export const APITokenServiceListRequest = { @@ -447,6 +496,9 @@ export const APITokenServiceListRequest = { if (message.scope !== 0) { writer.uint32(16).int32(message.scope); } + if (message.statusFilter !== 0) { + writer.uint32(40).int32(message.statusFilter); + } return writer; }, @@ -478,6 +530,13 @@ export const APITokenServiceListRequest = { message.scope = reader.int32() as any; continue; + case 5: + if (tag !== 40) { + break; + } + + message.statusFilter = reader.int32() as any; + continue; } if ((tag & 7) === 4 || tag === 0) { break; @@ -492,6 +551,9 @@ export const APITokenServiceListRequest = { includeRevoked: isSet(object.includeRevoked) ? Boolean(object.includeRevoked) : false, project: isSet(object.project) ? IdentityReference.fromJSON(object.project) : undefined, scope: isSet(object.scope) ? aPITokenServiceListRequest_ScopeFromJSON(object.scope) : 0, + statusFilter: isSet(object.statusFilter) + ? aPITokenServiceListRequest_StatusFilterFromJSON(object.statusFilter) + : 0, }; }, @@ -501,6 +563,8 @@ export const APITokenServiceListRequest = { message.project !== undefined && (obj.project = message.project ? IdentityReference.toJSON(message.project) : undefined); message.scope !== undefined && (obj.scope = aPITokenServiceListRequest_ScopeToJSON(message.scope)); + message.statusFilter !== undefined && + (obj.statusFilter = aPITokenServiceListRequest_StatusFilterToJSON(message.statusFilter)); return obj; }, @@ -515,6 +579,7 @@ export const APITokenServiceListRequest = { ? IdentityReference.fromPartial(object.project) : undefined; message.scope = object.scope ?? 0; + message.statusFilter = object.statusFilter ?? 0; return message; }, }; diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.APITokenServiceListRequest.jsonschema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.APITokenServiceListRequest.jsonschema.json index 984e5cd4b..711340e7e 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.APITokenServiceListRequest.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.APITokenServiceListRequest.jsonschema.json @@ -4,11 +4,32 @@ "additionalProperties": false, "patternProperties": { "^(include_revoked)$": { + "description": "Deprecated: use status_filter instead.", "type": "boolean" + }, + "^(status_filter)$": { + "anyOf": [ + { + "enum": [ + "STATUS_FILTER_ACTIVE", + "STATUS_FILTER_REVOKED", + "STATUS_FILTER_ALL" + ], + "title": "Status Filter", + "type": "string" + }, + { + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + ], + "description": "Filter tokens by their revocation status. Defaults to STATUS_FILTER_ACTIVE." } }, "properties": { "includeRevoked": { + "description": "Deprecated: use status_filter instead.", "type": "boolean" }, "project": { @@ -33,6 +54,25 @@ } ], "description": "filter by the scope of the token" + }, + "statusFilter": { + "anyOf": [ + { + "enum": [ + "STATUS_FILTER_ACTIVE", + "STATUS_FILTER_REVOKED", + "STATUS_FILTER_ALL" + ], + "title": "Status Filter", + "type": "string" + }, + { + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + ], + "description": "Filter tokens by their revocation status. Defaults to STATUS_FILTER_ACTIVE." } }, "title": "API Token Service List Request", diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.APITokenServiceListRequest.schema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.APITokenServiceListRequest.schema.json index d6bfc4712..d4b6d4103 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.APITokenServiceListRequest.schema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.APITokenServiceListRequest.schema.json @@ -4,11 +4,32 @@ "additionalProperties": false, "patternProperties": { "^(includeRevoked)$": { + "description": "Deprecated: use status_filter instead.", "type": "boolean" + }, + "^(statusFilter)$": { + "anyOf": [ + { + "enum": [ + "STATUS_FILTER_ACTIVE", + "STATUS_FILTER_REVOKED", + "STATUS_FILTER_ALL" + ], + "title": "Status Filter", + "type": "string" + }, + { + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + ], + "description": "Filter tokens by their revocation status. Defaults to STATUS_FILTER_ACTIVE." } }, "properties": { "include_revoked": { + "description": "Deprecated: use status_filter instead.", "type": "boolean" }, "project": { @@ -33,6 +54,25 @@ } ], "description": "filter by the scope of the token" + }, + "status_filter": { + "anyOf": [ + { + "enum": [ + "STATUS_FILTER_ACTIVE", + "STATUS_FILTER_REVOKED", + "STATUS_FILTER_ALL" + ], + "title": "Status Filter", + "type": "string" + }, + { + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + ], + "description": "Filter tokens by their revocation status. Defaults to STATUS_FILTER_ACTIVE." } }, "title": "API Token Service List Request", diff --git a/app/controlplane/internal/service/apitoken.go b/app/controlplane/internal/service/apitoken.go index c81628ca6..a0a41bb0f 100644 --- a/app/controlplane/internal/service/apitoken.go +++ b/app/controlplane/internal/service/apitoken.go @@ -1,5 +1,5 @@ // -// Copyright 2023-2025 The Chainloop Authors. +// Copyright 2023-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -100,8 +100,7 @@ func (s *APITokenService) List(ctx context.Context, req *pb.APITokenServiceListR defaultProjectFilter = []uuid.UUID{project.ID} } - // Only expose system tokens - tokens, err := s.APITokenUseCase.List(ctx, currentOrg.ID, biz.WithAPITokenRevoked(req.IncludeRevoked), biz.WithAPITokenProjectFilter(defaultProjectFilter), biz.WithAPITokenScope(mapTokenScope(req.Scope))) + tokens, err := s.APITokenUseCase.List(ctx, currentOrg.ID, biz.WithAPITokenStatusFilter(mapTokenStatusFilter(req.GetStatusFilter())), biz.WithAPITokenProjectFilter(defaultProjectFilter), biz.WithAPITokenScope(mapTokenScope(req.Scope))) if err != nil { return nil, handleUseCaseErr(err, s.log) } @@ -125,6 +124,17 @@ func mapTokenScope(scope pb.APITokenServiceListRequest_Scope) biz.APITokenScope return "" } +func mapTokenStatusFilter(f pb.APITokenServiceListRequest_StatusFilter) biz.APITokenStatusFilter { + switch f { + case pb.APITokenServiceListRequest_STATUS_FILTER_REVOKED: + return biz.APITokenStatusFilterRevoked + case pb.APITokenServiceListRequest_STATUS_FILTER_ALL: + return biz.APITokenStatusFilterAll + default: + return biz.APITokenStatusFilterActive + } +} + func (s *APITokenService) Revoke(ctx context.Context, req *pb.APITokenServiceRevokeRequest) (*pb.APITokenServiceRevokeResponse, error) { currentOrg, err := requireCurrentOrg(ctx) if err != nil { diff --git a/app/controlplane/pkg/biz/apitoken.go b/app/controlplane/pkg/biz/apitoken.go index 3f737f08d..92305f632 100644 --- a/app/controlplane/pkg/biz/apitoken.go +++ b/app/controlplane/pkg/biz/apitoken.go @@ -1,5 +1,5 @@ // -// Copyright 2024-2025 The Chainloop Authors. +// Copyright 2024-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -304,9 +304,9 @@ func WithAPITokenProjectFilter(projectIDs []uuid.UUID) APITokenListOpt { } } -func WithAPITokenRevoked(includeRevoked bool) APITokenListOpt { +func WithAPITokenStatusFilter(filter APITokenStatusFilter) APITokenListOpt { return func(opts *APITokenListFilters) { - opts.IncludeRevoked = includeRevoked + opts.StatusFilter = filter } } @@ -330,12 +330,25 @@ var availableAPITokenScopes = []APITokenScope{ APITokenScopeInstance, } +// APITokenStatusFilter controls which tokens are returned based on their revocation status. +type APITokenStatusFilter int + +const ( + // APITokenStatusFilterActive returns only active (non-revoked) tokens. This is the default. + APITokenStatusFilterActive APITokenStatusFilter = iota + // APITokenStatusFilterRevoked returns only revoked tokens. + APITokenStatusFilterRevoked + // APITokenStatusFilterAll returns all tokens regardless of revocation status. + APITokenStatusFilterAll +) + type APITokenListFilters struct { // FilterByProjects is used to filter the result by a project list // If it's empty, no filter will be applied FilterByProjects []uuid.UUID - // IncludeRevoked is used to include revoked tokens in the result - IncludeRevoked bool + // StatusFilter controls which tokens are returned based on revocation status. + // Defaults to APITokenStatusFilterActive. + StatusFilter APITokenStatusFilter // FilterByScope is used to filter the result by the scope of the token FilterByScope APITokenScope } diff --git a/app/controlplane/pkg/biz/apitoken_integration_test.go b/app/controlplane/pkg/biz/apitoken_integration_test.go index d346446f8..2175188ad 100644 --- a/app/controlplane/pkg/biz/apitoken_integration_test.go +++ b/app/controlplane/pkg/biz/apitoken_integration_test.go @@ -1,5 +1,5 @@ // -// Copyright 2024-2025 The Chainloop Authors. +// Copyright 2024-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -190,7 +190,7 @@ func (s *apiTokenTestSuite) TestRevoke() { s.Run("token can be revoked once", func() { err := s.APIToken.Revoke(ctx, s.org.ID, s.t1.ID.String()) s.NoError(err) - tokens, err := s.APIToken.List(ctx, s.org.ID, biz.WithAPITokenRevoked(true)) + tokens, err := s.APIToken.List(ctx, s.org.ID, biz.WithAPITokenStatusFilter(biz.APITokenStatusFilterAll)) s.NoError(err) s.Equal(s.t1.ID, tokens[0].ID) // It's revoked @@ -298,13 +298,30 @@ func (s *apiTokenTestSuite) TestList() { s.Equal(s.t2.ID, tokens[0].ID) }) - s.Run("doesn't return revoked unless requested", func() { - tokens, err := s.APIToken.List(ctx, s.org.ID, biz.WithAPITokenRevoked(true)) + s.Run("status filter all returns revoked and active tokens", func() { + tokens, err := s.APIToken.List(ctx, s.org.ID, biz.WithAPITokenStatusFilter(biz.APITokenStatusFilterAll)) s.NoError(err) require.Len(s.T(), tokens, 5) s.Equal(s.t1.ID, tokens[0].ID) s.Equal(s.t2.ID, tokens[1].ID) }) + + s.Run("status filter active returns only active tokens", func() { + tokens, err := s.APIToken.List(ctx, s.org.ID, biz.WithAPITokenStatusFilter(biz.APITokenStatusFilterActive)) + s.NoError(err) + require.Len(s.T(), tokens, 4) + for _, t := range tokens { + s.Nil(t.RevokedAt) + } + }) + + s.Run("status filter revoked returns only revoked tokens", func() { + tokens, err := s.APIToken.List(ctx, s.org.ID, biz.WithAPITokenStatusFilter(biz.APITokenStatusFilterRevoked)) + s.NoError(err) + require.Len(s.T(), tokens, 1) + s.Equal(s.t1.ID, tokens[0].ID) + s.NotNil(tokens[0].RevokedAt) + }) } func (s *apiTokenTestSuite) TestGeneratedJWT() { diff --git a/app/controlplane/pkg/data/apitoken.go b/app/controlplane/pkg/data/apitoken.go index 2f9a97699..26e697907 100644 --- a/app/controlplane/pkg/data/apitoken.go +++ b/app/controlplane/pkg/data/apitoken.go @@ -1,5 +1,5 @@ // -// Copyright 2023-2025 The Chainloop Authors. +// Copyright 2023-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -123,7 +123,12 @@ func (r *APITokenRepo) List(ctx context.Context, orgID *uuid.UUID, filters *biz. query = query.Where(apitoken.OrganizationIDIsNil()) } - if !filters.IncludeRevoked { + switch filters.StatusFilter { + case biz.APITokenStatusFilterRevoked: + query = query.Where(apitoken.RevokedAtNotNil()) + case biz.APITokenStatusFilterAll: + // no filter — return all tokens + default: // APITokenStatusFilterActive query = query.Where(apitoken.RevokedAtIsNil()) } From fbd1b691e836d8aa36ccfade7d1e3d098eaf33e7 Mon Sep 17 00:00:00 2001 From: Miguel Martinez Date: Tue, 17 Feb 2026 11:33:00 +0100 Subject: [PATCH 2/3] fix(api-tokens): suppress ENUM_ZERO_VALUE_SUFFIX lint for StatusFilter STATUS_FILTER_ACTIVE = 0 is intentional: active tokens are the safe default, matching proto3 zero-value semantics and preserving backward compatibility with existing callers. Signed-off-by: Miguel Martinez --- app/controlplane/api/controlplane/v1/api_token.proto | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/controlplane/api/controlplane/v1/api_token.proto b/app/controlplane/api/controlplane/v1/api_token.proto index 729487974..b73d39240 100644 --- a/app/controlplane/api/controlplane/v1/api_token.proto +++ b/app/controlplane/api/controlplane/v1/api_token.proto @@ -70,6 +70,9 @@ message APITokenServiceListRequest { // filter by the scope of the token Scope scope = 2; + // buf:lint:ignore ENUM_ZERO_VALUE_SUFFIX + // STATUS_FILTER_ACTIVE = 0 is intentional: active tokens are the default, + // matching proto3 zero-value semantics and preserving backward compatibility. enum StatusFilter { // Only active (non-revoked) tokens. This is the default. STATUS_FILTER_ACTIVE = 0; From c4e94c3c4d2251da4c40bbe62b0c4a762d284901 Mon Sep 17 00:00:00 2001 From: Miguel Martinez Date: Tue, 17 Feb 2026 11:41:29 +0100 Subject: [PATCH 3/3] chore: regenerate code after proto comment update Signed-off-by: Miguel Martinez --- app/cli/documentation/cli-reference.mdx | 2 +- app/controlplane/api/controlplane/v1/api_token.pb.go | 3 +++ .../api/gen/frontend/controlplane/v1/api_token.ts | 5 +++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/cli/documentation/cli-reference.mdx b/app/cli/documentation/cli-reference.mdx index 591785762..5daf3de01 100755 --- a/app/cli/documentation/cli-reference.mdx +++ b/app/cli/documentation/cli-reference.mdx @@ -2139,10 +2139,10 @@ chainloop organization api-token list [flags] Options ``` --a, --all show all API tokens including revoked ones -h, --help help for list -p, --project string filter by project name -s, --scope string filter by scope, available scopes: [project global] +--status string filter by token status, available values: [active revoked all] ``` Options inherited from parent commands diff --git a/app/controlplane/api/controlplane/v1/api_token.pb.go b/app/controlplane/api/controlplane/v1/api_token.pb.go index e5f82494f..9d5d62026 100644 --- a/app/controlplane/api/controlplane/v1/api_token.pb.go +++ b/app/controlplane/api/controlplane/v1/api_token.pb.go @@ -87,6 +87,9 @@ func (APITokenServiceListRequest_Scope) EnumDescriptor() ([]byte, []int) { return file_controlplane_v1_api_token_proto_rawDescGZIP(), []int{4, 0} } +// buf:lint:ignore ENUM_ZERO_VALUE_SUFFIX +// STATUS_FILTER_ACTIVE = 0 is intentional: active tokens are the default, +// matching proto3 zero-value semantics and preserving backward compatibility. type APITokenServiceListRequest_StatusFilter int32 const ( diff --git a/app/controlplane/api/gen/frontend/controlplane/v1/api_token.ts b/app/controlplane/api/gen/frontend/controlplane/v1/api_token.ts index 2580ace40..1aab5c571 100644 --- a/app/controlplane/api/gen/frontend/controlplane/v1/api_token.ts +++ b/app/controlplane/api/gen/frontend/controlplane/v1/api_token.ts @@ -88,6 +88,11 @@ export function aPITokenServiceListRequest_ScopeToJSON(object: APITokenServiceLi } } +/** + * buf:lint:ignore ENUM_ZERO_VALUE_SUFFIX + * STATUS_FILTER_ACTIVE = 0 is intentional: active tokens are the default, + * matching proto3 zero-value semantics and preserving backward compatibility. + */ export enum APITokenServiceListRequest_StatusFilter { /** STATUS_FILTER_ACTIVE - Only active (non-revoked) tokens. This is the default. */ STATUS_FILTER_ACTIVE = 0,