From be9cbc337ca1c34b6c2ae6fde1549a48fb67538e Mon Sep 17 00:00:00 2001 From: Miguel Martinez Trivino Date: Tue, 12 Dec 2023 13:11:17 +0100 Subject: [PATCH 1/8] feat: API token support Signed-off-by: Miguel Martinez Trivino --- .../api/controlplane/v1/api_token.pb.go | 729 +++++++++++ .../controlplane/v1/api_token.pb.validate.go | 1107 +++++++++++++++++ .../api/controlplane/v1/api_token.proto | 67 + .../api/controlplane/v1/api_token_grpc.pb.go | 198 +++ .../gen/frontend/controlplane/v1/api_token.ts | 854 +++++++++++++ app/controlplane/cmd/wire_gen.go | 60 +- app/controlplane/internal/biz/apitoken.go | 12 +- .../internal/biz/apitoken_integration_test.go | 7 +- app/controlplane/internal/data/apitoken.go | 11 + .../internal/jwt/apitoken/apitoken.go | 20 +- .../internal/jwt/apitoken/apitoken_test.go | 7 +- app/controlplane/internal/server/grpc.go | 24 +- app/controlplane/internal/service/apitoken.go | 137 ++ app/controlplane/internal/service/auth.go | 2 +- .../internal/service/casbackend.go | 8 +- .../internal/service/cascredential.go | 8 +- .../internal/service/casredirect.go | 3 +- app/controlplane/internal/service/context.go | 7 +- .../internal/service/integration.go | 14 +- .../internal/service/organization.go | 10 +- .../internal/service/orginvitation.go | 6 +- .../internal/service/orgmetric.go | 4 +- app/controlplane/internal/service/referrer.go | 2 +- .../internal/service/robotaccount.go | 6 +- app/controlplane/internal/service/service.go | 20 +- app/controlplane/internal/service/workflow.go | 8 +- .../internal/service/workflowcontract.go | 10 +- .../internal/service/workflowrun.go | 4 +- .../usercontext/apitoken_middleware.go | 108 ++ ...iddleware.go => currentuser_middleware.go} | 37 +- ...test.go => currentuser_middleware_test.go} | 0 31 files changed, 3370 insertions(+), 120 deletions(-) create mode 100644 app/controlplane/api/controlplane/v1/api_token.pb.go create mode 100644 app/controlplane/api/controlplane/v1/api_token.pb.validate.go create mode 100644 app/controlplane/api/controlplane/v1/api_token.proto create mode 100644 app/controlplane/api/controlplane/v1/api_token_grpc.pb.go create mode 100644 app/controlplane/api/gen/frontend/controlplane/v1/api_token.ts create mode 100644 app/controlplane/internal/service/apitoken.go create mode 100644 app/controlplane/internal/usercontext/apitoken_middleware.go rename app/controlplane/internal/usercontext/{userorg_middleware.go => currentuser_middleware.go} (78%) rename app/controlplane/internal/usercontext/{userorg_middleware_test.go => currentuser_middleware_test.go} (100%) diff --git a/app/controlplane/api/controlplane/v1/api_token.pb.go b/app/controlplane/api/controlplane/v1/api_token.pb.go new file mode 100644 index 000000000..8cdd4f170 --- /dev/null +++ b/app/controlplane/api/controlplane/v1/api_token.pb.go @@ -0,0 +1,729 @@ +// +// Copyright 2023 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. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.31.0 +// protoc (unknown) +// source: controlplane/v1/api_token.proto + +package v1 + +import ( + _ "github.com/envoyproxy/protoc-gen-validate/validate" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + durationpb "google.golang.org/protobuf/types/known/durationpb" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type APITokenServiceCreateRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Description *string `protobuf:"bytes,1,opt,name=description,proto3,oneof" json:"description,omitempty"` + ExpiresIn *durationpb.Duration `protobuf:"bytes,2,opt,name=expires_in,json=expiresIn,proto3,oneof" json:"expires_in,omitempty"` +} + +func (x *APITokenServiceCreateRequest) Reset() { + *x = APITokenServiceCreateRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_controlplane_v1_api_token_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *APITokenServiceCreateRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*APITokenServiceCreateRequest) ProtoMessage() {} + +func (x *APITokenServiceCreateRequest) ProtoReflect() protoreflect.Message { + mi := &file_controlplane_v1_api_token_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use APITokenServiceCreateRequest.ProtoReflect.Descriptor instead. +func (*APITokenServiceCreateRequest) Descriptor() ([]byte, []int) { + return file_controlplane_v1_api_token_proto_rawDescGZIP(), []int{0} +} + +func (x *APITokenServiceCreateRequest) GetDescription() string { + if x != nil && x.Description != nil { + return *x.Description + } + return "" +} + +func (x *APITokenServiceCreateRequest) GetExpiresIn() *durationpb.Duration { + if x != nil { + return x.ExpiresIn + } + return nil +} + +type APITokenServiceCreateResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Result *APITokenServiceCreateResponse_APITokenFull `protobuf:"bytes,1,opt,name=result,proto3" json:"result,omitempty"` +} + +func (x *APITokenServiceCreateResponse) Reset() { + *x = APITokenServiceCreateResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_controlplane_v1_api_token_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *APITokenServiceCreateResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*APITokenServiceCreateResponse) ProtoMessage() {} + +func (x *APITokenServiceCreateResponse) ProtoReflect() protoreflect.Message { + mi := &file_controlplane_v1_api_token_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use APITokenServiceCreateResponse.ProtoReflect.Descriptor instead. +func (*APITokenServiceCreateResponse) Descriptor() ([]byte, []int) { + return file_controlplane_v1_api_token_proto_rawDescGZIP(), []int{1} +} + +func (x *APITokenServiceCreateResponse) GetResult() *APITokenServiceCreateResponse_APITokenFull { + if x != nil { + return x.Result + } + return nil +} + +type APITokenServiceRevokeRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` +} + +func (x *APITokenServiceRevokeRequest) Reset() { + *x = APITokenServiceRevokeRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_controlplane_v1_api_token_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *APITokenServiceRevokeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*APITokenServiceRevokeRequest) ProtoMessage() {} + +func (x *APITokenServiceRevokeRequest) ProtoReflect() protoreflect.Message { + mi := &file_controlplane_v1_api_token_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use APITokenServiceRevokeRequest.ProtoReflect.Descriptor instead. +func (*APITokenServiceRevokeRequest) Descriptor() ([]byte, []int) { + return file_controlplane_v1_api_token_proto_rawDescGZIP(), []int{2} +} + +func (x *APITokenServiceRevokeRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +type APITokenServiceRevokeResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *APITokenServiceRevokeResponse) Reset() { + *x = APITokenServiceRevokeResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_controlplane_v1_api_token_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *APITokenServiceRevokeResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*APITokenServiceRevokeResponse) ProtoMessage() {} + +func (x *APITokenServiceRevokeResponse) ProtoReflect() protoreflect.Message { + mi := &file_controlplane_v1_api_token_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use APITokenServiceRevokeResponse.ProtoReflect.Descriptor instead. +func (*APITokenServiceRevokeResponse) Descriptor() ([]byte, []int) { + return file_controlplane_v1_api_token_proto_rawDescGZIP(), []int{3} +} + +type APITokenServiceListRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + IncludeRevoked bool `protobuf:"varint,1,opt,name=include_revoked,json=includeRevoked,proto3" json:"include_revoked,omitempty"` +} + +func (x *APITokenServiceListRequest) Reset() { + *x = APITokenServiceListRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_controlplane_v1_api_token_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *APITokenServiceListRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*APITokenServiceListRequest) ProtoMessage() {} + +func (x *APITokenServiceListRequest) ProtoReflect() protoreflect.Message { + mi := &file_controlplane_v1_api_token_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use APITokenServiceListRequest.ProtoReflect.Descriptor instead. +func (*APITokenServiceListRequest) Descriptor() ([]byte, []int) { + return file_controlplane_v1_api_token_proto_rawDescGZIP(), []int{4} +} + +func (x *APITokenServiceListRequest) GetIncludeRevoked() bool { + if x != nil { + return x.IncludeRevoked + } + return false +} + +type APITokenServiceListResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Result []*APITokenItem `protobuf:"bytes,1,rep,name=result,proto3" json:"result,omitempty"` +} + +func (x *APITokenServiceListResponse) Reset() { + *x = APITokenServiceListResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_controlplane_v1_api_token_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *APITokenServiceListResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*APITokenServiceListResponse) ProtoMessage() {} + +func (x *APITokenServiceListResponse) ProtoReflect() protoreflect.Message { + mi := &file_controlplane_v1_api_token_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use APITokenServiceListResponse.ProtoReflect.Descriptor instead. +func (*APITokenServiceListResponse) Descriptor() ([]byte, []int) { + return file_controlplane_v1_api_token_proto_rawDescGZIP(), []int{5} +} + +func (x *APITokenServiceListResponse) GetResult() []*APITokenItem { + if x != nil { + return x.Result + } + return nil +} + +type APITokenItem struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` + OrganizationId string `protobuf:"bytes,3,opt,name=organization_id,json=organizationId,proto3" json:"organization_id,omitempty"` + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + RevokedAt *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=revoked_at,json=revokedAt,proto3" json:"revoked_at,omitempty"` + ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"` +} + +func (x *APITokenItem) Reset() { + *x = APITokenItem{} + if protoimpl.UnsafeEnabled { + mi := &file_controlplane_v1_api_token_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *APITokenItem) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*APITokenItem) ProtoMessage() {} + +func (x *APITokenItem) ProtoReflect() protoreflect.Message { + mi := &file_controlplane_v1_api_token_proto_msgTypes[6] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use APITokenItem.ProtoReflect.Descriptor instead. +func (*APITokenItem) Descriptor() ([]byte, []int) { + return file_controlplane_v1_api_token_proto_rawDescGZIP(), []int{6} +} + +func (x *APITokenItem) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *APITokenItem) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *APITokenItem) GetOrganizationId() string { + if x != nil { + return x.OrganizationId + } + return "" +} + +func (x *APITokenItem) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +func (x *APITokenItem) GetRevokedAt() *timestamppb.Timestamp { + if x != nil { + return x.RevokedAt + } + return nil +} + +func (x *APITokenItem) GetExpiresAt() *timestamppb.Timestamp { + if x != nil { + return x.ExpiresAt + } + return nil +} + +type APITokenServiceCreateResponse_APITokenFull struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Item *APITokenItem `protobuf:"bytes,1,opt,name=item,proto3" json:"item,omitempty"` + Jwt string `protobuf:"bytes,2,opt,name=jwt,proto3" json:"jwt,omitempty"` +} + +func (x *APITokenServiceCreateResponse_APITokenFull) Reset() { + *x = APITokenServiceCreateResponse_APITokenFull{} + if protoimpl.UnsafeEnabled { + mi := &file_controlplane_v1_api_token_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *APITokenServiceCreateResponse_APITokenFull) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*APITokenServiceCreateResponse_APITokenFull) ProtoMessage() {} + +func (x *APITokenServiceCreateResponse_APITokenFull) ProtoReflect() protoreflect.Message { + mi := &file_controlplane_v1_api_token_proto_msgTypes[7] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use APITokenServiceCreateResponse_APITokenFull.ProtoReflect.Descriptor instead. +func (*APITokenServiceCreateResponse_APITokenFull) Descriptor() ([]byte, []int) { + return file_controlplane_v1_api_token_proto_rawDescGZIP(), []int{1, 0} +} + +func (x *APITokenServiceCreateResponse_APITokenFull) GetItem() *APITokenItem { + if x != nil { + return x.Item + } + return nil +} + +func (x *APITokenServiceCreateResponse_APITokenFull) GetJwt() string { + if x != nil { + return x.Jwt + } + return "" +} + +var File_controlplane_v1_api_token_proto protoreflect.FileDescriptor + +var file_controlplane_v1_api_token_proto_rawDesc = []byte{ + 0x0a, 0x1f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2f, 0x76, + 0x31, 0x2f, 0x61, 0x70, 0x69, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x12, 0x0f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, + 0x76, 0x31, 0x1a, 0x17, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x61, 0x6c, + 0x69, 0x64, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x75, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xa3, 0x01, 0x0a, + 0x1c, 0x41, 0x50, 0x49, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x25, 0x0a, + 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x48, 0x00, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, + 0x6e, 0x88, 0x01, 0x01, 0x12, 0x3d, 0x0a, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x5f, + 0x69, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x48, 0x01, 0x52, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x49, 0x6e, + 0x88, 0x01, 0x01, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x42, 0x0d, 0x0a, 0x0b, 0x5f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x5f, + 0x69, 0x6e, 0x22, 0xc9, 0x01, 0x0a, 0x1d, 0x41, 0x50, 0x49, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x53, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x53, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3b, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, + 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x50, 0x49, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x53, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x41, 0x50, 0x49, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x46, 0x75, 0x6c, + 0x6c, 0x52, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x1a, 0x53, 0x0a, 0x0c, 0x41, 0x50, 0x49, + 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x46, 0x75, 0x6c, 0x6c, 0x12, 0x31, 0x0a, 0x04, 0x69, 0x74, 0x65, + 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, + 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x50, 0x49, 0x54, 0x6f, 0x6b, + 0x65, 0x6e, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x12, 0x10, 0x0a, 0x03, + 0x6a, 0x77, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6a, 0x77, 0x74, 0x22, 0x38, + 0x0a, 0x1c, 0x41, 0x50, 0x49, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, + 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xfa, 0x42, 0x05, 0x72, + 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, 0x64, 0x22, 0x1f, 0x0a, 0x1d, 0x41, 0x50, 0x49, 0x54, + 0x6f, 0x6b, 0x65, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x76, 0x6f, 0x6b, + 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x45, 0x0a, 0x1a, 0x41, 0x50, 0x49, + 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x4c, 0x69, 0x73, 0x74, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x69, 0x6e, 0x63, 0x6c, 0x75, + 0x64, 0x65, 0x5f, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x0e, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, + 0x22, 0x54, 0x0a, 0x1b, 0x41, 0x50, 0x49, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x53, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x35, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x1d, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, + 0x31, 0x2e, 0x41, 0x50, 0x49, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x06, + 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0x9a, 0x02, 0x0a, 0x0c, 0x41, 0x50, 0x49, 0x54, 0x6f, + 0x6b, 0x65, 0x6e, 0x49, 0x74, 0x65, 0x6d, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, + 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, + 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x27, 0x0a, 0x0f, 0x6f, 0x72, 0x67, + 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0e, 0x6f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x49, 0x64, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, + 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x39, 0x0a, + 0x0a, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x72, + 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x41, 0x74, 0x12, 0x39, 0x0a, 0x0a, 0x65, 0x78, 0x70, 0x69, + 0x72, 0x65, 0x73, 0x5f, 0x61, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, + 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, + 0x73, 0x41, 0x74, 0x32, 0xc6, 0x02, 0x0a, 0x0f, 0x41, 0x50, 0x49, 0x54, 0x6f, 0x6b, 0x65, 0x6e, + 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x67, 0x0a, 0x06, 0x43, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x12, 0x2d, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, + 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x50, 0x49, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x53, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x2e, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, + 0x76, 0x31, 0x2e, 0x41, 0x50, 0x49, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x61, 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x2b, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, + 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x50, 0x49, 0x54, 0x6f, + 0x6b, 0x65, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, + 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x50, 0x49, 0x54, 0x6f, 0x6b, 0x65, 0x6e, + 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x67, 0x0a, 0x06, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x12, 0x2d, 0x2e, + 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, + 0x41, 0x50, 0x49, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, + 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x63, + 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x41, + 0x50, 0x49, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, + 0x76, 0x6f, 0x6b, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x4c, 0x5a, 0x4a, + 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x68, 0x61, 0x69, 0x6e, + 0x6c, 0x6f, 0x6f, 0x70, 0x2d, 0x64, 0x65, 0x76, 0x2f, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x6c, 0x6f, + 0x6f, 0x70, 0x2f, 0x61, 0x70, 0x70, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, + 0x61, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, + 0x6c, 0x61, 0x6e, 0x65, 0x2f, 0x76, 0x31, 0x3b, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, +} + +var ( + file_controlplane_v1_api_token_proto_rawDescOnce sync.Once + file_controlplane_v1_api_token_proto_rawDescData = file_controlplane_v1_api_token_proto_rawDesc +) + +func file_controlplane_v1_api_token_proto_rawDescGZIP() []byte { + file_controlplane_v1_api_token_proto_rawDescOnce.Do(func() { + file_controlplane_v1_api_token_proto_rawDescData = protoimpl.X.CompressGZIP(file_controlplane_v1_api_token_proto_rawDescData) + }) + return file_controlplane_v1_api_token_proto_rawDescData +} + +var file_controlplane_v1_api_token_proto_msgTypes = make([]protoimpl.MessageInfo, 8) +var file_controlplane_v1_api_token_proto_goTypes = []interface{}{ + (*APITokenServiceCreateRequest)(nil), // 0: controlplane.v1.APITokenServiceCreateRequest + (*APITokenServiceCreateResponse)(nil), // 1: controlplane.v1.APITokenServiceCreateResponse + (*APITokenServiceRevokeRequest)(nil), // 2: controlplane.v1.APITokenServiceRevokeRequest + (*APITokenServiceRevokeResponse)(nil), // 3: controlplane.v1.APITokenServiceRevokeResponse + (*APITokenServiceListRequest)(nil), // 4: controlplane.v1.APITokenServiceListRequest + (*APITokenServiceListResponse)(nil), // 5: controlplane.v1.APITokenServiceListResponse + (*APITokenItem)(nil), // 6: controlplane.v1.APITokenItem + (*APITokenServiceCreateResponse_APITokenFull)(nil), // 7: controlplane.v1.APITokenServiceCreateResponse.APITokenFull + (*durationpb.Duration)(nil), // 8: google.protobuf.Duration + (*timestamppb.Timestamp)(nil), // 9: google.protobuf.Timestamp +} +var file_controlplane_v1_api_token_proto_depIdxs = []int32{ + 8, // 0: controlplane.v1.APITokenServiceCreateRequest.expires_in:type_name -> google.protobuf.Duration + 7, // 1: controlplane.v1.APITokenServiceCreateResponse.result:type_name -> controlplane.v1.APITokenServiceCreateResponse.APITokenFull + 6, // 2: controlplane.v1.APITokenServiceListResponse.result:type_name -> controlplane.v1.APITokenItem + 9, // 3: controlplane.v1.APITokenItem.created_at:type_name -> google.protobuf.Timestamp + 9, // 4: controlplane.v1.APITokenItem.revoked_at:type_name -> google.protobuf.Timestamp + 9, // 5: controlplane.v1.APITokenItem.expires_at:type_name -> google.protobuf.Timestamp + 6, // 6: controlplane.v1.APITokenServiceCreateResponse.APITokenFull.item:type_name -> controlplane.v1.APITokenItem + 0, // 7: controlplane.v1.APITokenService.Create:input_type -> controlplane.v1.APITokenServiceCreateRequest + 4, // 8: controlplane.v1.APITokenService.List:input_type -> controlplane.v1.APITokenServiceListRequest + 2, // 9: controlplane.v1.APITokenService.Revoke:input_type -> controlplane.v1.APITokenServiceRevokeRequest + 1, // 10: controlplane.v1.APITokenService.Create:output_type -> controlplane.v1.APITokenServiceCreateResponse + 5, // 11: controlplane.v1.APITokenService.List:output_type -> controlplane.v1.APITokenServiceListResponse + 3, // 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 +} + +func init() { file_controlplane_v1_api_token_proto_init() } +func file_controlplane_v1_api_token_proto_init() { + if File_controlplane_v1_api_token_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_controlplane_v1_api_token_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*APITokenServiceCreateRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controlplane_v1_api_token_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*APITokenServiceCreateResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controlplane_v1_api_token_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*APITokenServiceRevokeRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controlplane_v1_api_token_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*APITokenServiceRevokeResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controlplane_v1_api_token_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*APITokenServiceListRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controlplane_v1_api_token_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*APITokenServiceListResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controlplane_v1_api_token_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*APITokenItem); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controlplane_v1_api_token_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*APITokenServiceCreateResponse_APITokenFull); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + file_controlplane_v1_api_token_proto_msgTypes[0].OneofWrappers = []interface{}{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_controlplane_v1_api_token_proto_rawDesc, + NumEnums: 0, + NumMessages: 8, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_controlplane_v1_api_token_proto_goTypes, + DependencyIndexes: file_controlplane_v1_api_token_proto_depIdxs, + MessageInfos: file_controlplane_v1_api_token_proto_msgTypes, + }.Build() + File_controlplane_v1_api_token_proto = out.File + file_controlplane_v1_api_token_proto_rawDesc = nil + file_controlplane_v1_api_token_proto_goTypes = nil + file_controlplane_v1_api_token_proto_depIdxs = nil +} diff --git a/app/controlplane/api/controlplane/v1/api_token.pb.validate.go b/app/controlplane/api/controlplane/v1/api_token.pb.validate.go new file mode 100644 index 000000000..179b2effb --- /dev/null +++ b/app/controlplane/api/controlplane/v1/api_token.pb.validate.go @@ -0,0 +1,1107 @@ +// Code generated by protoc-gen-validate. DO NOT EDIT. +// source: controlplane/v1/api_token.proto + +package v1 + +import ( + "bytes" + "errors" + "fmt" + "net" + "net/mail" + "net/url" + "regexp" + "sort" + "strings" + "time" + "unicode/utf8" + + "google.golang.org/protobuf/types/known/anypb" +) + +// ensure the imports are used +var ( + _ = bytes.MinRead + _ = errors.New("") + _ = fmt.Print + _ = utf8.UTFMax + _ = (*regexp.Regexp)(nil) + _ = (*strings.Reader)(nil) + _ = net.IPv4len + _ = time.Duration(0) + _ = (*url.URL)(nil) + _ = (*mail.Address)(nil) + _ = anypb.Any{} + _ = sort.Sort +) + +// define the regex for a UUID once up-front +var _api_token_uuidPattern = regexp.MustCompile("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$") + +// Validate checks the field values on APITokenServiceCreateRequest with the +// rules defined in the proto definition for this message. If any rules are +// violated, the first error encountered is returned, or nil if there are no violations. +func (m *APITokenServiceCreateRequest) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on APITokenServiceCreateRequest with the +// rules defined in the proto definition for this message. If any rules are +// violated, the result is a list of violation errors wrapped in +// APITokenServiceCreateRequestMultiError, or nil if none found. +func (m *APITokenServiceCreateRequest) ValidateAll() error { + return m.validate(true) +} + +func (m *APITokenServiceCreateRequest) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + if m.Description != nil { + // no validation rules for Description + } + + if m.ExpiresIn != nil { + + if all { + switch v := interface{}(m.GetExpiresIn()).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, APITokenServiceCreateRequestValidationError{ + field: "ExpiresIn", + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, APITokenServiceCreateRequestValidationError{ + field: "ExpiresIn", + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(m.GetExpiresIn()).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return APITokenServiceCreateRequestValidationError{ + field: "ExpiresIn", + reason: "embedded message failed validation", + cause: err, + } + } + } + + } + + if len(errors) > 0 { + return APITokenServiceCreateRequestMultiError(errors) + } + + return nil +} + +// APITokenServiceCreateRequestMultiError is an error wrapping multiple +// validation errors returned by APITokenServiceCreateRequest.ValidateAll() if +// the designated constraints aren't met. +type APITokenServiceCreateRequestMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m APITokenServiceCreateRequestMultiError) Error() string { + var msgs []string + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m APITokenServiceCreateRequestMultiError) AllErrors() []error { return m } + +// APITokenServiceCreateRequestValidationError is the validation error returned +// by APITokenServiceCreateRequest.Validate if the designated constraints +// aren't met. +type APITokenServiceCreateRequestValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e APITokenServiceCreateRequestValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e APITokenServiceCreateRequestValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e APITokenServiceCreateRequestValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e APITokenServiceCreateRequestValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e APITokenServiceCreateRequestValidationError) ErrorName() string { + return "APITokenServiceCreateRequestValidationError" +} + +// Error satisfies the builtin error interface +func (e APITokenServiceCreateRequestValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sAPITokenServiceCreateRequest.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = APITokenServiceCreateRequestValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = APITokenServiceCreateRequestValidationError{} + +// Validate checks the field values on APITokenServiceCreateResponse with the +// rules defined in the proto definition for this message. If any rules are +// violated, the first error encountered is returned, or nil if there are no violations. +func (m *APITokenServiceCreateResponse) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on APITokenServiceCreateResponse with +// the rules defined in the proto definition for this message. If any rules +// are violated, the result is a list of violation errors wrapped in +// APITokenServiceCreateResponseMultiError, or nil if none found. +func (m *APITokenServiceCreateResponse) ValidateAll() error { + return m.validate(true) +} + +func (m *APITokenServiceCreateResponse) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + if all { + switch v := interface{}(m.GetResult()).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, APITokenServiceCreateResponseValidationError{ + field: "Result", + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, APITokenServiceCreateResponseValidationError{ + field: "Result", + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(m.GetResult()).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return APITokenServiceCreateResponseValidationError{ + field: "Result", + reason: "embedded message failed validation", + cause: err, + } + } + } + + if len(errors) > 0 { + return APITokenServiceCreateResponseMultiError(errors) + } + + return nil +} + +// APITokenServiceCreateResponseMultiError is an error wrapping multiple +// validation errors returned by APITokenServiceCreateResponse.ValidateAll() +// if the designated constraints aren't met. +type APITokenServiceCreateResponseMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m APITokenServiceCreateResponseMultiError) Error() string { + var msgs []string + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m APITokenServiceCreateResponseMultiError) AllErrors() []error { return m } + +// APITokenServiceCreateResponseValidationError is the validation error +// returned by APITokenServiceCreateResponse.Validate if the designated +// constraints aren't met. +type APITokenServiceCreateResponseValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e APITokenServiceCreateResponseValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e APITokenServiceCreateResponseValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e APITokenServiceCreateResponseValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e APITokenServiceCreateResponseValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e APITokenServiceCreateResponseValidationError) ErrorName() string { + return "APITokenServiceCreateResponseValidationError" +} + +// Error satisfies the builtin error interface +func (e APITokenServiceCreateResponseValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sAPITokenServiceCreateResponse.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = APITokenServiceCreateResponseValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = APITokenServiceCreateResponseValidationError{} + +// Validate checks the field values on APITokenServiceRevokeRequest with the +// rules defined in the proto definition for this message. If any rules are +// violated, the first error encountered is returned, or nil if there are no violations. +func (m *APITokenServiceRevokeRequest) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on APITokenServiceRevokeRequest with the +// rules defined in the proto definition for this message. If any rules are +// violated, the result is a list of violation errors wrapped in +// APITokenServiceRevokeRequestMultiError, or nil if none found. +func (m *APITokenServiceRevokeRequest) ValidateAll() error { + return m.validate(true) +} + +func (m *APITokenServiceRevokeRequest) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + if err := m._validateUuid(m.GetId()); err != nil { + err = APITokenServiceRevokeRequestValidationError{ + field: "Id", + reason: "value must be a valid UUID", + cause: err, + } + if !all { + return err + } + errors = append(errors, err) + } + + if len(errors) > 0 { + return APITokenServiceRevokeRequestMultiError(errors) + } + + return nil +} + +func (m *APITokenServiceRevokeRequest) _validateUuid(uuid string) error { + if matched := _api_token_uuidPattern.MatchString(uuid); !matched { + return errors.New("invalid uuid format") + } + + return nil +} + +// APITokenServiceRevokeRequestMultiError is an error wrapping multiple +// validation errors returned by APITokenServiceRevokeRequest.ValidateAll() if +// the designated constraints aren't met. +type APITokenServiceRevokeRequestMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m APITokenServiceRevokeRequestMultiError) Error() string { + var msgs []string + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m APITokenServiceRevokeRequestMultiError) AllErrors() []error { return m } + +// APITokenServiceRevokeRequestValidationError is the validation error returned +// by APITokenServiceRevokeRequest.Validate if the designated constraints +// aren't met. +type APITokenServiceRevokeRequestValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e APITokenServiceRevokeRequestValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e APITokenServiceRevokeRequestValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e APITokenServiceRevokeRequestValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e APITokenServiceRevokeRequestValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e APITokenServiceRevokeRequestValidationError) ErrorName() string { + return "APITokenServiceRevokeRequestValidationError" +} + +// Error satisfies the builtin error interface +func (e APITokenServiceRevokeRequestValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sAPITokenServiceRevokeRequest.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = APITokenServiceRevokeRequestValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = APITokenServiceRevokeRequestValidationError{} + +// Validate checks the field values on APITokenServiceRevokeResponse with the +// rules defined in the proto definition for this message. If any rules are +// violated, the first error encountered is returned, or nil if there are no violations. +func (m *APITokenServiceRevokeResponse) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on APITokenServiceRevokeResponse with +// the rules defined in the proto definition for this message. If any rules +// are violated, the result is a list of violation errors wrapped in +// APITokenServiceRevokeResponseMultiError, or nil if none found. +func (m *APITokenServiceRevokeResponse) ValidateAll() error { + return m.validate(true) +} + +func (m *APITokenServiceRevokeResponse) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + if len(errors) > 0 { + return APITokenServiceRevokeResponseMultiError(errors) + } + + return nil +} + +// APITokenServiceRevokeResponseMultiError is an error wrapping multiple +// validation errors returned by APITokenServiceRevokeResponse.ValidateAll() +// if the designated constraints aren't met. +type APITokenServiceRevokeResponseMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m APITokenServiceRevokeResponseMultiError) Error() string { + var msgs []string + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m APITokenServiceRevokeResponseMultiError) AllErrors() []error { return m } + +// APITokenServiceRevokeResponseValidationError is the validation error +// returned by APITokenServiceRevokeResponse.Validate if the designated +// constraints aren't met. +type APITokenServiceRevokeResponseValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e APITokenServiceRevokeResponseValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e APITokenServiceRevokeResponseValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e APITokenServiceRevokeResponseValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e APITokenServiceRevokeResponseValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e APITokenServiceRevokeResponseValidationError) ErrorName() string { + return "APITokenServiceRevokeResponseValidationError" +} + +// Error satisfies the builtin error interface +func (e APITokenServiceRevokeResponseValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sAPITokenServiceRevokeResponse.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = APITokenServiceRevokeResponseValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = APITokenServiceRevokeResponseValidationError{} + +// Validate checks the field values on APITokenServiceListRequest with the +// rules defined in the proto definition for this message. If any rules are +// violated, the first error encountered is returned, or nil if there are no violations. +func (m *APITokenServiceListRequest) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on APITokenServiceListRequest with the +// rules defined in the proto definition for this message. If any rules are +// violated, the result is a list of violation errors wrapped in +// APITokenServiceListRequestMultiError, or nil if none found. +func (m *APITokenServiceListRequest) ValidateAll() error { + return m.validate(true) +} + +func (m *APITokenServiceListRequest) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + // no validation rules for IncludeRevoked + + if len(errors) > 0 { + return APITokenServiceListRequestMultiError(errors) + } + + return nil +} + +// APITokenServiceListRequestMultiError is an error wrapping multiple +// validation errors returned by APITokenServiceListRequest.ValidateAll() if +// the designated constraints aren't met. +type APITokenServiceListRequestMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m APITokenServiceListRequestMultiError) Error() string { + var msgs []string + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m APITokenServiceListRequestMultiError) AllErrors() []error { return m } + +// APITokenServiceListRequestValidationError is the validation error returned +// by APITokenServiceListRequest.Validate if the designated constraints aren't met. +type APITokenServiceListRequestValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e APITokenServiceListRequestValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e APITokenServiceListRequestValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e APITokenServiceListRequestValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e APITokenServiceListRequestValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e APITokenServiceListRequestValidationError) ErrorName() string { + return "APITokenServiceListRequestValidationError" +} + +// Error satisfies the builtin error interface +func (e APITokenServiceListRequestValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sAPITokenServiceListRequest.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = APITokenServiceListRequestValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = APITokenServiceListRequestValidationError{} + +// Validate checks the field values on APITokenServiceListResponse with the +// rules defined in the proto definition for this message. If any rules are +// violated, the first error encountered is returned, or nil if there are no violations. +func (m *APITokenServiceListResponse) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on APITokenServiceListResponse with the +// rules defined in the proto definition for this message. If any rules are +// violated, the result is a list of violation errors wrapped in +// APITokenServiceListResponseMultiError, or nil if none found. +func (m *APITokenServiceListResponse) ValidateAll() error { + return m.validate(true) +} + +func (m *APITokenServiceListResponse) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + for idx, item := range m.GetResult() { + _, _ = idx, item + + if all { + switch v := interface{}(item).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, APITokenServiceListResponseValidationError{ + field: fmt.Sprintf("Result[%v]", idx), + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, APITokenServiceListResponseValidationError{ + field: fmt.Sprintf("Result[%v]", idx), + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(item).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return APITokenServiceListResponseValidationError{ + field: fmt.Sprintf("Result[%v]", idx), + reason: "embedded message failed validation", + cause: err, + } + } + } + + } + + if len(errors) > 0 { + return APITokenServiceListResponseMultiError(errors) + } + + return nil +} + +// APITokenServiceListResponseMultiError is an error wrapping multiple +// validation errors returned by APITokenServiceListResponse.ValidateAll() if +// the designated constraints aren't met. +type APITokenServiceListResponseMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m APITokenServiceListResponseMultiError) Error() string { + var msgs []string + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m APITokenServiceListResponseMultiError) AllErrors() []error { return m } + +// APITokenServiceListResponseValidationError is the validation error returned +// by APITokenServiceListResponse.Validate if the designated constraints +// aren't met. +type APITokenServiceListResponseValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e APITokenServiceListResponseValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e APITokenServiceListResponseValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e APITokenServiceListResponseValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e APITokenServiceListResponseValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e APITokenServiceListResponseValidationError) ErrorName() string { + return "APITokenServiceListResponseValidationError" +} + +// Error satisfies the builtin error interface +func (e APITokenServiceListResponseValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sAPITokenServiceListResponse.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = APITokenServiceListResponseValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = APITokenServiceListResponseValidationError{} + +// Validate checks the field values on APITokenItem with the rules defined in +// the proto definition for this message. If any rules are violated, the first +// error encountered is returned, or nil if there are no violations. +func (m *APITokenItem) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on APITokenItem with the rules defined +// in the proto definition for this message. If any rules are violated, the +// result is a list of violation errors wrapped in APITokenItemMultiError, or +// nil if none found. +func (m *APITokenItem) ValidateAll() error { + return m.validate(true) +} + +func (m *APITokenItem) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + // no validation rules for Id + + // no validation rules for Description + + // no validation rules for OrganizationId + + if all { + switch v := interface{}(m.GetCreatedAt()).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, APITokenItemValidationError{ + field: "CreatedAt", + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, APITokenItemValidationError{ + field: "CreatedAt", + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(m.GetCreatedAt()).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return APITokenItemValidationError{ + field: "CreatedAt", + reason: "embedded message failed validation", + cause: err, + } + } + } + + if all { + switch v := interface{}(m.GetRevokedAt()).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, APITokenItemValidationError{ + field: "RevokedAt", + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, APITokenItemValidationError{ + field: "RevokedAt", + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(m.GetRevokedAt()).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return APITokenItemValidationError{ + field: "RevokedAt", + reason: "embedded message failed validation", + cause: err, + } + } + } + + if all { + switch v := interface{}(m.GetExpiresAt()).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, APITokenItemValidationError{ + field: "ExpiresAt", + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, APITokenItemValidationError{ + field: "ExpiresAt", + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(m.GetExpiresAt()).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return APITokenItemValidationError{ + field: "ExpiresAt", + reason: "embedded message failed validation", + cause: err, + } + } + } + + if len(errors) > 0 { + return APITokenItemMultiError(errors) + } + + return nil +} + +// APITokenItemMultiError is an error wrapping multiple validation errors +// returned by APITokenItem.ValidateAll() if the designated constraints aren't met. +type APITokenItemMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m APITokenItemMultiError) Error() string { + var msgs []string + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m APITokenItemMultiError) AllErrors() []error { return m } + +// APITokenItemValidationError is the validation error returned by +// APITokenItem.Validate if the designated constraints aren't met. +type APITokenItemValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e APITokenItemValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e APITokenItemValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e APITokenItemValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e APITokenItemValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e APITokenItemValidationError) ErrorName() string { return "APITokenItemValidationError" } + +// Error satisfies the builtin error interface +func (e APITokenItemValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sAPITokenItem.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = APITokenItemValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = APITokenItemValidationError{} + +// Validate checks the field values on +// APITokenServiceCreateResponse_APITokenFull with the rules defined in the +// proto definition for this message. If any rules are violated, the first +// error encountered is returned, or nil if there are no violations. +func (m *APITokenServiceCreateResponse_APITokenFull) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on +// APITokenServiceCreateResponse_APITokenFull with the rules defined in the +// proto definition for this message. If any rules are violated, the result is +// a list of violation errors wrapped in +// APITokenServiceCreateResponse_APITokenFullMultiError, or nil if none found. +func (m *APITokenServiceCreateResponse_APITokenFull) ValidateAll() error { + return m.validate(true) +} + +func (m *APITokenServiceCreateResponse_APITokenFull) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + if all { + switch v := interface{}(m.GetItem()).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, APITokenServiceCreateResponse_APITokenFullValidationError{ + field: "Item", + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, APITokenServiceCreateResponse_APITokenFullValidationError{ + field: "Item", + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(m.GetItem()).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return APITokenServiceCreateResponse_APITokenFullValidationError{ + field: "Item", + reason: "embedded message failed validation", + cause: err, + } + } + } + + // no validation rules for Jwt + + if len(errors) > 0 { + return APITokenServiceCreateResponse_APITokenFullMultiError(errors) + } + + return nil +} + +// APITokenServiceCreateResponse_APITokenFullMultiError is an error wrapping +// multiple validation errors returned by +// APITokenServiceCreateResponse_APITokenFull.ValidateAll() if the designated +// constraints aren't met. +type APITokenServiceCreateResponse_APITokenFullMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m APITokenServiceCreateResponse_APITokenFullMultiError) Error() string { + var msgs []string + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m APITokenServiceCreateResponse_APITokenFullMultiError) AllErrors() []error { return m } + +// APITokenServiceCreateResponse_APITokenFullValidationError is the validation +// error returned by APITokenServiceCreateResponse_APITokenFull.Validate if +// the designated constraints aren't met. +type APITokenServiceCreateResponse_APITokenFullValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e APITokenServiceCreateResponse_APITokenFullValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e APITokenServiceCreateResponse_APITokenFullValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e APITokenServiceCreateResponse_APITokenFullValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e APITokenServiceCreateResponse_APITokenFullValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e APITokenServiceCreateResponse_APITokenFullValidationError) ErrorName() string { + return "APITokenServiceCreateResponse_APITokenFullValidationError" +} + +// Error satisfies the builtin error interface +func (e APITokenServiceCreateResponse_APITokenFullValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sAPITokenServiceCreateResponse_APITokenFull.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = APITokenServiceCreateResponse_APITokenFullValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = APITokenServiceCreateResponse_APITokenFullValidationError{} diff --git a/app/controlplane/api/controlplane/v1/api_token.proto b/app/controlplane/api/controlplane/v1/api_token.proto new file mode 100644 index 000000000..ae2095c36 --- /dev/null +++ b/app/controlplane/api/controlplane/v1/api_token.proto @@ -0,0 +1,67 @@ +// +// Copyright 2023 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. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package controlplane.v1; + +option go_package = "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1;v1"; + +import "validate/validate.proto"; +import "google/protobuf/timestamp.proto"; +import "google/protobuf/duration.proto"; + +service APITokenService { + rpc Create (APITokenServiceCreateRequest) returns (APITokenServiceCreateResponse); + rpc List (APITokenServiceListRequest) returns (APITokenServiceListResponse); + rpc Revoke (APITokenServiceRevokeRequest) returns (APITokenServiceRevokeResponse); +} + +message APITokenServiceCreateRequest { + optional string description = 1; + optional google.protobuf.Duration expires_in = 2; +} + +message APITokenServiceCreateResponse { + APITokenFull result = 1; + + message APITokenFull { + APITokenItem item = 1; + string jwt = 2; + } +} + +message APITokenServiceRevokeRequest { + string id = 1 [(validate.rules).string.uuid = true]; +} + +message APITokenServiceRevokeResponse {} + +message APITokenServiceListRequest { + bool include_revoked = 1; +} + +message APITokenServiceListResponse { + repeated APITokenItem result = 1; +} + +message APITokenItem { + string id = 1; + string description = 2; + string organization_id = 3; + google.protobuf.Timestamp created_at = 4; + google.protobuf.Timestamp revoked_at = 5; + google.protobuf.Timestamp expires_at = 6; +} \ No newline at end of file diff --git a/app/controlplane/api/controlplane/v1/api_token_grpc.pb.go b/app/controlplane/api/controlplane/v1/api_token_grpc.pb.go new file mode 100644 index 000000000..0574292e5 --- /dev/null +++ b/app/controlplane/api/controlplane/v1/api_token_grpc.pb.go @@ -0,0 +1,198 @@ +// +// Copyright 2023 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. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.3.0 +// - protoc (unknown) +// source: controlplane/v1/api_token.proto + +package v1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +const ( + APITokenService_Create_FullMethodName = "/controlplane.v1.APITokenService/Create" + APITokenService_List_FullMethodName = "/controlplane.v1.APITokenService/List" + APITokenService_Revoke_FullMethodName = "/controlplane.v1.APITokenService/Revoke" +) + +// APITokenServiceClient is the client API for APITokenService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type APITokenServiceClient interface { + Create(ctx context.Context, in *APITokenServiceCreateRequest, opts ...grpc.CallOption) (*APITokenServiceCreateResponse, error) + List(ctx context.Context, in *APITokenServiceListRequest, opts ...grpc.CallOption) (*APITokenServiceListResponse, error) + Revoke(ctx context.Context, in *APITokenServiceRevokeRequest, opts ...grpc.CallOption) (*APITokenServiceRevokeResponse, error) +} + +type aPITokenServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewAPITokenServiceClient(cc grpc.ClientConnInterface) APITokenServiceClient { + return &aPITokenServiceClient{cc} +} + +func (c *aPITokenServiceClient) Create(ctx context.Context, in *APITokenServiceCreateRequest, opts ...grpc.CallOption) (*APITokenServiceCreateResponse, error) { + out := new(APITokenServiceCreateResponse) + err := c.cc.Invoke(ctx, APITokenService_Create_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *aPITokenServiceClient) List(ctx context.Context, in *APITokenServiceListRequest, opts ...grpc.CallOption) (*APITokenServiceListResponse, error) { + out := new(APITokenServiceListResponse) + err := c.cc.Invoke(ctx, APITokenService_List_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *aPITokenServiceClient) Revoke(ctx context.Context, in *APITokenServiceRevokeRequest, opts ...grpc.CallOption) (*APITokenServiceRevokeResponse, error) { + out := new(APITokenServiceRevokeResponse) + err := c.cc.Invoke(ctx, APITokenService_Revoke_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// APITokenServiceServer is the server API for APITokenService service. +// All implementations must embed UnimplementedAPITokenServiceServer +// for forward compatibility +type APITokenServiceServer interface { + Create(context.Context, *APITokenServiceCreateRequest) (*APITokenServiceCreateResponse, error) + List(context.Context, *APITokenServiceListRequest) (*APITokenServiceListResponse, error) + Revoke(context.Context, *APITokenServiceRevokeRequest) (*APITokenServiceRevokeResponse, error) + mustEmbedUnimplementedAPITokenServiceServer() +} + +// UnimplementedAPITokenServiceServer must be embedded to have forward compatible implementations. +type UnimplementedAPITokenServiceServer struct { +} + +func (UnimplementedAPITokenServiceServer) Create(context.Context, *APITokenServiceCreateRequest) (*APITokenServiceCreateResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Create not implemented") +} +func (UnimplementedAPITokenServiceServer) List(context.Context, *APITokenServiceListRequest) (*APITokenServiceListResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method List not implemented") +} +func (UnimplementedAPITokenServiceServer) Revoke(context.Context, *APITokenServiceRevokeRequest) (*APITokenServiceRevokeResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Revoke not implemented") +} +func (UnimplementedAPITokenServiceServer) mustEmbedUnimplementedAPITokenServiceServer() {} + +// UnsafeAPITokenServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to APITokenServiceServer will +// result in compilation errors. +type UnsafeAPITokenServiceServer interface { + mustEmbedUnimplementedAPITokenServiceServer() +} + +func RegisterAPITokenServiceServer(s grpc.ServiceRegistrar, srv APITokenServiceServer) { + s.RegisterService(&APITokenService_ServiceDesc, srv) +} + +func _APITokenService_Create_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(APITokenServiceCreateRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(APITokenServiceServer).Create(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: APITokenService_Create_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(APITokenServiceServer).Create(ctx, req.(*APITokenServiceCreateRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _APITokenService_List_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(APITokenServiceListRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(APITokenServiceServer).List(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: APITokenService_List_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(APITokenServiceServer).List(ctx, req.(*APITokenServiceListRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _APITokenService_Revoke_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(APITokenServiceRevokeRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(APITokenServiceServer).Revoke(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: APITokenService_Revoke_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(APITokenServiceServer).Revoke(ctx, req.(*APITokenServiceRevokeRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// APITokenService_ServiceDesc is the grpc.ServiceDesc for APITokenService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var APITokenService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "controlplane.v1.APITokenService", + HandlerType: (*APITokenServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Create", + Handler: _APITokenService_Create_Handler, + }, + { + MethodName: "List", + Handler: _APITokenService_List_Handler, + }, + { + MethodName: "Revoke", + Handler: _APITokenService_Revoke_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "controlplane/v1/api_token.proto", +} diff --git a/app/controlplane/api/gen/frontend/controlplane/v1/api_token.ts b/app/controlplane/api/gen/frontend/controlplane/v1/api_token.ts new file mode 100644 index 000000000..67dfd692b --- /dev/null +++ b/app/controlplane/api/gen/frontend/controlplane/v1/api_token.ts @@ -0,0 +1,854 @@ +/* eslint-disable */ +import { grpc } from "@improbable-eng/grpc-web"; +import { BrowserHeaders } from "browser-headers"; +import _m0 from "protobufjs/minimal"; +import { Duration } from "../../google/protobuf/duration"; +import { Timestamp } from "../../google/protobuf/timestamp"; + +export const protobufPackage = "controlplane.v1"; + +export interface APITokenServiceCreateRequest { + description?: string | undefined; + expiresIn?: Duration | undefined; +} + +export interface APITokenServiceCreateResponse { + result?: APITokenServiceCreateResponse_APITokenFull; +} + +export interface APITokenServiceCreateResponse_APITokenFull { + item?: APITokenItem; + jwt: string; +} + +export interface APITokenServiceRevokeRequest { + id: string; +} + +export interface APITokenServiceRevokeResponse { +} + +export interface APITokenServiceListRequest { + includeRevoked: boolean; +} + +export interface APITokenServiceListResponse { + result: APITokenItem[]; +} + +export interface APITokenItem { + id: string; + description: string; + organizationId: string; + createdAt?: Date; + revokedAt?: Date; + expiresAt?: Date; +} + +function createBaseAPITokenServiceCreateRequest(): APITokenServiceCreateRequest { + return { description: undefined, expiresIn: undefined }; +} + +export const APITokenServiceCreateRequest = { + encode(message: APITokenServiceCreateRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.description !== undefined) { + writer.uint32(10).string(message.description); + } + if (message.expiresIn !== undefined) { + Duration.encode(message.expiresIn, writer.uint32(18).fork()).ldelim(); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): APITokenServiceCreateRequest { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseAPITokenServiceCreateRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 10) { + break; + } + + message.description = reader.string(); + continue; + case 2: + if (tag !== 18) { + break; + } + + message.expiresIn = Duration.decode(reader, reader.uint32()); + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): APITokenServiceCreateRequest { + return { + description: isSet(object.description) ? String(object.description) : undefined, + expiresIn: isSet(object.expiresIn) ? Duration.fromJSON(object.expiresIn) : undefined, + }; + }, + + toJSON(message: APITokenServiceCreateRequest): unknown { + const obj: any = {}; + message.description !== undefined && (obj.description = message.description); + message.expiresIn !== undefined && + (obj.expiresIn = message.expiresIn ? Duration.toJSON(message.expiresIn) : undefined); + return obj; + }, + + create, I>>(base?: I): APITokenServiceCreateRequest { + return APITokenServiceCreateRequest.fromPartial(base ?? {}); + }, + + fromPartial, I>>(object: I): APITokenServiceCreateRequest { + const message = createBaseAPITokenServiceCreateRequest(); + message.description = object.description ?? undefined; + message.expiresIn = (object.expiresIn !== undefined && object.expiresIn !== null) + ? Duration.fromPartial(object.expiresIn) + : undefined; + return message; + }, +}; + +function createBaseAPITokenServiceCreateResponse(): APITokenServiceCreateResponse { + return { result: undefined }; +} + +export const APITokenServiceCreateResponse = { + encode(message: APITokenServiceCreateResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.result !== undefined) { + APITokenServiceCreateResponse_APITokenFull.encode(message.result, writer.uint32(10).fork()).ldelim(); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): APITokenServiceCreateResponse { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseAPITokenServiceCreateResponse(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 10) { + break; + } + + message.result = APITokenServiceCreateResponse_APITokenFull.decode(reader, reader.uint32()); + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): APITokenServiceCreateResponse { + return { + result: isSet(object.result) ? APITokenServiceCreateResponse_APITokenFull.fromJSON(object.result) : undefined, + }; + }, + + toJSON(message: APITokenServiceCreateResponse): unknown { + const obj: any = {}; + message.result !== undefined && + (obj.result = message.result ? APITokenServiceCreateResponse_APITokenFull.toJSON(message.result) : undefined); + return obj; + }, + + create, I>>(base?: I): APITokenServiceCreateResponse { + return APITokenServiceCreateResponse.fromPartial(base ?? {}); + }, + + fromPartial, I>>( + object: I, + ): APITokenServiceCreateResponse { + const message = createBaseAPITokenServiceCreateResponse(); + message.result = (object.result !== undefined && object.result !== null) + ? APITokenServiceCreateResponse_APITokenFull.fromPartial(object.result) + : undefined; + return message; + }, +}; + +function createBaseAPITokenServiceCreateResponse_APITokenFull(): APITokenServiceCreateResponse_APITokenFull { + return { item: undefined, jwt: "" }; +} + +export const APITokenServiceCreateResponse_APITokenFull = { + encode(message: APITokenServiceCreateResponse_APITokenFull, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.item !== undefined) { + APITokenItem.encode(message.item, writer.uint32(10).fork()).ldelim(); + } + if (message.jwt !== "") { + writer.uint32(18).string(message.jwt); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): APITokenServiceCreateResponse_APITokenFull { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseAPITokenServiceCreateResponse_APITokenFull(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 10) { + break; + } + + message.item = APITokenItem.decode(reader, reader.uint32()); + continue; + case 2: + if (tag !== 18) { + break; + } + + message.jwt = reader.string(); + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): APITokenServiceCreateResponse_APITokenFull { + return { + item: isSet(object.item) ? APITokenItem.fromJSON(object.item) : undefined, + jwt: isSet(object.jwt) ? String(object.jwt) : "", + }; + }, + + toJSON(message: APITokenServiceCreateResponse_APITokenFull): unknown { + const obj: any = {}; + message.item !== undefined && (obj.item = message.item ? APITokenItem.toJSON(message.item) : undefined); + message.jwt !== undefined && (obj.jwt = message.jwt); + return obj; + }, + + create, I>>( + base?: I, + ): APITokenServiceCreateResponse_APITokenFull { + return APITokenServiceCreateResponse_APITokenFull.fromPartial(base ?? {}); + }, + + fromPartial, I>>( + object: I, + ): APITokenServiceCreateResponse_APITokenFull { + const message = createBaseAPITokenServiceCreateResponse_APITokenFull(); + message.item = (object.item !== undefined && object.item !== null) + ? APITokenItem.fromPartial(object.item) + : undefined; + message.jwt = object.jwt ?? ""; + return message; + }, +}; + +function createBaseAPITokenServiceRevokeRequest(): APITokenServiceRevokeRequest { + return { id: "" }; +} + +export const APITokenServiceRevokeRequest = { + encode(message: APITokenServiceRevokeRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.id !== "") { + writer.uint32(10).string(message.id); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): APITokenServiceRevokeRequest { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseAPITokenServiceRevokeRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 10) { + break; + } + + message.id = reader.string(); + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): APITokenServiceRevokeRequest { + return { id: isSet(object.id) ? String(object.id) : "" }; + }, + + toJSON(message: APITokenServiceRevokeRequest): unknown { + const obj: any = {}; + message.id !== undefined && (obj.id = message.id); + return obj; + }, + + create, I>>(base?: I): APITokenServiceRevokeRequest { + return APITokenServiceRevokeRequest.fromPartial(base ?? {}); + }, + + fromPartial, I>>(object: I): APITokenServiceRevokeRequest { + const message = createBaseAPITokenServiceRevokeRequest(); + message.id = object.id ?? ""; + return message; + }, +}; + +function createBaseAPITokenServiceRevokeResponse(): APITokenServiceRevokeResponse { + return {}; +} + +export const APITokenServiceRevokeResponse = { + encode(_: APITokenServiceRevokeResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): APITokenServiceRevokeResponse { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseAPITokenServiceRevokeResponse(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(_: any): APITokenServiceRevokeResponse { + return {}; + }, + + toJSON(_: APITokenServiceRevokeResponse): unknown { + const obj: any = {}; + return obj; + }, + + create, I>>(base?: I): APITokenServiceRevokeResponse { + return APITokenServiceRevokeResponse.fromPartial(base ?? {}); + }, + + fromPartial, I>>(_: I): APITokenServiceRevokeResponse { + const message = createBaseAPITokenServiceRevokeResponse(); + return message; + }, +}; + +function createBaseAPITokenServiceListRequest(): APITokenServiceListRequest { + return { includeRevoked: false }; +} + +export const APITokenServiceListRequest = { + encode(message: APITokenServiceListRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.includeRevoked === true) { + writer.uint32(8).bool(message.includeRevoked); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): APITokenServiceListRequest { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseAPITokenServiceListRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 8) { + break; + } + + message.includeRevoked = reader.bool(); + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): APITokenServiceListRequest { + return { includeRevoked: isSet(object.includeRevoked) ? Boolean(object.includeRevoked) : false }; + }, + + toJSON(message: APITokenServiceListRequest): unknown { + const obj: any = {}; + message.includeRevoked !== undefined && (obj.includeRevoked = message.includeRevoked); + return obj; + }, + + create, I>>(base?: I): APITokenServiceListRequest { + return APITokenServiceListRequest.fromPartial(base ?? {}); + }, + + fromPartial, I>>(object: I): APITokenServiceListRequest { + const message = createBaseAPITokenServiceListRequest(); + message.includeRevoked = object.includeRevoked ?? false; + return message; + }, +}; + +function createBaseAPITokenServiceListResponse(): APITokenServiceListResponse { + return { result: [] }; +} + +export const APITokenServiceListResponse = { + encode(message: APITokenServiceListResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + for (const v of message.result) { + APITokenItem.encode(v!, writer.uint32(10).fork()).ldelim(); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): APITokenServiceListResponse { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseAPITokenServiceListResponse(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 10) { + break; + } + + message.result.push(APITokenItem.decode(reader, reader.uint32())); + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): APITokenServiceListResponse { + return { result: Array.isArray(object?.result) ? object.result.map((e: any) => APITokenItem.fromJSON(e)) : [] }; + }, + + toJSON(message: APITokenServiceListResponse): unknown { + const obj: any = {}; + if (message.result) { + obj.result = message.result.map((e) => e ? APITokenItem.toJSON(e) : undefined); + } else { + obj.result = []; + } + return obj; + }, + + create, I>>(base?: I): APITokenServiceListResponse { + return APITokenServiceListResponse.fromPartial(base ?? {}); + }, + + fromPartial, I>>(object: I): APITokenServiceListResponse { + const message = createBaseAPITokenServiceListResponse(); + message.result = object.result?.map((e) => APITokenItem.fromPartial(e)) || []; + return message; + }, +}; + +function createBaseAPITokenItem(): APITokenItem { + return { + id: "", + description: "", + organizationId: "", + createdAt: undefined, + revokedAt: undefined, + expiresAt: undefined, + }; +} + +export const APITokenItem = { + encode(message: APITokenItem, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.id !== "") { + writer.uint32(10).string(message.id); + } + if (message.description !== "") { + writer.uint32(18).string(message.description); + } + if (message.organizationId !== "") { + writer.uint32(26).string(message.organizationId); + } + if (message.createdAt !== undefined) { + Timestamp.encode(toTimestamp(message.createdAt), writer.uint32(34).fork()).ldelim(); + } + if (message.revokedAt !== undefined) { + Timestamp.encode(toTimestamp(message.revokedAt), writer.uint32(42).fork()).ldelim(); + } + if (message.expiresAt !== undefined) { + Timestamp.encode(toTimestamp(message.expiresAt), writer.uint32(50).fork()).ldelim(); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): APITokenItem { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseAPITokenItem(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 10) { + break; + } + + message.id = reader.string(); + continue; + case 2: + if (tag !== 18) { + break; + } + + message.description = reader.string(); + continue; + case 3: + if (tag !== 26) { + break; + } + + message.organizationId = reader.string(); + continue; + case 4: + if (tag !== 34) { + break; + } + + message.createdAt = fromTimestamp(Timestamp.decode(reader, reader.uint32())); + continue; + case 5: + if (tag !== 42) { + break; + } + + message.revokedAt = fromTimestamp(Timestamp.decode(reader, reader.uint32())); + continue; + case 6: + if (tag !== 50) { + break; + } + + message.expiresAt = fromTimestamp(Timestamp.decode(reader, reader.uint32())); + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): APITokenItem { + return { + id: isSet(object.id) ? String(object.id) : "", + description: isSet(object.description) ? String(object.description) : "", + organizationId: isSet(object.organizationId) ? String(object.organizationId) : "", + createdAt: isSet(object.createdAt) ? fromJsonTimestamp(object.createdAt) : undefined, + revokedAt: isSet(object.revokedAt) ? fromJsonTimestamp(object.revokedAt) : undefined, + expiresAt: isSet(object.expiresAt) ? fromJsonTimestamp(object.expiresAt) : undefined, + }; + }, + + toJSON(message: APITokenItem): unknown { + const obj: any = {}; + message.id !== undefined && (obj.id = message.id); + message.description !== undefined && (obj.description = message.description); + message.organizationId !== undefined && (obj.organizationId = message.organizationId); + message.createdAt !== undefined && (obj.createdAt = message.createdAt.toISOString()); + message.revokedAt !== undefined && (obj.revokedAt = message.revokedAt.toISOString()); + message.expiresAt !== undefined && (obj.expiresAt = message.expiresAt.toISOString()); + return obj; + }, + + create, I>>(base?: I): APITokenItem { + return APITokenItem.fromPartial(base ?? {}); + }, + + fromPartial, I>>(object: I): APITokenItem { + const message = createBaseAPITokenItem(); + message.id = object.id ?? ""; + message.description = object.description ?? ""; + message.organizationId = object.organizationId ?? ""; + message.createdAt = object.createdAt ?? undefined; + message.revokedAt = object.revokedAt ?? undefined; + message.expiresAt = object.expiresAt ?? undefined; + return message; + }, +}; + +export interface APITokenService { + Create( + request: DeepPartial, + metadata?: grpc.Metadata, + ): Promise; + List( + request: DeepPartial, + metadata?: grpc.Metadata, + ): Promise; + Revoke( + request: DeepPartial, + metadata?: grpc.Metadata, + ): Promise; +} + +export class APITokenServiceClientImpl implements APITokenService { + private readonly rpc: Rpc; + + constructor(rpc: Rpc) { + this.rpc = rpc; + this.Create = this.Create.bind(this); + this.List = this.List.bind(this); + this.Revoke = this.Revoke.bind(this); + } + + Create( + request: DeepPartial, + metadata?: grpc.Metadata, + ): Promise { + return this.rpc.unary(APITokenServiceCreateDesc, APITokenServiceCreateRequest.fromPartial(request), metadata); + } + + List( + request: DeepPartial, + metadata?: grpc.Metadata, + ): Promise { + return this.rpc.unary(APITokenServiceListDesc, APITokenServiceListRequest.fromPartial(request), metadata); + } + + Revoke( + request: DeepPartial, + metadata?: grpc.Metadata, + ): Promise { + return this.rpc.unary(APITokenServiceRevokeDesc, APITokenServiceRevokeRequest.fromPartial(request), metadata); + } +} + +export const APITokenServiceDesc = { serviceName: "controlplane.v1.APITokenService" }; + +export const APITokenServiceCreateDesc: UnaryMethodDefinitionish = { + methodName: "Create", + service: APITokenServiceDesc, + requestStream: false, + responseStream: false, + requestType: { + serializeBinary() { + return APITokenServiceCreateRequest.encode(this).finish(); + }, + } as any, + responseType: { + deserializeBinary(data: Uint8Array) { + const value = APITokenServiceCreateResponse.decode(data); + return { + ...value, + toObject() { + return value; + }, + }; + }, + } as any, +}; + +export const APITokenServiceListDesc: UnaryMethodDefinitionish = { + methodName: "List", + service: APITokenServiceDesc, + requestStream: false, + responseStream: false, + requestType: { + serializeBinary() { + return APITokenServiceListRequest.encode(this).finish(); + }, + } as any, + responseType: { + deserializeBinary(data: Uint8Array) { + const value = APITokenServiceListResponse.decode(data); + return { + ...value, + toObject() { + return value; + }, + }; + }, + } as any, +}; + +export const APITokenServiceRevokeDesc: UnaryMethodDefinitionish = { + methodName: "Revoke", + service: APITokenServiceDesc, + requestStream: false, + responseStream: false, + requestType: { + serializeBinary() { + return APITokenServiceRevokeRequest.encode(this).finish(); + }, + } as any, + responseType: { + deserializeBinary(data: Uint8Array) { + const value = APITokenServiceRevokeResponse.decode(data); + return { + ...value, + toObject() { + return value; + }, + }; + }, + } as any, +}; + +interface UnaryMethodDefinitionishR extends grpc.UnaryMethodDefinition { + requestStream: any; + responseStream: any; +} + +type UnaryMethodDefinitionish = UnaryMethodDefinitionishR; + +interface Rpc { + unary( + methodDesc: T, + request: any, + metadata: grpc.Metadata | undefined, + ): Promise; +} + +export class GrpcWebImpl { + private host: string; + private options: { + transport?: grpc.TransportFactory; + + debug?: boolean; + metadata?: grpc.Metadata; + upStreamRetryCodes?: number[]; + }; + + constructor( + host: string, + options: { + transport?: grpc.TransportFactory; + + debug?: boolean; + metadata?: grpc.Metadata; + upStreamRetryCodes?: number[]; + }, + ) { + this.host = host; + this.options = options; + } + + unary( + methodDesc: T, + _request: any, + metadata: grpc.Metadata | undefined, + ): Promise { + const request = { ..._request, ...methodDesc.requestType }; + const maybeCombinedMetadata = metadata && this.options.metadata + ? new BrowserHeaders({ ...this.options?.metadata.headersMap, ...metadata?.headersMap }) + : metadata || this.options.metadata; + return new Promise((resolve, reject) => { + grpc.unary(methodDesc, { + request, + host: this.host, + metadata: maybeCombinedMetadata, + transport: this.options.transport, + debug: this.options.debug, + onEnd: function (response) { + if (response.status === grpc.Code.OK) { + resolve(response.message!.toObject()); + } else { + const err = new GrpcWebError(response.statusMessage, response.status, response.trailers); + reject(err); + } + }, + }); + }); + } +} + +declare var self: any | undefined; +declare var window: any | undefined; +declare var global: any | undefined; +var tsProtoGlobalThis: any = (() => { + if (typeof globalThis !== "undefined") { + return globalThis; + } + if (typeof self !== "undefined") { + return self; + } + if (typeof window !== "undefined") { + return window; + } + if (typeof global !== "undefined") { + return global; + } + throw "Unable to locate global object"; +})(); + +type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; + +export type DeepPartial = T extends Builtin ? T + : T extends Array ? Array> : T extends ReadonlyArray ? ReadonlyArray> + : T extends {} ? { [K in keyof T]?: DeepPartial } + : Partial; + +type KeysOfUnion = T extends T ? keyof T : never; +export type Exact = P extends Builtin ? P + : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; + +function toTimestamp(date: Date): Timestamp { + const seconds = date.getTime() / 1_000; + const nanos = (date.getTime() % 1_000) * 1_000_000; + return { seconds, nanos }; +} + +function fromTimestamp(t: Timestamp): Date { + let millis = (t.seconds || 0) * 1_000; + millis += (t.nanos || 0) / 1_000_000; + return new Date(millis); +} + +function fromJsonTimestamp(o: any): Date { + if (o instanceof Date) { + return o; + } else if (typeof o === "string") { + return new Date(o); + } else { + return fromTimestamp(Timestamp.fromJSON(o)); + } +} + +function isSet(value: any): boolean { + return value !== null && value !== undefined; +} + +export class GrpcWebError extends tsProtoGlobalThis.Error { + constructor(message: string, public code: grpc.Code, public metadata: grpc.Metadata) { + super(message); + } +} diff --git a/app/controlplane/cmd/wire_gen.go b/app/controlplane/cmd/wire_gen.go index 66b394ae6..913489380 100644 --- a/app/controlplane/cmd/wire_gen.go +++ b/app/controlplane/cmd/wire_gen.go @@ -71,6 +71,12 @@ func wireApp(bootstrap *conf.Bootstrap, readerWriter credentials.ReaderWriter, l cleanup() return nil, nil, err } + apiTokenRepo := data.NewAPITokenRepo(dataData, logger) + apiTokenUseCase, err := biz.NewAPITokenUseCase(apiTokenRepo, auth, logger) + if err != nil { + cleanup() + return nil, nil, err + } workflowContractRepo := data.NewWorkflowContractRepo(dataData, logger) workflowContractUseCase := biz.NewWorkflowContractUseCase(workflowContractRepo, logger) workflowUseCase := biz.NewWorkflowUsecase(workflowRepo, workflowContractUseCase, logger) @@ -142,32 +148,36 @@ func wireApp(bootstrap *conf.Bootstrap, readerWriter credentials.ReaderWriter, l } orgInvitationService := service.NewOrgInvitationService(orgInvitationUseCase, v2...) referrerService := service.NewReferrerService(referrerUseCase, v2...) + apiTokenService := service.NewAPITokenService(apiTokenUseCase, v2...) opts := &server.Opts{ - UserUseCase: userUseCase, - RobotAccountUseCase: robotAccountUseCase, - CASBackendUseCase: casBackendUseCase, - CASClientUseCase: casClientUseCase, - IntegrationUseCase: integrationUseCase, - ReferrerUseCase: referrerUseCase, - WorkflowSvc: workflowService, - AuthSvc: authService, - RobotAccountSvc: robotAccountService, - WorkflowRunSvc: workflowRunService, - AttestationSvc: attestationService, - WorkflowContractSvc: workflowContractService, - ContextSvc: contextService, - CASCredsSvc: casCredentialsService, - OrgMetricsSvc: orgMetricsService, - IntegrationsSvc: integrationsService, - OrganizationSvc: organizationService, - CASBackendSvc: casBackendService, - CASRedirectSvc: casRedirectService, - OrgInvitationSvc: orgInvitationService, - ReferrerSvc: referrerService, - Logger: logger, - ServerConfig: confServer, - AuthConfig: auth, - Credentials: readerWriter, + UserUseCase: userUseCase, + RobotAccountUseCase: robotAccountUseCase, + CASBackendUseCase: casBackendUseCase, + CASClientUseCase: casClientUseCase, + IntegrationUseCase: integrationUseCase, + ReferrerUseCase: referrerUseCase, + APITokenUseCase: apiTokenUseCase, + OrganizationUserCase: organizationUseCase, + WorkflowSvc: workflowService, + AuthSvc: authService, + RobotAccountSvc: robotAccountService, + WorkflowRunSvc: workflowRunService, + AttestationSvc: attestationService, + WorkflowContractSvc: workflowContractService, + ContextSvc: contextService, + CASCredsSvc: casCredentialsService, + OrgMetricsSvc: orgMetricsService, + IntegrationsSvc: integrationsService, + OrganizationSvc: organizationService, + CASBackendSvc: casBackendService, + CASRedirectSvc: casRedirectService, + OrgInvitationSvc: orgInvitationService, + ReferrerSvc: referrerService, + APITokenSvc: apiTokenService, + Logger: logger, + ServerConfig: confServer, + AuthConfig: auth, + Credentials: readerWriter, } grpcServer, err := server.NewGRPCServer(opts) if err != nil { diff --git a/app/controlplane/internal/biz/apitoken.go b/app/controlplane/internal/biz/apitoken.go index 38400205e..54d21d192 100644 --- a/app/controlplane/internal/biz/apitoken.go +++ b/app/controlplane/internal/biz/apitoken.go @@ -46,6 +46,7 @@ type APITokenRepo interface { Create(ctx context.Context, description *string, expiresAt *time.Time, organizationID uuid.UUID) (*APIToken, error) List(ctx context.Context, orgID uuid.UUID, includeRevoked bool) ([]*APIToken, error) Revoke(ctx context.Context, orgID, ID uuid.UUID) error + FindByID(ctx context.Context, ID uuid.UUID) (*APIToken, error) } type APITokenUseCase struct { @@ -96,7 +97,7 @@ func (uc *APITokenUseCase) Create(ctx context.Context, description *string, expi } // generate the JWT - token.JWT, err = uc.jwtBuilder.GenerateJWT(orgID, token.ID.String(), expiresAt) + token.JWT, err = uc.jwtBuilder.GenerateJWT(token.ID.String(), expiresAt) if err != nil { return nil, fmt.Errorf("generating jwt: %w", err) } @@ -126,3 +127,12 @@ func (uc *APITokenUseCase) Revoke(ctx context.Context, orgID, id string) error { return uc.apiTokenRepo.Revoke(ctx, orgUUID, uuid) } + +func (uc *APITokenUseCase) FindByID(ctx context.Context, id string) (*APIToken, error) { + uuid, err := uuid.Parse(id) + if err != nil { + return nil, NewErrInvalidUUID(err) + } + + return uc.apiTokenRepo.FindByID(ctx, uuid) +} diff --git a/app/controlplane/internal/biz/apitoken_integration_test.go b/app/controlplane/internal/biz/apitoken_integration_test.go index 963f08027..0a5274753 100644 --- a/app/controlplane/internal/biz/apitoken_integration_test.go +++ b/app/controlplane/internal/biz/apitoken_integration_test.go @@ -22,8 +22,7 @@ import ( "github.com/chainloop-dev/chainloop/app/controlplane/internal/biz" "github.com/chainloop-dev/chainloop/app/controlplane/internal/biz/testhelpers" - "github.com/chainloop-dev/chainloop/app/controlplane/internal/jwt/apitoken" - "github.com/golang-jwt/jwt" + "github.com/golang-jwt/jwt/v4" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -153,7 +152,7 @@ func (s *apiTokenTestSuite) TestGeneratedJWT() { s.NoError(err) require.NotNil(s.T(), token) - claims := &apitoken.CustomClaims{} + claims := &jwt.RegisteredClaims{} tokenInfo, err := jwt.ParseWithClaims(token.JWT, claims, func(_ *jwt.Token) (interface{}, error) { return []byte("test"), nil }) @@ -162,7 +161,7 @@ func (s *apiTokenTestSuite) TestGeneratedJWT() { s.True(tokenInfo.Valid) // The resulting JWT should have the same org, token ID and expiration time than // the reference in the DB - s.Equal(token.OrganizationID.String(), claims.OrgID) + s.Equal(token.OrganizationID.String(), s.org.ID) s.Equal(token.ID.String(), claims.ID) s.Equal(token.ExpiresAt.Truncate(time.Second), claims.ExpiresAt.Truncate(time.Second)) } diff --git a/app/controlplane/internal/data/apitoken.go b/app/controlplane/internal/data/apitoken.go index fc7482de5..ee3331a03 100644 --- a/app/controlplane/internal/data/apitoken.go +++ b/app/controlplane/internal/data/apitoken.go @@ -53,6 +53,17 @@ func (r *APITokenRepo) Create(ctx context.Context, description *string, expiresA return entAPITokenToBiz(token), nil } +func (r *APITokenRepo) FindByID(ctx context.Context, id uuid.UUID) (*biz.APIToken, error) { + token, err := r.data.db.APIToken.Get(ctx, id) + if err != nil && !ent.IsNotFound(err) { + return nil, fmt.Errorf("getting APIToken: %w", err) + } else if token == nil { + return nil, nil + } + + return entAPITokenToBiz(token), nil +} + func (r *APITokenRepo) List(ctx context.Context, orgID uuid.UUID, includeRevoked bool) ([]*biz.APIToken, error) { query := r.data.db.APIToken.Query().Where(apitoken.OrganizationIDEQ(orgID)) if !includeRevoked { diff --git a/app/controlplane/internal/jwt/apitoken/apitoken.go b/app/controlplane/internal/jwt/apitoken/apitoken.go index 2bad971aa..47ecc557b 100644 --- a/app/controlplane/internal/jwt/apitoken/apitoken.go +++ b/app/controlplane/internal/jwt/apitoken/apitoken.go @@ -67,15 +67,12 @@ func NewBuilder(opts ...NewOpt) (*Builder, error) { } // it can both expire and being revoked, revocation is performed by checking the keyID against our revocation list -func (ra *Builder) GenerateJWT(orgID, keyID string, expiresAt *time.Time) (string, error) { - claims := CustomClaims{ - orgID, - jwt.RegisteredClaims{ - // Key identifier so we can check it's revocation status - ID: keyID, - Issuer: ra.issuer, - Audience: jwt.ClaimStrings{Audience}, - }, +func (ra *Builder) GenerateJWT(keyID string, expiresAt *time.Time) (string, error) { + claims := jwt.RegisteredClaims{ + // Key identifier so we can check it's revocation status + ID: keyID, + Issuer: ra.issuer, + Audience: jwt.ClaimStrings{Audience}, } // optional expiration value, i.e 30 days @@ -86,8 +83,3 @@ func (ra *Builder) GenerateJWT(orgID, keyID string, expiresAt *time.Time) (strin resultToken := jwt.NewWithClaims(SigningMethod, claims) return resultToken.SignedString([]byte(ra.hmacSecret)) } - -type CustomClaims struct { - OrgID string `json:"org_id"` - jwt.RegisteredClaims -} diff --git a/app/controlplane/internal/jwt/apitoken/apitoken_test.go b/app/controlplane/internal/jwt/apitoken/apitoken_test.go index ab58d52c9..eb7045b44 100644 --- a/app/controlplane/internal/jwt/apitoken/apitoken_test.go +++ b/app/controlplane/internal/jwt/apitoken/apitoken_test.go @@ -19,7 +19,7 @@ import ( "testing" "time" - "github.com/golang-jwt/jwt" + "github.com/golang-jwt/jwt/v4" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -76,19 +76,18 @@ func TestGenerateJWT(t *testing.T) { ) require.NoError(t, err) - token, err := b.GenerateJWT("org-id", "key-id", toPtrTime(time.Now().Add(1*time.Hour))) + token, err := b.GenerateJWT("key-id", toPtrTime(time.Now().Add(1*time.Hour))) assert.NoError(t, err) assert.NotEmpty(t, token) // Verify signature and check claims - claims := &CustomClaims{} + claims := &jwt.RegisteredClaims{} tokenInfo, err := jwt.ParseWithClaims(token, claims, func(_ *jwt.Token) (interface{}, error) { return []byte(hmacSecret), nil }) require.NoError(t, err) assert.True(t, tokenInfo.Valid) - assert.Equal(t, "org-id", claims.OrgID) assert.Equal(t, "key-id", claims.ID) assert.Equal(t, "my-issuer", claims.Issuer) assert.Contains(t, claims.Audience, Audience) diff --git a/app/controlplane/internal/server/grpc.go b/app/controlplane/internal/server/grpc.go index dd149021b..cffead322 100644 --- a/app/controlplane/internal/server/grpc.go +++ b/app/controlplane/internal/server/grpc.go @@ -46,12 +46,14 @@ import ( type Opts struct { // UseCases - UserUseCase *biz.UserUseCase - RobotAccountUseCase *biz.RobotAccountUseCase - CASBackendUseCase *biz.CASBackendUseCase - CASClientUseCase *biz.CASClientUseCase - IntegrationUseCase *biz.IntegrationUseCase - ReferrerUseCase *biz.ReferrerUseCase + UserUseCase *biz.UserUseCase + RobotAccountUseCase *biz.RobotAccountUseCase + CASBackendUseCase *biz.CASBackendUseCase + CASClientUseCase *biz.CASClientUseCase + IntegrationUseCase *biz.IntegrationUseCase + ReferrerUseCase *biz.ReferrerUseCase + APITokenUseCase *biz.APITokenUseCase + OrganizationUserCase *biz.OrganizationUseCase // Services WorkflowSvc *service.WorkflowService AuthSvc *service.AuthService @@ -68,6 +70,7 @@ type Opts struct { CASRedirectSvc *service.CASRedirectService OrgInvitationSvc *service.OrgInvitationService ReferrerSvc *service.ReferrerService + APITokenSvc *service.APITokenService // Utils Logger log.Logger ServerConfig *conf.Server @@ -126,6 +129,7 @@ func NewGRPCServer(opts *Opts) (*grpc.Server, error) { v1.RegisterCASRedirectServiceServer(srv, opts.CASRedirectSvc) v1.RegisterOrgInvitationServiceServer(srv, opts.OrgInvitationSvc) v1.RegisterReferrerServiceServer(srv, opts.ReferrerSvc) + v1.RegisterAPITokenServiceServer(srv, opts.APITokenSvc) // Register Prometheus metrics grpc_prometheus.Register(srv.Server) @@ -150,15 +154,17 @@ func craftMiddleware(opts *Opts) []middleware.Middleware { middlewares = append(middlewares, // If we require a logged in user we selector.Server( - // 1 - Extract the currentUser from the JWT + // 1 - Extract the currentUser/API token from the JWT + // NOTE: this works because both currentUser and API tokens JWT use the same signing method and secret jwtMiddleware.Server(func(token *jwt.Token) (interface{}, error) { return []byte(opts.AuthConfig.GeneratedJwsHmacSecret), nil }, jwtMiddleware.WithSigningMethod(user.SigningMethod), - jwtMiddleware.WithClaims(func() jwt.Claims { return &user.CustomClaims{} }), ), - // 1 - Set its user and organization + // 2.a - Set its user and organization usercontext.WithCurrentUserAndOrgMiddleware(opts.UserUseCase, logHelper), + // 2.b - Set its API token and organization as alternative to the user + usercontext.WithCurrentAPITokenAndOrgMiddleware(opts.APITokenUseCase, opts.OrganizationUserCase, logHelper), // 3 - Make sure its account is fully functional selector.Server( usercontext.CheckUserInAllowList(opts.AuthConfig.AllowList), diff --git a/app/controlplane/internal/service/apitoken.go b/app/controlplane/internal/service/apitoken.go new file mode 100644 index 000000000..b1d0d939d --- /dev/null +++ b/app/controlplane/internal/service/apitoken.go @@ -0,0 +1,137 @@ +// +// Copyright 2023 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. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package service + +import ( + "context" + "time" + + pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1" + "github.com/chainloop-dev/chainloop/app/controlplane/internal/biz" + sl "github.com/chainloop-dev/chainloop/internal/servicelogger" + "github.com/go-kratos/kratos/v2/errors" + "google.golang.org/protobuf/types/known/timestamppb" +) + +type APITokenService struct { + pb.UnimplementedAPITokenServiceServer + *service + + APITokenUseCase *biz.APITokenUseCase +} + +func NewAPITokenService(uc *biz.APITokenUseCase, opts ...NewOpt) *APITokenService { + return &APITokenService{ + service: newService(opts...), + APITokenUseCase: uc, + } +} + +func (s *APITokenService) Create(ctx context.Context, req *pb.APITokenServiceCreateRequest) (*pb.APITokenServiceCreateResponse, error) { + currentOrg, err := requireCurrentOrg(ctx) + if err != nil { + return nil, err + } + + // This is a API operation that requires actual user to be logged in not API token + _, err = requireCurrentUser(ctx) + if err != nil { + return nil, err + } + + var expiresIn *time.Duration + if req.ExpiresIn != nil { + expiresIn = new(time.Duration) + *expiresIn = req.ExpiresIn.AsDuration() + } + + token, err := s.APITokenUseCase.Create(ctx, req.Description, expiresIn, currentOrg.ID) + if err != nil && biz.IsNotFound(err) { + return nil, errors.NotFound("not found", err.Error()) + } else if err != nil { + return nil, sl.LogAndMaskErr(err, s.log) + } + + return &pb.APITokenServiceCreateResponse{ + Result: &pb.APITokenServiceCreateResponse_APITokenFull{ + Item: apiTokenBizToPb(token), + Jwt: token.JWT, + }, + }, nil +} + +func (s *APITokenService) List(ctx context.Context, req *pb.APITokenServiceListRequest) (*pb.APITokenServiceListResponse, error) { + currentOrg, err := requireCurrentOrg(ctx) + if err != nil { + return nil, err + } + + // This is a API operation that requires actual user to be logged in not API token + _, err = requireCurrentUser(ctx) + if err != nil { + return nil, err + } + + tokens, err := s.APITokenUseCase.List(ctx, currentOrg.ID, req.IncludeRevoked) + if err != nil && biz.IsNotFound(err) { + return nil, errors.NotFound("not found", err.Error()) + } else if err != nil { + return nil, sl.LogAndMaskErr(err, s.log) + } + + result := make([]*pb.APITokenItem, 0, len(tokens)) + for _, p := range tokens { + result = append(result, apiTokenBizToPb(p)) + } + + return &pb.APITokenServiceListResponse{Result: result}, nil +} + +func (s *APITokenService) Revoke(ctx context.Context, req *pb.APITokenServiceRevokeRequest) (*pb.APITokenServiceRevokeResponse, error) { + currentOrg, err := requireCurrentOrg(ctx) + if err != nil { + return nil, err + } + + // This is a API operation that requires actual user to be logged in not API token + _, err = requireCurrentUser(ctx) + if err != nil { + return nil, err + } + + if err := s.APITokenUseCase.Revoke(ctx, currentOrg.ID, req.Id); err != nil { + return nil, sl.LogAndMaskErr(err, s.log) + } + + return &pb.APITokenServiceRevokeResponse{}, nil +} + +func apiTokenBizToPb(in *biz.APIToken) *pb.APITokenItem { + res := &pb.APITokenItem{ + Id: in.ID.String(), Description: in.Description, OrganizationId: in.OrganizationID.String(), + CreatedAt: timestamppb.New(*in.CreatedAt), + } + + if in.ExpiresAt != nil { + res.ExpiresAt = timestamppb.New(*in.ExpiresAt) + } + + if in.RevokedAt != nil { + res.RevokedAt = timestamppb.New(*in.RevokedAt) + } + + return res +} diff --git a/app/controlplane/internal/service/auth.go b/app/controlplane/internal/service/auth.go index fd10f6f17..d1680e2b2 100644 --- a/app/controlplane/internal/service/auth.go +++ b/app/controlplane/internal/service/auth.go @@ -359,7 +359,7 @@ func setOauthCookie(w http.ResponseWriter, name, value string) { // DeleteAccount deletes an account func (svc *AuthService) DeleteAccount(ctx context.Context, _ *pb.AuthServiceDeleteAccountRequest) (*pb.AuthServiceDeleteAccountResponse, error) { - user, _, err := loadCurrentUserAndOrg(ctx) + user, err := requireCurrentUser(ctx) if err != nil { return nil, err } diff --git a/app/controlplane/internal/service/casbackend.go b/app/controlplane/internal/service/casbackend.go index fb742d165..d5a5063f2 100644 --- a/app/controlplane/internal/service/casbackend.go +++ b/app/controlplane/internal/service/casbackend.go @@ -45,7 +45,7 @@ func NewCASBackendService(uc *biz.CASBackendUseCase, providers backend.Providers } func (s *CASBackendService) List(ctx context.Context, _ *pb.CASBackendServiceListRequest) (*pb.CASBackendServiceListResponse, error) { - _, currentOrg, err := loadCurrentUserAndOrg(ctx) + currentOrg, err := requireCurrentOrg(ctx) if err != nil { return nil, err } @@ -64,7 +64,7 @@ func (s *CASBackendService) List(ctx context.Context, _ *pb.CASBackendServiceLis } func (s *CASBackendService) Create(ctx context.Context, req *pb.CASBackendServiceCreateRequest) (*pb.CASBackendServiceCreateResponse, error) { - _, currentOrg, err := loadCurrentUserAndOrg(ctx) + currentOrg, err := requireCurrentOrg(ctx) if err != nil { return nil, err } @@ -95,7 +95,7 @@ func (s *CASBackendService) Create(ctx context.Context, req *pb.CASBackendServic } func (s *CASBackendService) Update(ctx context.Context, req *pb.CASBackendServiceUpdateRequest) (*pb.CASBackendServiceUpdateResponse, error) { - _, currentOrg, err := loadCurrentUserAndOrg(ctx) + currentOrg, err := requireCurrentOrg(ctx) if err != nil { return nil, err } @@ -138,7 +138,7 @@ func (s *CASBackendService) Update(ctx context.Context, req *pb.CASBackendServic // Delete the CAS backend func (s *CASBackendService) Delete(ctx context.Context, req *pb.CASBackendServiceDeleteRequest) (*pb.CASBackendServiceDeleteResponse, error) { - _, currentOrg, err := loadCurrentUserAndOrg(ctx) + currentOrg, err := requireCurrentOrg(ctx) if err != nil { return nil, err } diff --git a/app/controlplane/internal/service/cascredential.go b/app/controlplane/internal/service/cascredential.go index e8ea852a0..701e3bb1f 100644 --- a/app/controlplane/internal/service/cascredential.go +++ b/app/controlplane/internal/service/cascredential.go @@ -48,7 +48,13 @@ func NewCASCredentialsService(casUC *biz.CASCredentialsUseCase, casmUC *biz.CASM // Get will generate temporary credentials to be used against the CAS service for the current organization func (s *CASCredentialsService) Get(ctx context.Context, req *pb.CASCredentialsServiceGetRequest) (*pb.CASCredentialsServiceGetResponse, error) { - currentUser, currentOrg, err := loadCurrentUserAndOrg(ctx) + // TODO: Add support API-Token-based authentication + currentUser, err := requireCurrentUser(ctx) + if err != nil { + return nil, err + } + + currentOrg, err := requireCurrentOrg(ctx) if err != nil { return nil, err } diff --git a/app/controlplane/internal/service/casredirect.go b/app/controlplane/internal/service/casredirect.go index 9235b483c..01ea9f296 100644 --- a/app/controlplane/internal/service/casredirect.go +++ b/app/controlplane/internal/service/casredirect.go @@ -68,7 +68,8 @@ func NewCASRedirectService(casmUC *biz.CASMappingUseCase, casCredsUC *biz.CASCre // The URL includes a JWT token that is used to authenticate the request, this token has all the information required to validate the request // The result would look like "https://cas.chainloop.dev/download/sha256:[DIGEST]?t=tokenJWT func (s *CASRedirectService) GetDownloadURL(ctx context.Context, req *pb.GetDownloadURLRequest) (*pb.GetDownloadURLResponse, error) { - currentUser, _, err := loadCurrentUserAndOrg(ctx) + // TODO: Add support API-Token-based authentication + currentUser, err := requireCurrentUser(ctx) if err != nil { return nil, err } diff --git a/app/controlplane/internal/service/context.go b/app/controlplane/internal/service/context.go index bcda039de..2f96974bb 100644 --- a/app/controlplane/internal/service/context.go +++ b/app/controlplane/internal/service/context.go @@ -39,7 +39,12 @@ func NewContextService(repoUC *biz.CASBackendUseCase, opts ...NewOpt) *ContextSe } func (s *ContextService) Current(ctx context.Context, _ *pb.ContextServiceCurrentRequest) (*pb.ContextServiceCurrentResponse, error) { - currentUser, currentOrg, err := loadCurrentUserAndOrg(ctx) + currentUser, err := requireCurrentUser(ctx) + if err != nil { + return nil, err + } + + currentOrg, err := requireCurrentOrg(ctx) if err != nil { return nil, err } diff --git a/app/controlplane/internal/service/integration.go b/app/controlplane/internal/service/integration.go index 2e83e3898..9567d992a 100644 --- a/app/controlplane/internal/service/integration.go +++ b/app/controlplane/internal/service/integration.go @@ -76,7 +76,7 @@ func (s *IntegrationsService) ListAvailable(_ context.Context, _ *pb.Integration } func (s *IntegrationsService) Register(ctx context.Context, req *pb.IntegrationsServiceRegisterRequest) (*pb.IntegrationsServiceRegisterResponse, error) { - _, org, err := loadCurrentUserAndOrg(ctx) + org, err := requireCurrentOrg(ctx) if err != nil { return nil, err } @@ -102,7 +102,7 @@ func (s *IntegrationsService) Register(ctx context.Context, req *pb.Integrations } func (s *IntegrationsService) Attach(ctx context.Context, req *pb.IntegrationsServiceAttachRequest) (*pb.IntegrationsServiceAttachResponse, error) { - _, org, err := loadCurrentUserAndOrg(ctx) + org, err := requireCurrentOrg(ctx) if err != nil { return nil, err } @@ -146,7 +146,7 @@ func (s *IntegrationsService) Attach(ctx context.Context, req *pb.IntegrationsSe } func (s *IntegrationsService) ListRegistrations(ctx context.Context, _ *pb.IntegrationsServiceListRegistrationsRequest) (*pb.IntegrationsServiceListRegistrationsResponse, error) { - _, org, err := loadCurrentUserAndOrg(ctx) + org, err := requireCurrentOrg(ctx) if err != nil { return nil, err } @@ -165,7 +165,7 @@ func (s *IntegrationsService) ListRegistrations(ctx context.Context, _ *pb.Integ } func (s *IntegrationsService) DescribeRegistration(ctx context.Context, req *pb.IntegrationsServiceDescribeRegistrationRequest) (*pb.IntegrationsServiceDescribeRegistrationResponse, error) { - _, org, err := loadCurrentUserAndOrg(ctx) + org, err := requireCurrentOrg(ctx) if err != nil { return nil, err } @@ -181,7 +181,7 @@ func (s *IntegrationsService) DescribeRegistration(ctx context.Context, req *pb. } func (s *IntegrationsService) Deregister(ctx context.Context, req *pb.IntegrationsServiceDeregisterRequest) (*pb.IntegrationsServiceDeregisterResponse, error) { - _, org, err := loadCurrentUserAndOrg(ctx) + org, err := requireCurrentOrg(ctx) if err != nil { return nil, err } @@ -197,7 +197,7 @@ func (s *IntegrationsService) Deregister(ctx context.Context, req *pb.Integratio } func (s *IntegrationsService) ListAttachments(ctx context.Context, req *pb.ListAttachmentsRequest) (*pb.ListAttachmentsResponse, error) { - _, org, err := loadCurrentUserAndOrg(ctx) + org, err := requireCurrentOrg(ctx) if err != nil { return nil, err } @@ -220,7 +220,7 @@ func (s *IntegrationsService) ListAttachments(ctx context.Context, req *pb.ListA } func (s *IntegrationsService) Detach(ctx context.Context, req *pb.IntegrationsServiceDetachRequest) (*pb.IntegrationsServiceDetachResponse, error) { - _, org, err := loadCurrentUserAndOrg(ctx) + org, err := requireCurrentOrg(ctx) if err != nil { return nil, err } diff --git a/app/controlplane/internal/service/organization.go b/app/controlplane/internal/service/organization.go index 22d608d53..c052812f3 100644 --- a/app/controlplane/internal/service/organization.go +++ b/app/controlplane/internal/service/organization.go @@ -43,7 +43,7 @@ func NewOrganizationService(muc *biz.MembershipUseCase, ouc *biz.OrganizationUse } func (s *OrganizationService) ListMemberships(ctx context.Context, _ *pb.OrganizationServiceListMembershipsRequest) (*pb.OrganizationServiceListMembershipsResponse, error) { - currentUser, _, err := loadCurrentUserAndOrg(ctx) + currentUser, err := requireCurrentUser(ctx) if err != nil { return nil, err } @@ -65,7 +65,7 @@ func (s *OrganizationService) ListMemberships(ctx context.Context, _ *pb.Organiz // Create persists an organization with a given name and associate it to the current user. func (s *OrganizationService) Create(ctx context.Context, req *pb.OrganizationServiceCreateRequest) (*pb.OrganizationServiceCreateResponse, error) { - currentUser, _, err := loadCurrentUserAndOrg(ctx) + currentUser, err := requireCurrentUser(ctx) if err != nil { return nil, err } @@ -83,7 +83,7 @@ func (s *OrganizationService) Create(ctx context.Context, req *pb.OrganizationSe } func (s *OrganizationService) Update(ctx context.Context, req *pb.OrganizationServiceUpdateRequest) (*pb.OrganizationServiceUpdateResponse, error) { - currentUser, _, err := loadCurrentUserAndOrg(ctx) + currentUser, err := requireCurrentUser(ctx) if err != nil { return nil, err } @@ -97,7 +97,7 @@ func (s *OrganizationService) Update(ctx context.Context, req *pb.OrganizationSe } func (s *OrganizationService) SetCurrentMembership(ctx context.Context, req *pb.SetCurrentMembershipRequest) (*pb.SetCurrentMembershipResponse, error) { - currentUser, _, err := loadCurrentUserAndOrg(ctx) + currentUser, err := requireCurrentUser(ctx) if err != nil { return nil, err } @@ -113,7 +113,7 @@ func (s *OrganizationService) SetCurrentMembership(ctx context.Context, req *pb. } func (s *OrganizationService) DeleteMembership(ctx context.Context, req *pb.DeleteMembershipRequest) (*pb.DeleteMembershipResponse, error) { - currentUser, _, err := loadCurrentUserAndOrg(ctx) + currentUser, err := requireCurrentUser(ctx) if err != nil { return nil, err } diff --git a/app/controlplane/internal/service/orginvitation.go b/app/controlplane/internal/service/orginvitation.go index 6ec6733a1..dca29bd84 100644 --- a/app/controlplane/internal/service/orginvitation.go +++ b/app/controlplane/internal/service/orginvitation.go @@ -38,7 +38,7 @@ func NewOrgInvitationService(uc *biz.OrgInvitationUseCase, opts ...NewOpt) *OrgI } func (s *OrgInvitationService) Create(ctx context.Context, req *pb.OrgInvitationServiceCreateRequest) (*pb.OrgInvitationServiceCreateResponse, error) { - user, _, err := loadCurrentUserAndOrg(ctx) + user, err := requireCurrentUser(ctx) if err != nil { return nil, err } @@ -53,7 +53,7 @@ func (s *OrgInvitationService) Create(ctx context.Context, req *pb.OrgInvitation } func (s *OrgInvitationService) Revoke(ctx context.Context, req *pb.OrgInvitationServiceRevokeRequest) (*pb.OrgInvitationServiceRevokeResponse, error) { - user, _, err := loadCurrentUserAndOrg(ctx) + user, err := requireCurrentUser(ctx) if err != nil { return nil, err } @@ -66,7 +66,7 @@ func (s *OrgInvitationService) Revoke(ctx context.Context, req *pb.OrgInvitation } func (s *OrgInvitationService) ListSent(ctx context.Context, _ *pb.OrgInvitationServiceListSentRequest) (*pb.OrgInvitationServiceListSentResponse, error) { - user, _, err := loadCurrentUserAndOrg(ctx) + user, err := requireCurrentUser(ctx) if err != nil { return nil, err } diff --git a/app/controlplane/internal/service/orgmetric.go b/app/controlplane/internal/service/orgmetric.go index ee1e246c9..a608fd645 100644 --- a/app/controlplane/internal/service/orgmetric.go +++ b/app/controlplane/internal/service/orgmetric.go @@ -38,7 +38,7 @@ func NewOrgMetricsService(uc *biz.OrgMetricsUseCase, opts ...NewOpt) *OrgMetrics } func (s *OrgMetricsService) Totals(ctx context.Context, req *pb.OrgMetricsServiceTotalsRequest) (*pb.OrgMetricsServiceTotalsResponse, error) { - _, currentOrg, err := loadCurrentUserAndOrg(ctx) + currentOrg, err := requireCurrentOrg(ctx) if err != nil { return nil, err } @@ -67,7 +67,7 @@ func (s *OrgMetricsService) Totals(ctx context.Context, req *pb.OrgMetricsServic } func (s *OrgMetricsService) TopWorkflowsByRunsCount(ctx context.Context, req *pb.TopWorkflowsByRunsCountRequest) (*pb.TopWorkflowsByRunsCountResponse, error) { - _, currentOrg, err := loadCurrentUserAndOrg(ctx) + currentOrg, err := requireCurrentOrg(ctx) if err != nil { return nil, err } diff --git a/app/controlplane/internal/service/referrer.go b/app/controlplane/internal/service/referrer.go index 0edac4c32..01f0e8121 100644 --- a/app/controlplane/internal/service/referrer.go +++ b/app/controlplane/internal/service/referrer.go @@ -38,7 +38,7 @@ func NewReferrerService(uc *biz.ReferrerUseCase, opts ...NewOpt) *ReferrerServic } func (s *ReferrerService) DiscoverPrivate(ctx context.Context, req *pb.ReferrerServiceDiscoverPrivateRequest) (*pb.ReferrerServiceDiscoverPrivateResponse, error) { - currentUser, _, err := loadCurrentUserAndOrg(ctx) + currentUser, err := requireCurrentUser(ctx) if err != nil { return nil, err } diff --git a/app/controlplane/internal/service/robotaccount.go b/app/controlplane/internal/service/robotaccount.go index 70f3daf45..327420f9b 100644 --- a/app/controlplane/internal/service/robotaccount.go +++ b/app/controlplane/internal/service/robotaccount.go @@ -40,7 +40,7 @@ func NewRobotAccountService(uc *biz.RobotAccountUseCase, opts ...NewOpt) *RobotA } func (s *RobotAccountService) Create(ctx context.Context, req *pb.RobotAccountServiceCreateRequest) (*pb.RobotAccountServiceCreateResponse, error) { - _, currentOrg, err := loadCurrentUserAndOrg(ctx) + currentOrg, err := requireCurrentOrg(ctx) if err != nil { return nil, err } @@ -60,7 +60,7 @@ func (s *RobotAccountService) Create(ctx context.Context, req *pb.RobotAccountSe } func (s *RobotAccountService) List(ctx context.Context, req *pb.RobotAccountServiceListRequest) (*pb.RobotAccountServiceListResponse, error) { - _, currentOrg, err := loadCurrentUserAndOrg(ctx) + currentOrg, err := requireCurrentOrg(ctx) if err != nil { return nil, err } @@ -91,7 +91,7 @@ func (s *RobotAccountService) List(ctx context.Context, req *pb.RobotAccountServ } func (s *RobotAccountService) Revoke(ctx context.Context, req *pb.RobotAccountServiceRevokeRequest) (*pb.RobotAccountServiceRevokeResponse, error) { - _, currentOrg, err := loadCurrentUserAndOrg(ctx) + currentOrg, err := requireCurrentOrg(ctx) if err != nil { return nil, err } diff --git a/app/controlplane/internal/service/service.go b/app/controlplane/internal/service/service.go index 53c121c9f..f945a490e 100644 --- a/app/controlplane/internal/service/service.go +++ b/app/controlplane/internal/service/service.go @@ -45,17 +45,27 @@ var ProviderSet = wire.NewSet( NewOrganizationService, NewOrgInvitationService, NewReferrerService, + NewAPITokenService, wire.Struct(new(NewWorkflowRunServiceOpts), "*"), wire.Struct(new(NewAttestationServiceOpts), "*"), ) -func loadCurrentUserAndOrg(ctx context.Context) (*usercontext.User, *usercontext.Org, error) { - currentUser, currentOrg := usercontext.CurrentUser(ctx), usercontext.CurrentOrg(ctx) - if currentUser == nil || currentOrg == nil { - return nil, nil, errors.NotFound("not found", "logged in user and org not found") +func requireCurrentUser(ctx context.Context) (*usercontext.User, error) { + currentUser := usercontext.CurrentUser(ctx) + if currentUser == nil { + return nil, errors.NotFound("not found", "logged in user") } - return currentUser, currentOrg, nil + return currentUser, nil +} + +func requireCurrentOrg(ctx context.Context) (*usercontext.Org, error) { + currentOrg := usercontext.CurrentOrg(ctx) + if currentOrg == nil { + return nil, errors.NotFound("not found", "current organization not set") + } + + return currentOrg, nil } func newService(opts ...NewOpt) *service { diff --git a/app/controlplane/internal/service/workflow.go b/app/controlplane/internal/service/workflow.go index 985c1a646..5fbcda4bf 100644 --- a/app/controlplane/internal/service/workflow.go +++ b/app/controlplane/internal/service/workflow.go @@ -41,7 +41,7 @@ func NewWorkflowService(uc *biz.WorkflowUseCase, opts ...NewOpt) *WorkflowServic } func (s *WorkflowService) Create(ctx context.Context, req *pb.WorkflowServiceCreateRequest) (*pb.WorkflowServiceCreateResponse, error) { - _, currentOrg, err := loadCurrentUserAndOrg(ctx) + currentOrg, err := requireCurrentOrg(ctx) if err != nil { return nil, err } @@ -68,7 +68,7 @@ func (s *WorkflowService) Create(ctx context.Context, req *pb.WorkflowServiceCre } func (s *WorkflowService) Update(ctx context.Context, req *pb.WorkflowServiceUpdateRequest) (*pb.WorkflowServiceUpdateResponse, error) { - _, currentOrg, err := loadCurrentUserAndOrg(ctx) + currentOrg, err := requireCurrentOrg(ctx) if err != nil { return nil, err } @@ -94,7 +94,7 @@ func (s *WorkflowService) Update(ctx context.Context, req *pb.WorkflowServiceUpd } func (s *WorkflowService) List(ctx context.Context, _ *pb.WorkflowServiceListRequest) (*pb.WorkflowServiceListResponse, error) { - _, currentOrg, err := loadCurrentUserAndOrg(ctx) + currentOrg, err := requireCurrentOrg(ctx) if err != nil { return nil, err } @@ -114,7 +114,7 @@ func (s *WorkflowService) List(ctx context.Context, _ *pb.WorkflowServiceListReq } func (s *WorkflowService) Delete(ctx context.Context, req *pb.WorkflowServiceDeleteRequest) (*pb.WorkflowServiceDeleteResponse, error) { - _, currentOrg, err := loadCurrentUserAndOrg(ctx) + currentOrg, err := requireCurrentOrg(ctx) if err != nil { return nil, err } diff --git a/app/controlplane/internal/service/workflowcontract.go b/app/controlplane/internal/service/workflowcontract.go index 3c4f3bb35..d70c0220b 100644 --- a/app/controlplane/internal/service/workflowcontract.go +++ b/app/controlplane/internal/service/workflowcontract.go @@ -40,7 +40,7 @@ func NewWorkflowSchemaService(uc *biz.WorkflowContractUseCase, opts ...NewOpt) * } func (s *WorkflowContractService) List(ctx context.Context, _ *pb.WorkflowContractServiceListRequest) (*pb.WorkflowContractServiceListResponse, error) { - _, currentOrg, err := loadCurrentUserAndOrg(ctx) + currentOrg, err := requireCurrentOrg(ctx) if err != nil { return nil, err } @@ -59,7 +59,7 @@ func (s *WorkflowContractService) List(ctx context.Context, _ *pb.WorkflowContra } func (s *WorkflowContractService) Describe(ctx context.Context, req *pb.WorkflowContractServiceDescribeRequest) (*pb.WorkflowContractServiceDescribeResponse, error) { - _, currentOrg, err := loadCurrentUserAndOrg(ctx) + currentOrg, err := requireCurrentOrg(ctx) if err != nil { return nil, err } @@ -80,7 +80,7 @@ func (s *WorkflowContractService) Describe(ctx context.Context, req *pb.Workflow } func (s *WorkflowContractService) Create(ctx context.Context, req *pb.WorkflowContractServiceCreateRequest) (*pb.WorkflowContractServiceCreateResponse, error) { - _, currentOrg, err := loadCurrentUserAndOrg(ctx) + currentOrg, err := requireCurrentOrg(ctx) if err != nil { return nil, err } @@ -95,7 +95,7 @@ func (s *WorkflowContractService) Create(ctx context.Context, req *pb.WorkflowCo } func (s *WorkflowContractService) Update(ctx context.Context, req *pb.WorkflowContractServiceUpdateRequest) (*pb.WorkflowContractServiceUpdateResponse, error) { - _, currentOrg, err := loadCurrentUserAndOrg(ctx) + currentOrg, err := requireCurrentOrg(ctx) if err != nil { return nil, err } @@ -117,7 +117,7 @@ func (s *WorkflowContractService) Update(ctx context.Context, req *pb.WorkflowCo } func (s *WorkflowContractService) Delete(ctx context.Context, req *pb.WorkflowContractServiceDeleteRequest) (*pb.WorkflowContractServiceDeleteResponse, error) { - _, currentOrg, err := loadCurrentUserAndOrg(ctx) + currentOrg, err := requireCurrentOrg(ctx) if err != nil { return nil, err } diff --git a/app/controlplane/internal/service/workflowrun.go b/app/controlplane/internal/service/workflowrun.go index 23ba1e38d..1d1d9cfff 100644 --- a/app/controlplane/internal/service/workflowrun.go +++ b/app/controlplane/internal/service/workflowrun.go @@ -57,7 +57,7 @@ func NewWorkflowRunService(opts *NewWorkflowRunServiceOpts) *WorkflowRunService } func (s *WorkflowRunService) List(ctx context.Context, req *pb.WorkflowRunServiceListRequest) (*pb.WorkflowRunServiceListResponse, error) { - _, currentOrg, err := loadCurrentUserAndOrg(ctx) + currentOrg, err := requireCurrentOrg(ctx) if err != nil { return nil, err } @@ -99,7 +99,7 @@ func bizCursorToPb(cursor string) *pb.PaginationResponse { const workflowRunEntity = "Workflow Run" func (s *WorkflowRunService) View(ctx context.Context, req *pb.WorkflowRunServiceViewRequest) (*pb.WorkflowRunServiceViewResponse, error) { - _, currentOrg, err := loadCurrentUserAndOrg(ctx) + currentOrg, err := requireCurrentOrg(ctx) if err != nil { return nil, err } diff --git a/app/controlplane/internal/usercontext/apitoken_middleware.go b/app/controlplane/internal/usercontext/apitoken_middleware.go new file mode 100644 index 000000000..b53f9f5a7 --- /dev/null +++ b/app/controlplane/internal/usercontext/apitoken_middleware.go @@ -0,0 +1,108 @@ +// +// Copyright 2023 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. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package usercontext + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/go-kratos/kratos/v2/log" + "github.com/golang-jwt/jwt/v4" + + "github.com/chainloop-dev/chainloop/app/controlplane/internal/biz" + "github.com/chainloop-dev/chainloop/app/controlplane/internal/jwt/apitoken" + "github.com/go-kratos/kratos/v2/middleware" + jwtMiddleware "github.com/go-kratos/kratos/v2/middleware/auth/jwt" +) + +type APIToken struct { + ID string + CreatedAt *time.Time +} + +func withCurrentAPIToken(ctx context.Context, token *APIToken) context.Context { + return context.WithValue(ctx, currentAPITokenCtxKey{}, token) +} + +type currentAPITokenCtxKey struct{} + +// Middleware that injects the API-Token + organization to the context +func WithCurrentAPITokenAndOrgMiddleware(apiTokenUC *biz.APITokenUseCase, orgUC *biz.OrganizationUseCase, logger *log.Helper) middleware.Middleware { + return func(handler middleware.Handler) middleware.Handler { + return func(ctx context.Context, req interface{}) (interface{}, error) { + rawClaims, ok := jwtMiddleware.FromContext(ctx) + // If not found means that there is no currentUser set in the context + if !ok { + logger.Warn("couldn't extract org/user, JWT parser middleware not running before this one?") + return nil, errors.New("can't extract JWT info from the context") + } + + genericClaims, ok := rawClaims.(jwt.MapClaims) + if !ok { + return nil, errors.New("error mapping the claims") + } + + // We've received an API-token + if genericClaims.VerifyAudience(apitoken.Audience, true) { + var err error + tokenID := genericClaims["jti"].(string) + if tokenID == "" { + return nil, errors.New("error mapping the API-token claims") + } + + ctx, err = setCurrentAPIToken(ctx, apiTokenUC, orgUC, tokenID) + if err != nil { + return nil, fmt.Errorf("error setting current org and user: %w", err) + } + } + + return handler(ctx, req) + } + } +} + +// Set the current organization and API-Token in the context +func setCurrentAPIToken(ctx context.Context, apiTokenUC *biz.APITokenUseCase, orgUC *biz.OrganizationUseCase, tokenID string) (context.Context, error) { + if tokenID == "" { + return nil, errors.New("error retrieving the key ID from the API token") + } + + // Check that the token exists and is not revoked + token, err := apiTokenUC.FindByID(ctx, tokenID) + if err != nil { + return nil, fmt.Errorf("error retrieving the API token: %w", err) + } else if token == nil { + return nil, errors.New("API token not found") + } + + if token.RevokedAt != nil { + return nil, errors.New("API token revoked") + } + + // Find the associated organization + org, err := orgUC.FindByID(ctx, token.OrganizationID.String()) + if err != nil { + return nil, fmt.Errorf("error retrieving the organization: %w", err) + } else if org == nil { + return nil, errors.New("organization not found") + } + + ctx = withCurrentOrg(ctx, &Org{Name: org.Name, ID: org.ID, CreatedAt: org.CreatedAt}) + ctx = withCurrentAPIToken(ctx, &APIToken{ID: token.ID.String(), CreatedAt: token.CreatedAt}) + return ctx, nil +} diff --git a/app/controlplane/internal/usercontext/userorg_middleware.go b/app/controlplane/internal/usercontext/currentuser_middleware.go similarity index 78% rename from app/controlplane/internal/usercontext/userorg_middleware.go rename to app/controlplane/internal/usercontext/currentuser_middleware.go index 51cee7419..49fc22e13 100644 --- a/app/controlplane/internal/usercontext/userorg_middleware.go +++ b/app/controlplane/internal/usercontext/currentuser_middleware.go @@ -18,9 +18,11 @@ package usercontext import ( "context" "errors" + "fmt" "time" "github.com/go-kratos/kratos/v2/log" + "github.com/golang-jwt/jwt/v4" "github.com/chainloop-dev/chainloop/app/controlplane/internal/biz" "github.com/chainloop-dev/chainloop/app/controlplane/internal/jwt/user" @@ -70,7 +72,7 @@ func CurrentOrg(ctx context.Context) *Org { type currentUserCtxKey struct{} type currentOrgCtxKey struct{} -// Middleware that injects the current user to the context +// Middleware that injects the current user / API-token entry + organization to the context func WithCurrentUserAndOrgMiddleware(userUseCase biz.UserOrgFinder, logger *log.Helper) middleware.Middleware { return func(handler middleware.Handler) middleware.Handler { return func(ctx context.Context, req interface{}) (interface{}, error) { @@ -78,28 +80,27 @@ func WithCurrentUserAndOrgMiddleware(userUseCase biz.UserOrgFinder, logger *log. // If not found means that there is no currentUser set in the context if !ok { logger.Warn("couldn't extract org/user, JWT parser middleware not running before this one?") - return nil, errors.New("can't extract info from the token") + return nil, errors.New("can't extract JWT info from the context") } - customClaims, ok := rawClaims.(*user.CustomClaims) + genericClaims, ok := rawClaims.(jwt.MapClaims) if !ok { return nil, errors.New("error mapping the claims") } - // Do not accept tokens that are crafted for a different audience in this system - if !customClaims.VerifyAudience(user.Audience, true) { - return nil, errors.New("unexpected token, invalid audience") - } - - userID := customClaims.UserID - if userID == "" { - return nil, errors.New("error retrieving the user information from the auth token") - } - - // Find user in DB and set it as currentUser - ctx, err := setCurrentOrgAndUser(ctx, userUseCase, userID, logger) - if err != nil { - return nil, err + // Check wether the token is for a user or an API-token and handle accordingly + // We've received a token for a user + if genericClaims.VerifyAudience(user.Audience, true) { + userID := genericClaims["user_id"].(string) + if userID == "" { + return nil, errors.New("error mapping the user claims") + } + + var err error + ctx, err = setCurrentUser(ctx, userUseCase, userID, logger) + if err != nil { + return nil, fmt.Errorf("error setting current org and user: %w", err) + } } return handler(ctx, req) @@ -108,7 +109,7 @@ func WithCurrentUserAndOrgMiddleware(userUseCase biz.UserOrgFinder, logger *log. } // Find organization and user in DB -func setCurrentOrgAndUser(ctx context.Context, userUC biz.UserOrgFinder, userID string, logger *log.Helper) (context.Context, error) { +func setCurrentUser(ctx context.Context, userUC biz.UserOrgFinder, userID string, logger *log.Helper) (context.Context, error) { u, err := userUC.FindByID(ctx, userID) if err != nil { return nil, err diff --git a/app/controlplane/internal/usercontext/userorg_middleware_test.go b/app/controlplane/internal/usercontext/currentuser_middleware_test.go similarity index 100% rename from app/controlplane/internal/usercontext/userorg_middleware_test.go rename to app/controlplane/internal/usercontext/currentuser_middleware_test.go From d05c33dd1bc0c024a5618350494336334f805a3e Mon Sep 17 00:00:00 2001 From: Miguel Martinez Trivino Date: Tue, 12 Dec 2023 13:59:00 +0100 Subject: [PATCH 2/8] feat: API token support Signed-off-by: Miguel Martinez Trivino --- app/cli/cmd/root.go | 69 ++++++++++++++----- app/controlplane/internal/service/apitoken.go | 3 + app/controlplane/internal/service/context.go | 30 ++++++-- app/controlplane/internal/service/service.go | 9 +++ .../usercontext/apitoken_middleware.go | 10 +++ .../usercontext/currentuser_middleware.go | 2 + 6 files changed, 97 insertions(+), 26 deletions(-) diff --git a/app/cli/cmd/root.go b/app/cli/cmd/root.go index c7540f44e..bd55a8001 100644 --- a/app/cli/cmd/root.go +++ b/app/cli/cmd/root.go @@ -39,10 +39,15 @@ var ( logger zerolog.Logger defaultCPAPI = "api.cp.chainloop.dev:443" defaultCASAPI = "api.cas.chainloop.dev:443" + apiToken string ) -const useWorkflowRobotAccount = "withWorkflowRobotAccount" -const appName = "chainloop" +const ( + useWorkflowRobotAccount = "withWorkflowRobotAccount" + appName = "chainloop" + //nolint:gosec + apiTokenEnvVarName = "CHAINLOOP_API_TOKEN" +) func NewRootCmd(l zerolog.Logger) *cobra.Command { rootCmd := &cobra.Command{ @@ -51,33 +56,24 @@ func NewRootCmd(l zerolog.Logger) *cobra.Command { SilenceErrors: true, SilenceUsage: true, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + logger.Debug().Str("path", viper.ConfigFileUsed()).Msg("using config file") + var err error logger, err = initLogger(l) if err != nil { return err } - logger.Debug().Str("path", viper.ConfigFileUsed()).Msg("using config file") - - // Some actions do not need authentication headers - storedToken := viper.GetString(confOptions.authToken.viperKey) - - // If the CMD uses a workflow robot account instead of the regular Auth token we override it - // TODO: the attestation CLI should get split from this one - if _, ok := cmd.Annotations[useWorkflowRobotAccount]; ok { - storedToken = robotAccount - if storedToken != "" { - logger.Debug().Msg("loaded token from robot account") - } else { - return newGracefulError(ErrRobotAccountRequired) - } - } - if flagInsecure { logger.Warn().Msg("API contacted in insecure mode") } - conn, err := grpcconn.New(viper.GetString(confOptions.controlplaneAPI.viperKey), storedToken, flagInsecure) + apiToken, err := loadControlplaneAuthToken(cmd) + if err != nil { + return fmt.Errorf("loading controlplane auth token: %w", err) + } + + conn, err := grpcconn.New(viper.GetString(confOptions.controlplaneAPI.viperKey), apiToken, flagInsecure) if err != nil { return err } @@ -107,6 +103,14 @@ func NewRootCmd(l zerolog.Logger) *cobra.Command { rootCmd.PersistentFlags().BoolVar(&flagDebug, "debug", false, "Enable debug/verbose logging mode") rootCmd.PersistentFlags().StringVarP(&flagOutputFormat, "output", "o", "table", "Output format, valid options are json and table") + // Override the oauth authentication requirement for the CLI by providing an API token + rootCmd.PersistentFlags().StringVarP(&apiToken, "token", "t", "", fmt.Sprintf("API token. NOTE: Alternatively use the env variable %s", apiTokenEnvVarName)) + // We do not use viper in this case because we do not want this token to be saved in the config file + // Instead we load the env variable manually + if apiToken == "" { + apiToken = os.Getenv(apiTokenEnvVarName) + } + rootCmd.AddCommand(newWorkflowCmd(), newAuthCmd(), NewVersionCmd(), newAttestationCmd(), newArtifactCmd(), newConfigCmd(), newIntegrationCmd(), newOrganizationCmd(), newCASBackendCmd(), @@ -182,3 +186,30 @@ func cleanup(conn *grpc.ClientConn) error { } return nil } + +// Load the controlplane based on the following order: +// 1. If the CMD uses a robot account instead of the regular auth token we override it +// 2. If the CMD uses an API token flag/env variable we override it +// 3. If the CMD uses a config file we load it from there +func loadControlplaneAuthToken(cmd *cobra.Command) (string, error) { + // If the CMD uses a robot account instead of the regular auth token we override it + // TODO: the attestation CLI should get split from this one + if _, ok := cmd.Annotations[useWorkflowRobotAccount]; ok { + if robotAccount != "" { + logger.Debug().Msg("loaded token from robot account") + } else { + return "", newGracefulError(ErrRobotAccountRequired) + } + + return robotAccount, nil + } + + // override if token is passed as a flag/env variable + if apiToken != "" { + logger.Info().Msg("API token provided to the command line") + return apiToken, nil + } + + // loaded from config file, previously stored via "auth login" + return viper.GetString(confOptions.authToken.viperKey), nil +} diff --git a/app/controlplane/internal/service/apitoken.go b/app/controlplane/internal/service/apitoken.go index b1d0d939d..7837e4bed 100644 --- a/app/controlplane/internal/service/apitoken.go +++ b/app/controlplane/internal/service/apitoken.go @@ -47,6 +47,7 @@ func (s *APITokenService) Create(ctx context.Context, req *pb.APITokenServiceCre } // This is a API operation that requires actual user to be logged in not API token + // TODO: replace with authz layer, i.e casbin policies _, err = requireCurrentUser(ctx) if err != nil { return nil, err @@ -80,6 +81,7 @@ func (s *APITokenService) List(ctx context.Context, req *pb.APITokenServiceListR } // This is a API operation that requires actual user to be logged in not API token + // TODO: replace with authz layer, i.e casbin policies _, err = requireCurrentUser(ctx) if err != nil { return nil, err @@ -107,6 +109,7 @@ func (s *APITokenService) Revoke(ctx context.Context, req *pb.APITokenServiceRev } // This is a API operation that requires actual user to be logged in not API token + // TODO: replace with authz layer, i.e casbin policies _, err = requireCurrentUser(ctx) if err != nil { return nil, err diff --git a/app/controlplane/internal/service/context.go b/app/controlplane/internal/service/context.go index 2f96974bb..3c5b126fe 100644 --- a/app/controlplane/internal/service/context.go +++ b/app/controlplane/internal/service/context.go @@ -21,6 +21,7 @@ import ( pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1" "github.com/chainloop-dev/chainloop/app/controlplane/internal/biz" sl "github.com/chainloop-dev/chainloop/internal/servicelogger" + errors "github.com/go-kratos/kratos/v2/errors" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -39,21 +40,36 @@ func NewContextService(repoUC *biz.CASBackendUseCase, opts ...NewOpt) *ContextSe } func (s *ContextService) Current(ctx context.Context, _ *pb.ContextServiceCurrentRequest) (*pb.ContextServiceCurrentResponse, error) { - currentUser, err := requireCurrentUser(ctx) + currentOrg, err := requireCurrentOrg(ctx) if err != nil { return nil, err } - currentOrg, err := requireCurrentOrg(ctx) - if err != nil { + // load either user or API token + currentUser, err := requireCurrentUser(ctx) + if err != nil && !errors.IsNotFound(err) { return nil, err } - res := &pb.ContextServiceCurrentResponse_Result{ - CurrentUser: &pb.User{ + currentAPIToken, err := requireAPIToken(ctx) + if err != nil && !errors.IsNotFound(err) { + return nil, err + } + + if currentUser == nil && currentAPIToken == nil { + return nil, errors.NotFound("not found", "logged in user") + } + + res := &pb.ContextServiceCurrentResponse_Result{CurrentOrg: bizOrgToPb((*biz.Organization)(currentOrg))} + + if currentAPIToken != nil { + res.CurrentUser = &pb.User{ + Id: currentAPIToken.ID, Email: "API-token@chainloop", CreatedAt: timestamppb.New(*currentAPIToken.CreatedAt), + } + } else if currentUser != nil { + res.CurrentUser = &pb.User{ Id: currentUser.ID, Email: currentUser.Email, CreatedAt: timestamppb.New(*currentUser.CreatedAt), - }, - CurrentOrg: bizOrgToPb((*biz.Organization)(currentOrg)), + } } backend, err := s.uc.FindDefaultBackend(ctx, currentOrg.ID) diff --git a/app/controlplane/internal/service/service.go b/app/controlplane/internal/service/service.go index f945a490e..afdd4d775 100644 --- a/app/controlplane/internal/service/service.go +++ b/app/controlplane/internal/service/service.go @@ -59,6 +59,15 @@ func requireCurrentUser(ctx context.Context) (*usercontext.User, error) { return currentUser, nil } +func requireAPIToken(ctx context.Context) (*usercontext.APIToken, error) { + token := usercontext.CurrentAPIToken(ctx) + if token == nil { + return nil, errors.NotFound("not found", "API token") + } + + return token, nil +} + func requireCurrentOrg(ctx context.Context) (*usercontext.Org, error) { currentOrg := usercontext.CurrentOrg(ctx) if currentOrg == nil { diff --git a/app/controlplane/internal/usercontext/apitoken_middleware.go b/app/controlplane/internal/usercontext/apitoken_middleware.go index b53f9f5a7..a8cdc2552 100644 --- a/app/controlplane/internal/usercontext/apitoken_middleware.go +++ b/app/controlplane/internal/usercontext/apitoken_middleware.go @@ -39,6 +39,14 @@ func withCurrentAPIToken(ctx context.Context, token *APIToken) context.Context { return context.WithValue(ctx, currentAPITokenCtxKey{}, token) } +func CurrentAPIToken(ctx context.Context) *APIToken { + res := ctx.Value(currentAPITokenCtxKey{}) + if res == nil { + return nil + } + return res.(*APIToken) +} + type currentAPITokenCtxKey struct{} // Middleware that injects the API-Token + organization to the context @@ -69,6 +77,8 @@ func WithCurrentAPITokenAndOrgMiddleware(apiTokenUC *biz.APITokenUseCase, orgUC if err != nil { return nil, fmt.Errorf("error setting current org and user: %w", err) } + + logger.Infow("msg", "[authN] processed credentials", "id", tokenID, "type", "API-token") } return handler(ctx, req) diff --git a/app/controlplane/internal/usercontext/currentuser_middleware.go b/app/controlplane/internal/usercontext/currentuser_middleware.go index 49fc22e13..158851335 100644 --- a/app/controlplane/internal/usercontext/currentuser_middleware.go +++ b/app/controlplane/internal/usercontext/currentuser_middleware.go @@ -101,6 +101,8 @@ func WithCurrentUserAndOrgMiddleware(userUseCase biz.UserOrgFinder, logger *log. if err != nil { return nil, fmt.Errorf("error setting current org and user: %w", err) } + + logger.Infow("msg", "[authN] processed credentials", "id", userID, "type", "user") } return handler(ctx, req) From 9b8d01b59b35b6198af90c50ad0e43e07076ade9 Mon Sep 17 00:00:00 2001 From: Miguel Martinez Trivino Date: Tue, 12 Dec 2023 14:06:23 +0100 Subject: [PATCH 3/8] feat: API token support Signed-off-by: Miguel Martinez Trivino --- .../internal/usercontext/apitoken_middleware.go | 4 ++-- .../internal/usercontext/currentuser_middleware.go | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/controlplane/internal/usercontext/apitoken_middleware.go b/app/controlplane/internal/usercontext/apitoken_middleware.go index a8cdc2552..1957e7c67 100644 --- a/app/controlplane/internal/usercontext/apitoken_middleware.go +++ b/app/controlplane/internal/usercontext/apitoken_middleware.go @@ -73,7 +73,7 @@ func WithCurrentAPITokenAndOrgMiddleware(apiTokenUC *biz.APITokenUseCase, orgUC return nil, errors.New("error mapping the API-token claims") } - ctx, err = setCurrentAPIToken(ctx, apiTokenUC, orgUC, tokenID) + ctx, err = setCurrentOrgAndAPIToken(ctx, apiTokenUC, orgUC, tokenID) if err != nil { return nil, fmt.Errorf("error setting current org and user: %w", err) } @@ -87,7 +87,7 @@ func WithCurrentAPITokenAndOrgMiddleware(apiTokenUC *biz.APITokenUseCase, orgUC } // Set the current organization and API-Token in the context -func setCurrentAPIToken(ctx context.Context, apiTokenUC *biz.APITokenUseCase, orgUC *biz.OrganizationUseCase, tokenID string) (context.Context, error) { +func setCurrentOrgAndAPIToken(ctx context.Context, apiTokenUC *biz.APITokenUseCase, orgUC *biz.OrganizationUseCase, tokenID string) (context.Context, error) { if tokenID == "" { return nil, errors.New("error retrieving the key ID from the API token") } diff --git a/app/controlplane/internal/usercontext/currentuser_middleware.go b/app/controlplane/internal/usercontext/currentuser_middleware.go index 158851335..55647f0df 100644 --- a/app/controlplane/internal/usercontext/currentuser_middleware.go +++ b/app/controlplane/internal/usercontext/currentuser_middleware.go @@ -72,7 +72,7 @@ func CurrentOrg(ctx context.Context) *Org { type currentUserCtxKey struct{} type currentOrgCtxKey struct{} -// Middleware that injects the current user / API-token entry + organization to the context +// Middleware that injects the current user + organization to the context func WithCurrentUserAndOrgMiddleware(userUseCase biz.UserOrgFinder, logger *log.Helper) middleware.Middleware { return func(handler middleware.Handler) middleware.Handler { return func(ctx context.Context, req interface{}) (interface{}, error) { @@ -97,7 +97,7 @@ func WithCurrentUserAndOrgMiddleware(userUseCase biz.UserOrgFinder, logger *log. } var err error - ctx, err = setCurrentUser(ctx, userUseCase, userID, logger) + ctx, err = setCurrentOrgAndUser(ctx, userUseCase, userID, logger) if err != nil { return nil, fmt.Errorf("error setting current org and user: %w", err) } @@ -111,7 +111,7 @@ func WithCurrentUserAndOrgMiddleware(userUseCase biz.UserOrgFinder, logger *log. } // Find organization and user in DB -func setCurrentUser(ctx context.Context, userUC biz.UserOrgFinder, userID string, logger *log.Helper) (context.Context, error) { +func setCurrentOrgAndUser(ctx context.Context, userUC biz.UserOrgFinder, userID string, logger *log.Helper) (context.Context, error) { u, err := userUC.FindByID(ctx, userID) if err != nil { return nil, err From 31a83f4e6aabe3a79535058e12d5887381bd2251 Mon Sep 17 00:00:00 2001 From: Miguel Martinez Trivino Date: Tue, 12 Dec 2023 16:53:57 +0100 Subject: [PATCH 4/8] feat: API token support Signed-off-by: Miguel Martinez Trivino --- .../currentuser_middleware_test.go | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/app/controlplane/internal/usercontext/currentuser_middleware_test.go b/app/controlplane/internal/usercontext/currentuser_middleware_test.go index 09765479f..e19fa54e5 100644 --- a/app/controlplane/internal/usercontext/currentuser_middleware_test.go +++ b/app/controlplane/internal/usercontext/currentuser_middleware_test.go @@ -40,13 +40,15 @@ func TestWithCurrentUserAndOrgMiddleware(t *testing.T) { audience string userExist bool orgExist bool - wantErr bool + // the middleware logic got skipped + skipped bool + wantErr bool }{ { - name: "invalid audience", + name: "invalid audience", // in this case it gets ignored loggedIn: true, audience: "another-aud", - wantErr: true, + skipped: true, }, { name: "logged in, user and org exists", @@ -86,12 +88,12 @@ func TestWithCurrentUserAndOrgMiddleware(t *testing.T) { usecase := bizMocks.NewUserOrgFinder(t) ctx := context.Background() if tc.loggedIn { - ctx = jwtmiddleware.NewContext(ctx, &userjwtbuilder.CustomClaims{ - UserID: wantUser.ID, - RegisteredClaims: jwt.RegisteredClaims{ - Audience: jwt.ClaimStrings{tc.audience}, - }, - }) + c := jwt.MapClaims{ + "aud": tc.audience, + "user_id": wantUser.ID, + } + + ctx = jwtmiddleware.NewContext(ctx, c) } if tc.userExist { @@ -113,9 +115,11 @@ func TestWithCurrentUserAndOrgMiddleware(t *testing.T) { return nil, nil } - // Check that the wrapped handler contains the user and org - assert.Equal(t, CurrentOrg(ctx).ID, wantOrg.ID) - assert.Equal(t, CurrentUser(ctx).ID, wantUser.ID) + if !tc.skipped { + // Check that the wrapped handler contains the user and org + assert.Equal(t, CurrentOrg(ctx).ID, wantOrg.ID) + assert.Equal(t, CurrentUser(ctx).ID, wantUser.ID) + } return nil, nil })(ctx, nil) From 4eb03485530ccd8d16b98f366b6c4d3a800e61dc Mon Sep 17 00:00:00 2001 From: Miguel Martinez Trivino Date: Tue, 12 Dec 2023 22:37:58 +0100 Subject: [PATCH 5/8] feat: API token support Signed-off-by: Miguel Martinez Trivino --- app/controlplane/internal/biz/apitoken.go | 9 +- .../internal/biz/apitoken_integration_test.go | 24 +++ .../internal/biz/mocks/APITokenRepo.go | 127 ++++++++++++++++ .../usercontext/apitoken_middleware.go | 4 +- .../usercontext/apitoken_middleware_test.go | 141 ++++++++++++++++++ .../usercontext/currentuser_middleware.go | 4 +- 6 files changed, 304 insertions(+), 5 deletions(-) create mode 100644 app/controlplane/internal/biz/mocks/APITokenRepo.go create mode 100644 app/controlplane/internal/usercontext/apitoken_middleware_test.go diff --git a/app/controlplane/internal/biz/apitoken.go b/app/controlplane/internal/biz/apitoken.go index 54d21d192..cdb41de2d 100644 --- a/app/controlplane/internal/biz/apitoken.go +++ b/app/controlplane/internal/biz/apitoken.go @@ -134,5 +134,12 @@ func (uc *APITokenUseCase) FindByID(ctx context.Context, id string) (*APIToken, return nil, NewErrInvalidUUID(err) } - return uc.apiTokenRepo.FindByID(ctx, uuid) + t, err := uc.apiTokenRepo.FindByID(ctx, uuid) + if err != nil { + return nil, fmt.Errorf("finding token: %w", err) + } else if t == nil { + return nil, NewErrNotFound("token") + } + + return t, nil } diff --git a/app/controlplane/internal/biz/apitoken_integration_test.go b/app/controlplane/internal/biz/apitoken_integration_test.go index 0a5274753..3b61fb4e3 100644 --- a/app/controlplane/internal/biz/apitoken_integration_test.go +++ b/app/controlplane/internal/biz/apitoken_integration_test.go @@ -23,6 +23,7 @@ import ( "github.com/chainloop-dev/chainloop/app/controlplane/internal/biz" "github.com/chainloop-dev/chainloop/app/controlplane/internal/biz/testhelpers" "github.com/golang-jwt/jwt/v4" + "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -96,6 +97,29 @@ func (s *apiTokenTestSuite) TestRevoke() { }) } +func (s *apiTokenTestSuite) TestFindByID() { + ctx := context.Background() + + s.T().Run("invalid ID", func(t *testing.T) { + _, err := s.APIToken.FindByID(ctx, "deadbeef") + s.Error(err) + s.True(biz.IsErrInvalidUUID(err)) + }) + + s.T().Run("token not found", func(t *testing.T) { + token, err := s.APIToken.FindByID(ctx, uuid.NewString()) + s.Error(err) + s.True(biz.IsNotFound(err)) + s.Nil(token) + }) + + s.T().Run("token is found", func(t *testing.T) { + token, err := s.APIToken.FindByID(ctx, s.t1.ID.String()) + s.NoError(err) + s.Equal(s.t1.ID, token.ID) + }) +} + func (s *apiTokenTestSuite) TestList() { ctx := context.Background() s.T().Run("invalid org ID", func(t *testing.T) { diff --git a/app/controlplane/internal/biz/mocks/APITokenRepo.go b/app/controlplane/internal/biz/mocks/APITokenRepo.go new file mode 100644 index 000000000..b5c4c17cb --- /dev/null +++ b/app/controlplane/internal/biz/mocks/APITokenRepo.go @@ -0,0 +1,127 @@ +// Code generated by mockery v2.20.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + biz "github.com/chainloop-dev/chainloop/app/controlplane/internal/biz" + + mock "github.com/stretchr/testify/mock" + + time "time" + + uuid "github.com/google/uuid" +) + +// APITokenRepo is an autogenerated mock type for the APITokenRepo type +type APITokenRepo struct { + mock.Mock +} + +// Create provides a mock function with given fields: ctx, description, expiresAt, organizationID +func (_m *APITokenRepo) Create(ctx context.Context, description *string, expiresAt *time.Time, organizationID uuid.UUID) (*biz.APIToken, error) { + ret := _m.Called(ctx, description, expiresAt, organizationID) + + var r0 *biz.APIToken + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *string, *time.Time, uuid.UUID) (*biz.APIToken, error)); ok { + return rf(ctx, description, expiresAt, organizationID) + } + if rf, ok := ret.Get(0).(func(context.Context, *string, *time.Time, uuid.UUID) *biz.APIToken); ok { + r0 = rf(ctx, description, expiresAt, organizationID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*biz.APIToken) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *string, *time.Time, uuid.UUID) error); ok { + r1 = rf(ctx, description, expiresAt, organizationID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// FindByID provides a mock function with given fields: ctx, ID +func (_m *APITokenRepo) FindByID(ctx context.Context, ID uuid.UUID) (*biz.APIToken, error) { + ret := _m.Called(ctx, ID) + + var r0 *biz.APIToken + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) (*biz.APIToken, error)); ok { + return rf(ctx, ID) + } + if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) *biz.APIToken); ok { + r0 = rf(ctx, ID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*biz.APIToken) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, uuid.UUID) error); ok { + r1 = rf(ctx, ID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// List provides a mock function with given fields: ctx, orgID, includeRevoked +func (_m *APITokenRepo) List(ctx context.Context, orgID uuid.UUID, includeRevoked bool) ([]*biz.APIToken, error) { + ret := _m.Called(ctx, orgID, includeRevoked) + + var r0 []*biz.APIToken + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID, bool) ([]*biz.APIToken, error)); ok { + return rf(ctx, orgID, includeRevoked) + } + if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID, bool) []*biz.APIToken); ok { + r0 = rf(ctx, orgID, includeRevoked) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*biz.APIToken) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, uuid.UUID, bool) error); ok { + r1 = rf(ctx, orgID, includeRevoked) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Revoke provides a mock function with given fields: ctx, orgID, ID +func (_m *APITokenRepo) Revoke(ctx context.Context, orgID uuid.UUID, ID uuid.UUID) error { + ret := _m.Called(ctx, orgID, ID) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID, uuid.UUID) error); ok { + r0 = rf(ctx, orgID, ID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +type mockConstructorTestingTNewAPITokenRepo interface { + mock.TestingT + Cleanup(func()) +} + +// NewAPITokenRepo creates a new instance of APITokenRepo. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewAPITokenRepo(t mockConstructorTestingTNewAPITokenRepo) *APITokenRepo { + mock := &APITokenRepo{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/app/controlplane/internal/usercontext/apitoken_middleware.go b/app/controlplane/internal/usercontext/apitoken_middleware.go index 1957e7c67..abe85dabb 100644 --- a/app/controlplane/internal/usercontext/apitoken_middleware.go +++ b/app/controlplane/internal/usercontext/apitoken_middleware.go @@ -68,8 +68,8 @@ func WithCurrentAPITokenAndOrgMiddleware(apiTokenUC *biz.APITokenUseCase, orgUC // We've received an API-token if genericClaims.VerifyAudience(apitoken.Audience, true) { var err error - tokenID := genericClaims["jti"].(string) - if tokenID == "" { + tokenID, ok := genericClaims["jti"].(string) + if !ok || tokenID == "" { return nil, errors.New("error mapping the API-token claims") } diff --git a/app/controlplane/internal/usercontext/apitoken_middleware_test.go b/app/controlplane/internal/usercontext/apitoken_middleware_test.go new file mode 100644 index 000000000..383156163 --- /dev/null +++ b/app/controlplane/internal/usercontext/apitoken_middleware_test.go @@ -0,0 +1,141 @@ +// +// Copyright 2023 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. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package usercontext + +import ( + "context" + "io" + "testing" + + "github.com/chainloop-dev/chainloop/app/controlplane/internal/biz" + bizMocks "github.com/chainloop-dev/chainloop/app/controlplane/internal/biz/mocks" + "github.com/chainloop-dev/chainloop/app/controlplane/internal/conf" + "github.com/chainloop-dev/chainloop/app/controlplane/internal/jwt/apitoken" + "github.com/go-kratos/kratos/v2/log" + jwtmiddleware "github.com/go-kratos/kratos/v2/middleware/auth/jwt" + "github.com/golang-jwt/jwt/v4" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestWithCurrentAPITokenAndOrgMiddleware(t *testing.T) { + logger := log.NewHelper(log.NewStdLogger(io.Discard)) + testCases := []struct { + name string + receivedToken bool + audience string + tokenExists bool + orgExist bool + // the middleware logic got skipped + skipped bool + wantErr bool + }{ + { + name: "invalid audience", // in this case it gets ignored + receivedToken: true, + audience: "another-aud", + skipped: true, + }, + { + name: "token and org exists", + receivedToken: true, + audience: apitoken.Audience, + tokenExists: true, + orgExist: true, + wantErr: false, + }, + { + name: "token does not exist", + receivedToken: true, + audience: apitoken.Audience, + tokenExists: false, + wantErr: true, + }, + { + name: "org does not exist", + receivedToken: true, + audience: apitoken.Audience, + tokenExists: true, + wantErr: true, + }, + { + name: "no token received", + receivedToken: false, + audience: apitoken.Audience, + wantErr: true, + }, + } + + wantOrgID := uuid.New() + wantOrg := &biz.Organization{ID: wantOrgID.String()} + wantToken := &biz.APIToken{ID: uuid.New(), OrganizationID: wantOrgID} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + apiTokenRepo := bizMocks.NewAPITokenRepo(t) + orgRepo := bizMocks.NewOrganizationRepo(t) + apiTokenUC, err := biz.NewAPITokenUseCase(apiTokenRepo, &conf.Auth{GeneratedJwsHmacSecret: "test"}, nil) + require.NoError(t, err) + orgUC := biz.NewOrganizationUseCase(orgRepo, nil, nil, nil, nil) + require.NoError(t, err) + + ctx := context.Background() + if tc.receivedToken { + c := jwt.MapClaims{ + "aud": tc.audience, + "jti": wantToken.ID.String(), + } + + ctx = jwtmiddleware.NewContext(ctx, c) + } + + if tc.tokenExists { + apiTokenRepo.On("FindByID", ctx, wantToken.ID).Return(wantToken, nil) + } else if tc.receivedToken { + apiTokenRepo.On("FindByID", ctx, wantToken.ID).Maybe().Return(nil, nil) + } + + if tc.orgExist { + orgRepo.On("FindByID", ctx, wantOrgID).Return(wantOrg, nil) + } else if tc.receivedToken { + orgRepo.On("FindByID", ctx, wantOrgID).Maybe().Return(nil, nil) + } + + m := WithCurrentAPITokenAndOrgMiddleware(apiTokenUC, orgUC, logger) + _, err = m( + func(ctx context.Context, _ interface{}) (interface{}, error) { + if tc.wantErr { + return nil, nil + } + + if !tc.skipped { + // Check that the wrapped handler contains the user and org + assert.Equal(t, CurrentOrg(ctx).ID, wantOrg.ID) + assert.Equal(t, CurrentAPIToken(ctx).ID, wantToken.ID.String()) + } + + return nil, nil + })(ctx, nil) + + if tc.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/app/controlplane/internal/usercontext/currentuser_middleware.go b/app/controlplane/internal/usercontext/currentuser_middleware.go index 55647f0df..a387afaad 100644 --- a/app/controlplane/internal/usercontext/currentuser_middleware.go +++ b/app/controlplane/internal/usercontext/currentuser_middleware.go @@ -91,8 +91,8 @@ func WithCurrentUserAndOrgMiddleware(userUseCase biz.UserOrgFinder, logger *log. // Check wether the token is for a user or an API-token and handle accordingly // We've received a token for a user if genericClaims.VerifyAudience(user.Audience, true) { - userID := genericClaims["user_id"].(string) - if userID == "" { + userID, ok := genericClaims["user_id"].(string) + if !ok || userID == "" { return nil, errors.New("error mapping the user claims") } From 10657767cb50e52d07f102b19aa49a99f766da24 Mon Sep 17 00:00:00 2001 From: Miguel Martinez Trivino Date: Tue, 12 Dec 2023 23:35:54 +0100 Subject: [PATCH 6/8] feat(CLI): token management Signed-off-by: Miguel Martinez Trivino --- app/cli/cmd/organization.go | 1 + app/cli/cmd/organization_apitoken.go | 32 +++++++ app/cli/cmd/organization_apitoken_create.go | 90 +++++++++++++++++++ app/cli/cmd/organization_apitoken_list.go | 45 ++++++++++ app/cli/cmd/organization_apitoken_revoke.go | 47 ++++++++++ app/cli/cmd/output.go | 3 +- app/cli/internal/action/apitoken_create.go | 90 +++++++++++++++++++ app/cli/internal/action/apitoken_list.go | 46 ++++++++++ app/cli/internal/action/apitoken_revoke.go | 40 +++++++++ ...o => workflow_robotaccount_revoke copy.go} | 0 10 files changed, 393 insertions(+), 1 deletion(-) create mode 100644 app/cli/cmd/organization_apitoken.go create mode 100644 app/cli/cmd/organization_apitoken_create.go create mode 100644 app/cli/cmd/organization_apitoken_list.go create mode 100644 app/cli/cmd/organization_apitoken_revoke.go create mode 100644 app/cli/internal/action/apitoken_create.go create mode 100644 app/cli/internal/action/apitoken_list.go create mode 100644 app/cli/internal/action/apitoken_revoke.go rename app/cli/internal/action/{workflow_robotaccount_revoke.go => workflow_robotaccount_revoke copy.go} (100%) diff --git a/app/cli/cmd/organization.go b/app/cli/cmd/organization.go index 7b1aadc9c..976cd334d 100644 --- a/app/cli/cmd/organization.go +++ b/app/cli/cmd/organization.go @@ -34,6 +34,7 @@ func newOrganizationCmd() *cobra.Command { newOrganizationLeaveCmd(), newOrganizationDescribeCmd(), newOrganizationInvitationCmd(), + newOrganizationAPITokenCmd(), ) return cmd } diff --git a/app/cli/cmd/organization_apitoken.go b/app/cli/cmd/organization_apitoken.go new file mode 100644 index 000000000..313f979c0 --- /dev/null +++ b/app/cli/cmd/organization_apitoken.go @@ -0,0 +1,32 @@ +// +// Copyright 2023 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. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "github.com/spf13/cobra" +) + +func newOrganizationAPITokenCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "api-token", + Aliases: []string{"token"}, + Short: "API token management", + } + + cmd.AddCommand(newAPITokenCreateCmd(), newAPITokenListCmd(), newAPITokenRevokeCmd()) + + return cmd +} diff --git a/app/cli/cmd/organization_apitoken_create.go b/app/cli/cmd/organization_apitoken_create.go new file mode 100644 index 000000000..cb9ce1f9b --- /dev/null +++ b/app/cli/cmd/organization_apitoken_create.go @@ -0,0 +1,90 @@ +// +// Copyright 2023 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. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "context" + "fmt" + "time" + + "github.com/chainloop-dev/chainloop/app/cli/internal/action" + "github.com/jedib0t/go-pretty/v6/table" + "github.com/spf13/cobra" +) + +func newAPITokenCreateCmd() *cobra.Command { + var ( + description string + expiresIn time.Duration + ) + + cmd := &cobra.Command{ + Use: "create", + Short: "Create an API token", + RunE: func(cmd *cobra.Command, args []string) error { + var duration *time.Duration + if expiresIn != 0 { + duration = &expiresIn + } + + res, err := action.NewAPITokenCreate(actionOpts).Run(context.Background(), description, duration) + if err != nil { + return fmt.Errorf("creating API token: %w", err) + } + + return encodeOutput([]*action.APITokenItem{res}, apiTokenListTableOutput) + }, + } + + cmd.Flags().StringVar(&description, "description", "", "API token description") + cmd.Flags().DurationVar(&expiresIn, "expiration", 0, "optional API token expiration, in hours i.e 1h, 24h, 178h (week), ...") + + return cmd +} + +func apiTokenListTableOutput(tokens []*action.APITokenItem) error { + if len(tokens) == 0 { + fmt.Println("there are no API tokens in this org") + return nil + } + + t := newTableWriter() + + t.AppendHeader(table.Row{"ID", "Description", "Created At", "Expires At", "Revoked At"}) + for _, p := range tokens { + r := table.Row{p.ID, p.Description, p.CreatedAt.Format(time.RFC822)} + if p.ExpiresAt != nil { + r = append(r, p.ExpiresAt.Format(time.RFC822)) + } else { + r = append(r, "") + } + + if p.RevokedAt != nil { + fmt.Println("revoked at", p.RevokedAt.Format(time.RFC822)) + r = append(r, p.RevokedAt.Format(time.RFC822)) + } + + t.AppendRow(r) + } + t.Render() + + if len(tokens) == 1 && tokens[0].JWT != "" { + // Output the token too + fmt.Printf("\nSave the following token since it will not printed again: \n\n %s\n\n", tokens[0].JWT) + } + + return nil +} diff --git a/app/cli/cmd/organization_apitoken_list.go b/app/cli/cmd/organization_apitoken_list.go new file mode 100644 index 000000000..9b6b7817b --- /dev/null +++ b/app/cli/cmd/organization_apitoken_list.go @@ -0,0 +1,45 @@ +// +// Copyright 2023 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. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "context" + "fmt" + + "github.com/chainloop-dev/chainloop/app/cli/internal/action" + "github.com/spf13/cobra" +) + +func newAPITokenListCmd() *cobra.Command { + var includeRevoked bool + + cmd := &cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Short: "List API tokens in this organization", + RunE: func(cmd *cobra.Command, args []string) error { + res, err := action.NewAPITokenList(actionOpts).Run(context.Background(), includeRevoked) + if err != nil { + return fmt.Errorf("listing API tokens: %w", err) + } + + return encodeOutput(res, apiTokenListTableOutput) + }, + } + + cmd.Flags().BoolVarP(&includeRevoked, "all", "a", false, "show all API tokens including revoked ones") + return cmd +} diff --git a/app/cli/cmd/organization_apitoken_revoke.go b/app/cli/cmd/organization_apitoken_revoke.go new file mode 100644 index 000000000..41b51dce9 --- /dev/null +++ b/app/cli/cmd/organization_apitoken_revoke.go @@ -0,0 +1,47 @@ +// +// Copyright 2023 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. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "context" + "fmt" + + "github.com/chainloop-dev/chainloop/app/cli/internal/action" + "github.com/spf13/cobra" +) + +func newAPITokenRevokeCmd() *cobra.Command { + var apiTokenID string + + cmd := &cobra.Command{ + Use: "revoke", + Short: "revoke API token", + RunE: func(cmd *cobra.Command, args []string) error { + if err := action.NewAPITokenRevoke(actionOpts).Run(context.Background(), apiTokenID); err != nil { + return fmt.Errorf("revoking API token: %w", err) + } + + logger.Info().Msg("API token revoked!") + return nil + }, + } + + cmd.Flags().StringVar(&apiTokenID, "id", "", "API token ID") + err := cmd.MarkFlagRequired("id") + cobra.CheckErr(err) + + return cmd +} diff --git a/app/cli/cmd/output.go b/app/cli/cmd/output.go index cc6101ed3..2eb3a846f 100644 --- a/app/cli/cmd/output.go +++ b/app/cli/cmd/output.go @@ -44,7 +44,8 @@ type tabulatedData interface { []*action.AttachedIntegrationItem | []*action.MembershipItem | []*action.CASBackendItem | - []*action.OrgInvitationItem + []*action.OrgInvitationItem | + []*action.APITokenItem } var ErrOutputFormatNotImplemented = errors.New("format not implemented") diff --git a/app/cli/internal/action/apitoken_create.go b/app/cli/internal/action/apitoken_create.go new file mode 100644 index 000000000..ca4c9eeb9 --- /dev/null +++ b/app/cli/internal/action/apitoken_create.go @@ -0,0 +1,90 @@ +// +// Copyright 2023 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. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package action + +import ( + "context" + "errors" + "fmt" + "time" + + pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1" + "google.golang.org/protobuf/types/known/durationpb" +) + +type APITokenCreate struct { + cfg *ActionsOpts +} + +func NewAPITokenCreate(cfg *ActionsOpts) *APITokenCreate { + return &APITokenCreate{cfg} +} + +func (action *APITokenCreate) Run(ctx context.Context, description string, expiresIn *time.Duration) (*APITokenItem, error) { + client := pb.NewAPITokenServiceClient(action.cfg.CPConnection) + + req := &pb.APITokenServiceCreateRequest{Description: &description} + if expiresIn != nil { + req.ExpiresIn = durationpb.New(*expiresIn) + } + + resp, err := client.Create(ctx, req) + if err != nil { + return nil, fmt.Errorf("creating API token: %w", err) + } + + p := resp.Result + if p == nil { + return nil, errors.New("not found") + } + + item := pbAPITokenItemToAPITokenItem(p.Item) + item.JWT = p.Jwt + + return item, nil +} + +type APITokenItem struct { + ID string `json:"id"` + Description string `json:"description"` + // JWT is returned only during the creation + JWT string `json:"jwt,omitempty"` + CreatedAt *time.Time `json:"createdAt"` + RevokedAt *time.Time `json:"revokedAt,omitempty"` + ExpiresAt *time.Time `json:"expiresAt,omitempty"` +} + +func pbAPITokenItemToAPITokenItem(p *pb.APITokenItem) *APITokenItem { + if p == nil { + return nil + } + + item := &APITokenItem{ + ID: p.Id, + Description: p.Description, + CreatedAt: toTimePtr(p.CreatedAt.AsTime()), + } + + if p.RevokedAt != nil { + item.RevokedAt = toTimePtr(p.RevokedAt.AsTime()) + } + + if p.ExpiresAt != nil { + item.ExpiresAt = toTimePtr(p.ExpiresAt.AsTime()) + } + + return item +} diff --git a/app/cli/internal/action/apitoken_list.go b/app/cli/internal/action/apitoken_list.go new file mode 100644 index 000000000..6e85eb82d --- /dev/null +++ b/app/cli/internal/action/apitoken_list.go @@ -0,0 +1,46 @@ +// +// Copyright 2023 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. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package action + +import ( + "context" + "fmt" + + pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1" +) + +type APITokenList struct { + cfg *ActionsOpts +} + +func NewAPITokenList(cfg *ActionsOpts) *APITokenList { + return &APITokenList{cfg} +} + +func (action *APITokenList) Run(ctx context.Context, includeRevoked bool) ([]*APITokenItem, error) { + client := pb.NewAPITokenServiceClient(action.cfg.CPConnection) + resp, err := client.List(ctx, &pb.APITokenServiceListRequest{IncludeRevoked: includeRevoked}) + if err != nil { + return nil, fmt.Errorf("listing API tokens: %w", err) + } + + result := make([]*APITokenItem, 0, len(resp.Result)) + for _, t := range resp.Result { + result = append(result, pbAPITokenItemToAPITokenItem(t)) + } + + return result, nil +} diff --git a/app/cli/internal/action/apitoken_revoke.go b/app/cli/internal/action/apitoken_revoke.go new file mode 100644 index 000000000..2b07167f2 --- /dev/null +++ b/app/cli/internal/action/apitoken_revoke.go @@ -0,0 +1,40 @@ +// +// Copyright 2023 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. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package action + +import ( + "context" + "fmt" + + pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1" +) + +type APITokenRevoke struct { + cfg *ActionsOpts +} + +func NewAPITokenRevoke(cfg *ActionsOpts) *APITokenRevoke { + return &APITokenRevoke{cfg} +} + +func (action *APITokenRevoke) Run(ctx context.Context, apiTokenID string) error { + client := pb.NewAPITokenServiceClient(action.cfg.CPConnection) + if _, err := client.Revoke(ctx, &pb.APITokenServiceRevokeRequest{Id: apiTokenID}); err != nil { + return fmt.Errorf("revoking API token: %w", err) + } + + return nil +} diff --git a/app/cli/internal/action/workflow_robotaccount_revoke.go b/app/cli/internal/action/workflow_robotaccount_revoke copy.go similarity index 100% rename from app/cli/internal/action/workflow_robotaccount_revoke.go rename to app/cli/internal/action/workflow_robotaccount_revoke copy.go From 054a46b08a156145933f8e4c30468440d06777ca Mon Sep 17 00:00:00 2001 From: Miguel Martinez Trivino Date: Thu, 14 Dec 2023 16:20:18 +0100 Subject: [PATCH 7/8] feat(CLI): token management Signed-off-by: Miguel Martinez Trivino --- .../usercontext/apitoken_middleware.go | 2 ++ .../usercontext/apitoken_middleware_test.go | 22 +++++++++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/app/controlplane/internal/usercontext/apitoken_middleware.go b/app/controlplane/internal/usercontext/apitoken_middleware.go index abe85dabb..fc5afc907 100644 --- a/app/controlplane/internal/usercontext/apitoken_middleware.go +++ b/app/controlplane/internal/usercontext/apitoken_middleware.go @@ -100,6 +100,8 @@ func setCurrentOrgAndAPIToken(ctx context.Context, apiTokenUC *biz.APITokenUseCa return nil, errors.New("API token not found") } + // Note: Expiration time does not need to be checked because that's done at the JWT + // verification layer, which happens before this middleware is called if token.RevokedAt != nil { return nil, errors.New("API token revoked") } diff --git a/app/controlplane/internal/usercontext/apitoken_middleware_test.go b/app/controlplane/internal/usercontext/apitoken_middleware_test.go index 383156163..50c09bafa 100644 --- a/app/controlplane/internal/usercontext/apitoken_middleware_test.go +++ b/app/controlplane/internal/usercontext/apitoken_middleware_test.go @@ -19,6 +19,7 @@ import ( "context" "io" "testing" + "time" "github.com/chainloop-dev/chainloop/app/controlplane/internal/biz" bizMocks "github.com/chainloop-dev/chainloop/app/controlplane/internal/biz/mocks" @@ -39,6 +40,7 @@ func TestWithCurrentAPITokenAndOrgMiddleware(t *testing.T) { receivedToken bool audience string tokenExists bool + tokenRevoked bool orgExist bool // the middleware logic got skipped skipped bool @@ -58,6 +60,14 @@ func TestWithCurrentAPITokenAndOrgMiddleware(t *testing.T) { orgExist: true, wantErr: false, }, + { + name: "token revoked", + receivedToken: true, + audience: apitoken.Audience, + tokenExists: true, + tokenRevoked: true, + wantErr: true, + }, { name: "token does not exist", receivedToken: true, @@ -80,11 +90,11 @@ func TestWithCurrentAPITokenAndOrgMiddleware(t *testing.T) { }, } - wantOrgID := uuid.New() - wantOrg := &biz.Organization{ID: wantOrgID.String()} - wantToken := &biz.APIToken{ID: uuid.New(), OrganizationID: wantOrgID} - for _, tc := range testCases { + wantOrgID := uuid.New() + wantOrg := &biz.Organization{ID: wantOrgID.String()} + wantToken := &biz.APIToken{ID: uuid.New(), OrganizationID: wantOrgID} + t.Run(tc.name, func(t *testing.T) { apiTokenRepo := bizMocks.NewAPITokenRepo(t) orgRepo := bizMocks.NewOrganizationRepo(t) @@ -104,6 +114,10 @@ func TestWithCurrentAPITokenAndOrgMiddleware(t *testing.T) { } if tc.tokenExists { + if tc.tokenRevoked { + wantToken.RevokedAt = toTimePtr(time.Now()) + } + apiTokenRepo.On("FindByID", ctx, wantToken.ID).Return(wantToken, nil) } else if tc.receivedToken { apiTokenRepo.On("FindByID", ctx, wantToken.ID).Maybe().Return(nil, nil) From 4a0e37121cf320fc92f3b9e079cfee65ce93040b Mon Sep 17 00:00:00 2001 From: Miguel Martinez Trivino Date: Fri, 15 Dec 2023 09:31:52 +0100 Subject: [PATCH 8/8] feat(CLI): token management Signed-off-by: Miguel Martinez Trivino --- app/cli/cmd/organization_apitoken.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/cli/cmd/organization_apitoken.go b/app/cli/cmd/organization_apitoken.go index 313f979c0..e0bf76419 100644 --- a/app/cli/cmd/organization_apitoken.go +++ b/app/cli/cmd/organization_apitoken.go @@ -24,6 +24,8 @@ func newOrganizationAPITokenCmd() *cobra.Command { Use: "api-token", Aliases: []string{"token"}, Short: "API token management", + Long: `Manage API tokens to authenticate with the Chainloop API. +NOTE: They are not meant to be used during the attestation process, for that purpose you'll need to use a robot accounts instead.`, } cmd.AddCommand(newAPITokenCreateCmd(), newAPITokenListCmd(), newAPITokenRevokeCmd())