From 0e0a86d73aa3408909de77a7cceddf2bb8cd21be Mon Sep 17 00:00:00 2001 From: Sylwester Piskozub Date: Fri, 10 Apr 2026 09:49:47 +0200 Subject: [PATCH 1/7] feat: add operation authorization forward middleware Signed-off-by: Sylwester Piskozub --- app/controlplane/cmd/wire.go | 2 +- app/controlplane/cmd/wire_gen.go | 2 + app/controlplane/configs/config.devel.yaml | 4 + .../conf/controlplane/config/v1/conf.pb.go | 315 +++++++++++------- .../conf/controlplane/config/v1/conf.proto | 10 + app/controlplane/internal/server/grpc.go | 7 +- .../operation_authorization_middleware.go | 160 +++++++++ ...operation_authorization_middleware_test.go | 184 ++++++++++ .../templates/controlplane/configmap.yaml | 4 + deployment/chainloop/values.yaml | 7 + 10 files changed, 569 insertions(+), 126 deletions(-) create mode 100644 app/controlplane/internal/usercontext/operation_authorization_middleware.go create mode 100644 app/controlplane/internal/usercontext/operation_authorization_middleware_test.go diff --git a/app/controlplane/cmd/wire.go b/app/controlplane/cmd/wire.go index 3c2512649..5fd11a5ea 100644 --- a/app/controlplane/cmd/wire.go +++ b/app/controlplane/cmd/wire.go @@ -59,7 +59,7 @@ func wireApp(context.Context, *conf.Bootstrap, credentials.ReaderWriter, log.Log wire.Bind(new(biz.CASClient), new(*biz.CASClientUseCase)), serviceOpts, wire.Value([]biz.CASClientOpts{}), - wire.FieldsOf(new(*conf.Bootstrap), "Server", "Auth", "Data", "CasServer", "ReferrerSharedIndex", "Onboarding", "PrometheusIntegration", "PolicyProviders", "NatsServer", "FederatedAuthentication"), + wire.FieldsOf(new(*conf.Bootstrap), "Server", "Auth", "Data", "CasServer", "ReferrerSharedIndex", "Onboarding", "PrometheusIntegration", "PolicyProviders", "NatsServer", "FederatedAuthentication", "OperationAuthorizationProvider"), wire.FieldsOf(new(*conf.Data), "Database"), dispatcher.New, authz.NewCasbinEnforcer, diff --git a/app/controlplane/cmd/wire_gen.go b/app/controlplane/cmd/wire_gen.go index 366b9fe28..43c9c58c9 100644 --- a/app/controlplane/cmd/wire_gen.go +++ b/app/controlplane/cmd/wire_gen.go @@ -304,6 +304,7 @@ func wireApp(contextContext context.Context, bootstrap *conf.Bootstrap, readerWr groupService := service.NewGroupService(groupUseCase, v5...) projectService := service.NewProjectService(v5...) federatedAuthentication := bootstrap.FederatedAuthentication + operationAuthorizationProvider := bootstrap.OperationAuthorizationProvider validator, err := newProtoValidator() if err != nil { cleanup2() @@ -350,6 +351,7 @@ func wireApp(contextContext context.Context, bootstrap *conf.Bootstrap, readerWr ServerConfig: confServer, AuthConfig: auth, FederatedConfig: federatedAuthentication, + OperationAuthConfig: operationAuthorizationProvider, BootstrapConfig: bootstrap, Credentials: readerWriter, Validator: validator, diff --git a/app/controlplane/configs/config.devel.yaml b/app/controlplane/configs/config.devel.yaml index dc546e062..9282c486c 100644 --- a/app/controlplane/configs/config.devel.yaml +++ b/app/controlplane/configs/config.devel.yaml @@ -108,4 +108,8 @@ enable_profiler: true # enabled: true # url: http://localhost:8002/machine-identity/verify-token +# operation_authorization_provider: +# enabled: true +# url: http://localhost:8002/v1/authorize + ui_dashboard_url: http://localhost:3000 diff --git a/app/controlplane/internal/conf/controlplane/config/v1/conf.pb.go b/app/controlplane/internal/conf/controlplane/config/v1/conf.pb.go index 81d2fd82a..2c509046b 100644 --- a/app/controlplane/internal/conf/controlplane/config/v1/conf.pb.go +++ b/app/controlplane/internal/conf/controlplane/config/v1/conf.pb.go @@ -80,8 +80,10 @@ type Bootstrap struct { RestrictOrgCreation bool `protobuf:"varint,18,opt,name=restrict_org_creation,json=restrictOrgCreation,proto3" json:"restrict_org_creation,omitempty"` // External URL of the platform UI, if available UiDashboardUrl string `protobuf:"bytes,19,opt,name=ui_dashboard_url,json=uiDashboardUrl,proto3" json:"ui_dashboard_url,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Optional external operation authorization provider + OperationAuthorizationProvider *OperationAuthorizationProvider `protobuf:"bytes,20,opt,name=operation_authorization_provider,json=operationAuthorizationProvider,proto3" json:"operation_authorization_provider,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Bootstrap) Reset() { @@ -248,6 +250,67 @@ func (x *Bootstrap) GetUiDashboardUrl() string { return "" } +func (x *Bootstrap) GetOperationAuthorizationProvider() *OperationAuthorizationProvider { + if x != nil { + return x.OperationAuthorizationProvider + } + return nil +} + +type OperationAuthorizationProvider struct { + state protoimpl.MessageState `protogen:"open.v1"` + // URL of the authorization endpoint + Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"` + // Whether to enable the operation authorization + Enabled bool `protobuf:"varint,2,opt,name=enabled,proto3" json:"enabled,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OperationAuthorizationProvider) Reset() { + *x = OperationAuthorizationProvider{} + mi := &file_controlplane_config_v1_conf_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OperationAuthorizationProvider) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OperationAuthorizationProvider) ProtoMessage() {} + +func (x *OperationAuthorizationProvider) ProtoReflect() protoreflect.Message { + mi := &file_controlplane_config_v1_conf_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OperationAuthorizationProvider.ProtoReflect.Descriptor instead. +func (*OperationAuthorizationProvider) Descriptor() ([]byte, []int) { + return file_controlplane_config_v1_conf_proto_rawDescGZIP(), []int{1} +} + +func (x *OperationAuthorizationProvider) GetUrl() string { + if x != nil { + return x.Url + } + return "" +} + +func (x *OperationAuthorizationProvider) GetEnabled() bool { + if x != nil { + return x.Enabled + } + return false +} + type FederatedAuthentication struct { state protoimpl.MessageState `protogen:"open.v1"` // URL of the federated verification endpoint @@ -260,7 +323,7 @@ type FederatedAuthentication struct { func (x *FederatedAuthentication) Reset() { *x = FederatedAuthentication{} - mi := &file_controlplane_config_v1_conf_proto_msgTypes[1] + mi := &file_controlplane_config_v1_conf_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -272,7 +335,7 @@ func (x *FederatedAuthentication) String() string { func (*FederatedAuthentication) ProtoMessage() {} func (x *FederatedAuthentication) ProtoReflect() protoreflect.Message { - mi := &file_controlplane_config_v1_conf_proto_msgTypes[1] + mi := &file_controlplane_config_v1_conf_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -285,7 +348,7 @@ func (x *FederatedAuthentication) ProtoReflect() protoreflect.Message { // Deprecated: Use FederatedAuthentication.ProtoReflect.Descriptor instead. func (*FederatedAuthentication) Descriptor() ([]byte, []int) { - return file_controlplane_config_v1_conf_proto_rawDescGZIP(), []int{1} + return file_controlplane_config_v1_conf_proto_rawDescGZIP(), []int{2} } func (x *FederatedAuthentication) GetUrl() string { @@ -319,7 +382,7 @@ type PolicyProvider struct { func (x *PolicyProvider) Reset() { *x = PolicyProvider{} - mi := &file_controlplane_config_v1_conf_proto_msgTypes[2] + mi := &file_controlplane_config_v1_conf_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -331,7 +394,7 @@ func (x *PolicyProvider) String() string { func (*PolicyProvider) ProtoMessage() {} func (x *PolicyProvider) ProtoReflect() protoreflect.Message { - mi := &file_controlplane_config_v1_conf_proto_msgTypes[2] + mi := &file_controlplane_config_v1_conf_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -344,7 +407,7 @@ func (x *PolicyProvider) ProtoReflect() protoreflect.Message { // Deprecated: Use PolicyProvider.ProtoReflect.Descriptor instead. func (*PolicyProvider) Descriptor() ([]byte, []int) { - return file_controlplane_config_v1_conf_proto_rawDescGZIP(), []int{2} + return file_controlplane_config_v1_conf_proto_rawDescGZIP(), []int{3} } func (x *PolicyProvider) GetName() string { @@ -392,7 +455,7 @@ type ReferrerSharedIndex struct { func (x *ReferrerSharedIndex) Reset() { *x = ReferrerSharedIndex{} - mi := &file_controlplane_config_v1_conf_proto_msgTypes[3] + mi := &file_controlplane_config_v1_conf_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -404,7 +467,7 @@ func (x *ReferrerSharedIndex) String() string { func (*ReferrerSharedIndex) ProtoMessage() {} func (x *ReferrerSharedIndex) ProtoReflect() protoreflect.Message { - mi := &file_controlplane_config_v1_conf_proto_msgTypes[3] + mi := &file_controlplane_config_v1_conf_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -417,7 +480,7 @@ func (x *ReferrerSharedIndex) ProtoReflect() protoreflect.Message { // Deprecated: Use ReferrerSharedIndex.ProtoReflect.Descriptor instead. func (*ReferrerSharedIndex) Descriptor() ([]byte, []int) { - return file_controlplane_config_v1_conf_proto_rawDescGZIP(), []int{3} + return file_controlplane_config_v1_conf_proto_rawDescGZIP(), []int{4} } func (x *ReferrerSharedIndex) GetEnabled() bool { @@ -446,7 +509,7 @@ type Server struct { func (x *Server) Reset() { *x = Server{} - mi := &file_controlplane_config_v1_conf_proto_msgTypes[4] + mi := &file_controlplane_config_v1_conf_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -458,7 +521,7 @@ func (x *Server) String() string { func (*Server) ProtoMessage() {} func (x *Server) ProtoReflect() protoreflect.Message { - mi := &file_controlplane_config_v1_conf_proto_msgTypes[4] + mi := &file_controlplane_config_v1_conf_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -471,7 +534,7 @@ func (x *Server) ProtoReflect() protoreflect.Message { // Deprecated: Use Server.ProtoReflect.Descriptor instead. func (*Server) Descriptor() ([]byte, []int) { - return file_controlplane_config_v1_conf_proto_rawDescGZIP(), []int{4} + return file_controlplane_config_v1_conf_proto_rawDescGZIP(), []int{5} } func (x *Server) GetHttp() *Server_HTTP { @@ -504,7 +567,7 @@ type Data struct { func (x *Data) Reset() { *x = Data{} - mi := &file_controlplane_config_v1_conf_proto_msgTypes[5] + mi := &file_controlplane_config_v1_conf_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -516,7 +579,7 @@ func (x *Data) String() string { func (*Data) ProtoMessage() {} func (x *Data) ProtoReflect() protoreflect.Message { - mi := &file_controlplane_config_v1_conf_proto_msgTypes[5] + mi := &file_controlplane_config_v1_conf_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -529,7 +592,7 @@ func (x *Data) ProtoReflect() protoreflect.Message { // Deprecated: Use Data.ProtoReflect.Descriptor instead. func (*Data) Descriptor() ([]byte, []int) { - return file_controlplane_config_v1_conf_proto_rawDescGZIP(), []int{5} + return file_controlplane_config_v1_conf_proto_rawDescGZIP(), []int{6} } func (x *Data) GetDatabase() *Data_Database { @@ -554,7 +617,7 @@ type Auth struct { func (x *Auth) Reset() { *x = Auth{} - mi := &file_controlplane_config_v1_conf_proto_msgTypes[6] + mi := &file_controlplane_config_v1_conf_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -566,7 +629,7 @@ func (x *Auth) String() string { func (*Auth) ProtoMessage() {} func (x *Auth) ProtoReflect() protoreflect.Message { - mi := &file_controlplane_config_v1_conf_proto_msgTypes[6] + mi := &file_controlplane_config_v1_conf_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -579,7 +642,7 @@ func (x *Auth) ProtoReflect() protoreflect.Message { // Deprecated: Use Auth.ProtoReflect.Descriptor instead. func (*Auth) Descriptor() ([]byte, []int) { - return file_controlplane_config_v1_conf_proto_rawDescGZIP(), []int{6} + return file_controlplane_config_v1_conf_proto_rawDescGZIP(), []int{7} } func (x *Auth) GetGeneratedJwsHmacSecret() string { @@ -631,7 +694,7 @@ type TSA struct { func (x *TSA) Reset() { *x = TSA{} - mi := &file_controlplane_config_v1_conf_proto_msgTypes[7] + mi := &file_controlplane_config_v1_conf_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -643,7 +706,7 @@ func (x *TSA) String() string { func (*TSA) ProtoMessage() {} func (x *TSA) ProtoReflect() protoreflect.Message { - mi := &file_controlplane_config_v1_conf_proto_msgTypes[7] + mi := &file_controlplane_config_v1_conf_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -656,7 +719,7 @@ func (x *TSA) ProtoReflect() protoreflect.Message { // Deprecated: Use TSA.ProtoReflect.Descriptor instead. func (*TSA) Descriptor() ([]byte, []int) { - return file_controlplane_config_v1_conf_proto_rawDescGZIP(), []int{7} + return file_controlplane_config_v1_conf_proto_rawDescGZIP(), []int{8} } func (x *TSA) GetUrl() string { @@ -697,7 +760,7 @@ type CA struct { func (x *CA) Reset() { *x = CA{} - mi := &file_controlplane_config_v1_conf_proto_msgTypes[8] + mi := &file_controlplane_config_v1_conf_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -709,7 +772,7 @@ func (x *CA) String() string { func (*CA) ProtoMessage() {} func (x *CA) ProtoReflect() protoreflect.Message { - mi := &file_controlplane_config_v1_conf_proto_msgTypes[8] + mi := &file_controlplane_config_v1_conf_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -722,7 +785,7 @@ func (x *CA) ProtoReflect() protoreflect.Message { // Deprecated: Use CA.ProtoReflect.Descriptor instead. func (*CA) Descriptor() ([]byte, []int) { - return file_controlplane_config_v1_conf_proto_rawDescGZIP(), []int{8} + return file_controlplane_config_v1_conf_proto_rawDescGZIP(), []int{9} } func (x *CA) GetCa() isCA_Ca { @@ -784,7 +847,7 @@ type PrometheusIntegrationSpec struct { func (x *PrometheusIntegrationSpec) Reset() { *x = PrometheusIntegrationSpec{} - mi := &file_controlplane_config_v1_conf_proto_msgTypes[9] + mi := &file_controlplane_config_v1_conf_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -796,7 +859,7 @@ func (x *PrometheusIntegrationSpec) String() string { func (*PrometheusIntegrationSpec) ProtoMessage() {} func (x *PrometheusIntegrationSpec) ProtoReflect() protoreflect.Message { - mi := &file_controlplane_config_v1_conf_proto_msgTypes[9] + mi := &file_controlplane_config_v1_conf_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -809,7 +872,7 @@ func (x *PrometheusIntegrationSpec) ProtoReflect() protoreflect.Message { // Deprecated: Use PrometheusIntegrationSpec.ProtoReflect.Descriptor instead. func (*PrometheusIntegrationSpec) Descriptor() ([]byte, []int) { - return file_controlplane_config_v1_conf_proto_rawDescGZIP(), []int{9} + return file_controlplane_config_v1_conf_proto_rawDescGZIP(), []int{10} } func (x *PrometheusIntegrationSpec) GetOrgName() string { @@ -828,7 +891,7 @@ type Bootstrap_Observability struct { func (x *Bootstrap_Observability) Reset() { *x = Bootstrap_Observability{} - mi := &file_controlplane_config_v1_conf_proto_msgTypes[10] + mi := &file_controlplane_config_v1_conf_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -840,7 +903,7 @@ func (x *Bootstrap_Observability) String() string { func (*Bootstrap_Observability) ProtoMessage() {} func (x *Bootstrap_Observability) ProtoReflect() protoreflect.Message { - mi := &file_controlplane_config_v1_conf_proto_msgTypes[10] + mi := &file_controlplane_config_v1_conf_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -883,7 +946,7 @@ type Bootstrap_CASServer struct { func (x *Bootstrap_CASServer) Reset() { *x = Bootstrap_CASServer{} - mi := &file_controlplane_config_v1_conf_proto_msgTypes[11] + mi := &file_controlplane_config_v1_conf_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -895,7 +958,7 @@ func (x *Bootstrap_CASServer) String() string { func (*Bootstrap_CASServer) ProtoMessage() {} func (x *Bootstrap_CASServer) ProtoReflect() protoreflect.Message { - mi := &file_controlplane_config_v1_conf_proto_msgTypes[11] + mi := &file_controlplane_config_v1_conf_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -956,7 +1019,7 @@ type Bootstrap_NatsServer struct { func (x *Bootstrap_NatsServer) Reset() { *x = Bootstrap_NatsServer{} - mi := &file_controlplane_config_v1_conf_proto_msgTypes[12] + mi := &file_controlplane_config_v1_conf_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -968,7 +1031,7 @@ func (x *Bootstrap_NatsServer) String() string { func (*Bootstrap_NatsServer) ProtoMessage() {} func (x *Bootstrap_NatsServer) ProtoReflect() protoreflect.Message { - mi := &file_controlplane_config_v1_conf_proto_msgTypes[12] + mi := &file_controlplane_config_v1_conf_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1035,7 +1098,7 @@ type Bootstrap_Observability_Sentry struct { func (x *Bootstrap_Observability_Sentry) Reset() { *x = Bootstrap_Observability_Sentry{} - mi := &file_controlplane_config_v1_conf_proto_msgTypes[13] + mi := &file_controlplane_config_v1_conf_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1047,7 +1110,7 @@ func (x *Bootstrap_Observability_Sentry) String() string { func (*Bootstrap_Observability_Sentry) ProtoMessage() {} func (x *Bootstrap_Observability_Sentry) ProtoReflect() protoreflect.Message { - mi := &file_controlplane_config_v1_conf_proto_msgTypes[13] + mi := &file_controlplane_config_v1_conf_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1091,7 +1154,7 @@ type Server_HTTP struct { func (x *Server_HTTP) Reset() { *x = Server_HTTP{} - mi := &file_controlplane_config_v1_conf_proto_msgTypes[14] + mi := &file_controlplane_config_v1_conf_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1103,7 +1166,7 @@ func (x *Server_HTTP) String() string { func (*Server_HTTP) ProtoMessage() {} func (x *Server_HTTP) ProtoReflect() protoreflect.Message { - mi := &file_controlplane_config_v1_conf_proto_msgTypes[14] + mi := &file_controlplane_config_v1_conf_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1116,7 +1179,7 @@ func (x *Server_HTTP) ProtoReflect() protoreflect.Message { // Deprecated: Use Server_HTTP.ProtoReflect.Descriptor instead. func (*Server_HTTP) Descriptor() ([]byte, []int) { - return file_controlplane_config_v1_conf_proto_rawDescGZIP(), []int{4, 0} + return file_controlplane_config_v1_conf_proto_rawDescGZIP(), []int{5, 0} } func (x *Server_HTTP) GetNetwork() string { @@ -1158,7 +1221,7 @@ type Server_TLS struct { func (x *Server_TLS) Reset() { *x = Server_TLS{} - mi := &file_controlplane_config_v1_conf_proto_msgTypes[15] + mi := &file_controlplane_config_v1_conf_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1170,7 +1233,7 @@ func (x *Server_TLS) String() string { func (*Server_TLS) ProtoMessage() {} func (x *Server_TLS) ProtoReflect() protoreflect.Message { - mi := &file_controlplane_config_v1_conf_proto_msgTypes[15] + mi := &file_controlplane_config_v1_conf_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1183,7 +1246,7 @@ func (x *Server_TLS) ProtoReflect() protoreflect.Message { // Deprecated: Use Server_TLS.ProtoReflect.Descriptor instead. func (*Server_TLS) Descriptor() ([]byte, []int) { - return file_controlplane_config_v1_conf_proto_rawDescGZIP(), []int{4, 1} + return file_controlplane_config_v1_conf_proto_rawDescGZIP(), []int{5, 1} } func (x *Server_TLS) GetCertificate() string { @@ -1212,7 +1275,7 @@ type Server_GRPC struct { func (x *Server_GRPC) Reset() { *x = Server_GRPC{} - mi := &file_controlplane_config_v1_conf_proto_msgTypes[16] + mi := &file_controlplane_config_v1_conf_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1224,7 +1287,7 @@ func (x *Server_GRPC) String() string { func (*Server_GRPC) ProtoMessage() {} func (x *Server_GRPC) ProtoReflect() protoreflect.Message { - mi := &file_controlplane_config_v1_conf_proto_msgTypes[16] + mi := &file_controlplane_config_v1_conf_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1237,7 +1300,7 @@ func (x *Server_GRPC) ProtoReflect() protoreflect.Message { // Deprecated: Use Server_GRPC.ProtoReflect.Descriptor instead. func (*Server_GRPC) Descriptor() ([]byte, []int) { - return file_controlplane_config_v1_conf_proto_rawDescGZIP(), []int{4, 2} + return file_controlplane_config_v1_conf_proto_rawDescGZIP(), []int{5, 2} } func (x *Server_GRPC) GetNetwork() string { @@ -1284,7 +1347,7 @@ type Data_Database struct { func (x *Data_Database) Reset() { *x = Data_Database{} - mi := &file_controlplane_config_v1_conf_proto_msgTypes[17] + mi := &file_controlplane_config_v1_conf_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1296,7 +1359,7 @@ func (x *Data_Database) String() string { func (*Data_Database) ProtoMessage() {} func (x *Data_Database) ProtoReflect() protoreflect.Message { - mi := &file_controlplane_config_v1_conf_proto_msgTypes[17] + mi := &file_controlplane_config_v1_conf_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1309,7 +1372,7 @@ func (x *Data_Database) ProtoReflect() protoreflect.Message { // Deprecated: Use Data_Database.ProtoReflect.Descriptor instead. func (*Data_Database) Descriptor() ([]byte, []int) { - return file_controlplane_config_v1_conf_proto_rawDescGZIP(), []int{5, 0} + return file_controlplane_config_v1_conf_proto_rawDescGZIP(), []int{6, 0} } func (x *Data_Database) GetDriver() string { @@ -1361,7 +1424,7 @@ type Auth_OIDC struct { func (x *Auth_OIDC) Reset() { *x = Auth_OIDC{} - mi := &file_controlplane_config_v1_conf_proto_msgTypes[18] + mi := &file_controlplane_config_v1_conf_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1373,7 +1436,7 @@ func (x *Auth_OIDC) String() string { func (*Auth_OIDC) ProtoMessage() {} func (x *Auth_OIDC) ProtoReflect() protoreflect.Message { - mi := &file_controlplane_config_v1_conf_proto_msgTypes[18] + mi := &file_controlplane_config_v1_conf_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1386,7 +1449,7 @@ func (x *Auth_OIDC) ProtoReflect() protoreflect.Message { // Deprecated: Use Auth_OIDC.ProtoReflect.Descriptor instead. func (*Auth_OIDC) Descriptor() ([]byte, []int) { - return file_controlplane_config_v1_conf_proto_rawDescGZIP(), []int{6, 0} + return file_controlplane_config_v1_conf_proto_rawDescGZIP(), []int{7, 0} } func (x *Auth_OIDC) GetDomain() string { @@ -1428,7 +1491,7 @@ type CA_FileCA struct { func (x *CA_FileCA) Reset() { *x = CA_FileCA{} - mi := &file_controlplane_config_v1_conf_proto_msgTypes[19] + mi := &file_controlplane_config_v1_conf_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1440,7 +1503,7 @@ func (x *CA_FileCA) String() string { func (*CA_FileCA) ProtoMessage() {} func (x *CA_FileCA) ProtoReflect() protoreflect.Message { - mi := &file_controlplane_config_v1_conf_proto_msgTypes[19] + mi := &file_controlplane_config_v1_conf_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1453,7 +1516,7 @@ func (x *CA_FileCA) ProtoReflect() protoreflect.Message { // Deprecated: Use CA_FileCA.ProtoReflect.Descriptor instead. func (*CA_FileCA) Descriptor() ([]byte, []int) { - return file_controlplane_config_v1_conf_proto_rawDescGZIP(), []int{8, 0} + return file_controlplane_config_v1_conf_proto_rawDescGZIP(), []int{9, 0} } func (x *CA_FileCA) GetCertPath() string { @@ -1494,7 +1557,7 @@ type CA_EJBCA struct { func (x *CA_EJBCA) Reset() { *x = CA_EJBCA{} - mi := &file_controlplane_config_v1_conf_proto_msgTypes[20] + mi := &file_controlplane_config_v1_conf_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1506,7 +1569,7 @@ func (x *CA_EJBCA) String() string { func (*CA_EJBCA) ProtoMessage() {} func (x *CA_EJBCA) ProtoReflect() protoreflect.Message { - mi := &file_controlplane_config_v1_conf_proto_msgTypes[20] + mi := &file_controlplane_config_v1_conf_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1519,7 +1582,7 @@ func (x *CA_EJBCA) ProtoReflect() protoreflect.Message { // Deprecated: Use CA_EJBCA.ProtoReflect.Descriptor instead. func (*CA_EJBCA) Descriptor() ([]byte, []int) { - return file_controlplane_config_v1_conf_proto_rawDescGZIP(), []int{8, 1} + return file_controlplane_config_v1_conf_proto_rawDescGZIP(), []int{9, 1} } func (x *CA_EJBCA) GetServerUrl() string { @@ -1575,7 +1638,7 @@ var File_controlplane_config_v1_conf_proto protoreflect.FileDescriptor const file_controlplane_config_v1_conf_proto_rawDesc = "" + "\n" + - "!controlplane/config/v1/conf.proto\x12\x16controlplane.config.v1\x1a\x1bbuf/validate/validate.proto\x1a#controlplane/config/v1/config.proto\x1a\x1bcredentials/v1/config.proto\x1a\x1egoogle/protobuf/duration.proto\"\xb8\x0e\n" + + "!controlplane/config/v1/conf.proto\x12\x16controlplane.config.v1\x1a\x1bbuf/validate/validate.proto\x1a#controlplane/config/v1/config.proto\x1a\x1bcredentials/v1/config.proto\x1a\x1egoogle/protobuf/duration.proto\"\xbb\x0f\n" + "\tBootstrap\x126\n" + "\x06server\x18\x01 \x01(\v2\x1e.controlplane.config.v1.ServerR\x06server\x120\n" + "\x04data\x18\x02 \x01(\v2\x1c.controlplane.config.v1.DataR\x04data\x120\n" + @@ -1601,7 +1664,8 @@ const file_controlplane_config_v1_conf_proto_rawDesc = "" + "natsServer\x12j\n" + "\x18federated_authentication\x18\x10 \x01(\v2/.controlplane.config.v1.FederatedAuthenticationR\x17federatedAuthentication\x122\n" + "\x15restrict_org_creation\x18\x12 \x01(\bR\x13restrictOrgCreation\x12(\n" + - "\x10ui_dashboard_url\x18\x13 \x01(\tR\x0euiDashboardUrl\x1a\x9d\x01\n" + + "\x10ui_dashboard_url\x18\x13 \x01(\tR\x0euiDashboardUrl\x12\x80\x01\n" + + " operation_authorization_provider\x18\x14 \x01(\v26.controlplane.config.v1.OperationAuthorizationProviderR\x1eoperationAuthorizationProvider\x1a\x9d\x01\n" + "\rObservability\x12N\n" + "\x06sentry\x18\x01 \x01(\v26.controlplane.config.v1.Bootstrap.Observability.SentryR\x06sentry\x1a<\n" + "\x06Sentry\x12\x10\n" + @@ -1617,7 +1681,10 @@ const file_controlplane_config_v1_conf_proto_rawDesc = "" + "\x03uri\x18\x01 \x01(\tB\a\xbaH\x04r\x02\x10\x01R\x03uri\x12\x1f\n" + "\x05token\x18\x02 \x01(\tB\a\xbaH\x04r\x02\x10\x01H\x00R\x05token\x12\x1a\n" + "\breplicas\x18\x03 \x01(\x05R\breplicasB\x10\n" + - "\x0eauthentication\"O\n" + + "\x0eauthentication\"V\n" + + "\x1eOperationAuthorizationProvider\x12\x1a\n" + + "\x03url\x18\x01 \x01(\tB\b\xbaH\x05r\x03\x88\x01\x01R\x03url\x12\x18\n" + + "\aenabled\x18\x02 \x01(\bR\aenabled\"O\n" + "\x17FederatedAuthentication\x12\x1a\n" + "\x03url\x18\x01 \x01(\tB\b\xbaH\x05r\x03\x88\x01\x01R\x03url\x12\x18\n" + "\aenabled\x18\x02 \x01(\bR\aenabled\"\xee\x01\n" + @@ -1707,69 +1774,71 @@ func file_controlplane_config_v1_conf_proto_rawDescGZIP() []byte { return file_controlplane_config_v1_conf_proto_rawDescData } -var file_controlplane_config_v1_conf_proto_msgTypes = make([]protoimpl.MessageInfo, 21) +var file_controlplane_config_v1_conf_proto_msgTypes = make([]protoimpl.MessageInfo, 22) var file_controlplane_config_v1_conf_proto_goTypes = []any{ (*Bootstrap)(nil), // 0: controlplane.config.v1.Bootstrap - (*FederatedAuthentication)(nil), // 1: controlplane.config.v1.FederatedAuthentication - (*PolicyProvider)(nil), // 2: controlplane.config.v1.PolicyProvider - (*ReferrerSharedIndex)(nil), // 3: controlplane.config.v1.ReferrerSharedIndex - (*Server)(nil), // 4: controlplane.config.v1.Server - (*Data)(nil), // 5: controlplane.config.v1.Data - (*Auth)(nil), // 6: controlplane.config.v1.Auth - (*TSA)(nil), // 7: controlplane.config.v1.TSA - (*CA)(nil), // 8: controlplane.config.v1.CA - (*PrometheusIntegrationSpec)(nil), // 9: controlplane.config.v1.PrometheusIntegrationSpec - (*Bootstrap_Observability)(nil), // 10: controlplane.config.v1.Bootstrap.Observability - (*Bootstrap_CASServer)(nil), // 11: controlplane.config.v1.Bootstrap.CASServer - (*Bootstrap_NatsServer)(nil), // 12: controlplane.config.v1.Bootstrap.NatsServer - (*Bootstrap_Observability_Sentry)(nil), // 13: controlplane.config.v1.Bootstrap.Observability.Sentry - (*Server_HTTP)(nil), // 14: controlplane.config.v1.Server.HTTP - (*Server_TLS)(nil), // 15: controlplane.config.v1.Server.TLS - (*Server_GRPC)(nil), // 16: controlplane.config.v1.Server.GRPC - (*Data_Database)(nil), // 17: controlplane.config.v1.Data.Database - (*Auth_OIDC)(nil), // 18: controlplane.config.v1.Auth.OIDC - (*CA_FileCA)(nil), // 19: controlplane.config.v1.CA.FileCA - (*CA_EJBCA)(nil), // 20: controlplane.config.v1.CA.EJBCA - (*v1.Credentials)(nil), // 21: credentials.v1.Credentials - (*v11.OnboardingSpec)(nil), // 22: controlplane.config.v1.OnboardingSpec - (*v11.AllowList)(nil), // 23: controlplane.config.v1.AllowList - (*durationpb.Duration)(nil), // 24: google.protobuf.Duration + (*OperationAuthorizationProvider)(nil), // 1: controlplane.config.v1.OperationAuthorizationProvider + (*FederatedAuthentication)(nil), // 2: controlplane.config.v1.FederatedAuthentication + (*PolicyProvider)(nil), // 3: controlplane.config.v1.PolicyProvider + (*ReferrerSharedIndex)(nil), // 4: controlplane.config.v1.ReferrerSharedIndex + (*Server)(nil), // 5: controlplane.config.v1.Server + (*Data)(nil), // 6: controlplane.config.v1.Data + (*Auth)(nil), // 7: controlplane.config.v1.Auth + (*TSA)(nil), // 8: controlplane.config.v1.TSA + (*CA)(nil), // 9: controlplane.config.v1.CA + (*PrometheusIntegrationSpec)(nil), // 10: controlplane.config.v1.PrometheusIntegrationSpec + (*Bootstrap_Observability)(nil), // 11: controlplane.config.v1.Bootstrap.Observability + (*Bootstrap_CASServer)(nil), // 12: controlplane.config.v1.Bootstrap.CASServer + (*Bootstrap_NatsServer)(nil), // 13: controlplane.config.v1.Bootstrap.NatsServer + (*Bootstrap_Observability_Sentry)(nil), // 14: controlplane.config.v1.Bootstrap.Observability.Sentry + (*Server_HTTP)(nil), // 15: controlplane.config.v1.Server.HTTP + (*Server_TLS)(nil), // 16: controlplane.config.v1.Server.TLS + (*Server_GRPC)(nil), // 17: controlplane.config.v1.Server.GRPC + (*Data_Database)(nil), // 18: controlplane.config.v1.Data.Database + (*Auth_OIDC)(nil), // 19: controlplane.config.v1.Auth.OIDC + (*CA_FileCA)(nil), // 20: controlplane.config.v1.CA.FileCA + (*CA_EJBCA)(nil), // 21: controlplane.config.v1.CA.EJBCA + (*v1.Credentials)(nil), // 22: credentials.v1.Credentials + (*v11.OnboardingSpec)(nil), // 23: controlplane.config.v1.OnboardingSpec + (*v11.AllowList)(nil), // 24: controlplane.config.v1.AllowList + (*durationpb.Duration)(nil), // 25: google.protobuf.Duration } var file_controlplane_config_v1_conf_proto_depIdxs = []int32{ - 4, // 0: controlplane.config.v1.Bootstrap.server:type_name -> controlplane.config.v1.Server - 5, // 1: controlplane.config.v1.Bootstrap.data:type_name -> controlplane.config.v1.Data - 6, // 2: controlplane.config.v1.Bootstrap.auth:type_name -> controlplane.config.v1.Auth - 10, // 3: controlplane.config.v1.Bootstrap.observability:type_name -> controlplane.config.v1.Bootstrap.Observability - 21, // 4: controlplane.config.v1.Bootstrap.credentials_service:type_name -> credentials.v1.Credentials - 11, // 5: controlplane.config.v1.Bootstrap.cas_server:type_name -> controlplane.config.v1.Bootstrap.CASServer - 3, // 6: controlplane.config.v1.Bootstrap.referrer_shared_index:type_name -> controlplane.config.v1.ReferrerSharedIndex - 8, // 7: controlplane.config.v1.Bootstrap.certificate_authority:type_name -> controlplane.config.v1.CA - 8, // 8: controlplane.config.v1.Bootstrap.certificate_authorities:type_name -> controlplane.config.v1.CA - 7, // 9: controlplane.config.v1.Bootstrap.timestamp_authorities:type_name -> controlplane.config.v1.TSA - 22, // 10: controlplane.config.v1.Bootstrap.onboarding:type_name -> controlplane.config.v1.OnboardingSpec - 9, // 11: controlplane.config.v1.Bootstrap.prometheus_integration:type_name -> controlplane.config.v1.PrometheusIntegrationSpec - 2, // 12: controlplane.config.v1.Bootstrap.policy_providers:type_name -> controlplane.config.v1.PolicyProvider - 12, // 13: controlplane.config.v1.Bootstrap.nats_server:type_name -> controlplane.config.v1.Bootstrap.NatsServer - 1, // 14: controlplane.config.v1.Bootstrap.federated_authentication:type_name -> controlplane.config.v1.FederatedAuthentication - 14, // 15: controlplane.config.v1.Server.http:type_name -> controlplane.config.v1.Server.HTTP - 16, // 16: controlplane.config.v1.Server.grpc:type_name -> controlplane.config.v1.Server.GRPC - 14, // 17: controlplane.config.v1.Server.http_metrics:type_name -> controlplane.config.v1.Server.HTTP - 17, // 18: controlplane.config.v1.Data.database:type_name -> controlplane.config.v1.Data.Database - 23, // 19: controlplane.config.v1.Auth.allow_list:type_name -> controlplane.config.v1.AllowList - 18, // 20: controlplane.config.v1.Auth.oidc:type_name -> controlplane.config.v1.Auth.OIDC - 19, // 21: controlplane.config.v1.CA.file_ca:type_name -> controlplane.config.v1.CA.FileCA - 20, // 22: controlplane.config.v1.CA.ejbca_ca:type_name -> controlplane.config.v1.CA.EJBCA - 13, // 23: controlplane.config.v1.Bootstrap.Observability.sentry:type_name -> controlplane.config.v1.Bootstrap.Observability.Sentry - 16, // 24: controlplane.config.v1.Bootstrap.CASServer.grpc:type_name -> controlplane.config.v1.Server.GRPC - 24, // 25: controlplane.config.v1.Server.HTTP.timeout:type_name -> google.protobuf.Duration - 24, // 26: controlplane.config.v1.Server.GRPC.timeout:type_name -> google.protobuf.Duration - 15, // 27: controlplane.config.v1.Server.GRPC.tls_config:type_name -> controlplane.config.v1.Server.TLS - 24, // 28: controlplane.config.v1.Data.Database.max_conn_idle_time:type_name -> google.protobuf.Duration - 29, // [29:29] is the sub-list for method output_type - 29, // [29:29] is the sub-list for method input_type - 29, // [29:29] is the sub-list for extension type_name - 29, // [29:29] is the sub-list for extension extendee - 0, // [0:29] is the sub-list for field type_name + 5, // 0: controlplane.config.v1.Bootstrap.server:type_name -> controlplane.config.v1.Server + 6, // 1: controlplane.config.v1.Bootstrap.data:type_name -> controlplane.config.v1.Data + 7, // 2: controlplane.config.v1.Bootstrap.auth:type_name -> controlplane.config.v1.Auth + 11, // 3: controlplane.config.v1.Bootstrap.observability:type_name -> controlplane.config.v1.Bootstrap.Observability + 22, // 4: controlplane.config.v1.Bootstrap.credentials_service:type_name -> credentials.v1.Credentials + 12, // 5: controlplane.config.v1.Bootstrap.cas_server:type_name -> controlplane.config.v1.Bootstrap.CASServer + 4, // 6: controlplane.config.v1.Bootstrap.referrer_shared_index:type_name -> controlplane.config.v1.ReferrerSharedIndex + 9, // 7: controlplane.config.v1.Bootstrap.certificate_authority:type_name -> controlplane.config.v1.CA + 9, // 8: controlplane.config.v1.Bootstrap.certificate_authorities:type_name -> controlplane.config.v1.CA + 8, // 9: controlplane.config.v1.Bootstrap.timestamp_authorities:type_name -> controlplane.config.v1.TSA + 23, // 10: controlplane.config.v1.Bootstrap.onboarding:type_name -> controlplane.config.v1.OnboardingSpec + 10, // 11: controlplane.config.v1.Bootstrap.prometheus_integration:type_name -> controlplane.config.v1.PrometheusIntegrationSpec + 3, // 12: controlplane.config.v1.Bootstrap.policy_providers:type_name -> controlplane.config.v1.PolicyProvider + 13, // 13: controlplane.config.v1.Bootstrap.nats_server:type_name -> controlplane.config.v1.Bootstrap.NatsServer + 2, // 14: controlplane.config.v1.Bootstrap.federated_authentication:type_name -> controlplane.config.v1.FederatedAuthentication + 1, // 15: controlplane.config.v1.Bootstrap.operation_authorization_provider:type_name -> controlplane.config.v1.OperationAuthorizationProvider + 15, // 16: controlplane.config.v1.Server.http:type_name -> controlplane.config.v1.Server.HTTP + 17, // 17: controlplane.config.v1.Server.grpc:type_name -> controlplane.config.v1.Server.GRPC + 15, // 18: controlplane.config.v1.Server.http_metrics:type_name -> controlplane.config.v1.Server.HTTP + 18, // 19: controlplane.config.v1.Data.database:type_name -> controlplane.config.v1.Data.Database + 24, // 20: controlplane.config.v1.Auth.allow_list:type_name -> controlplane.config.v1.AllowList + 19, // 21: controlplane.config.v1.Auth.oidc:type_name -> controlplane.config.v1.Auth.OIDC + 20, // 22: controlplane.config.v1.CA.file_ca:type_name -> controlplane.config.v1.CA.FileCA + 21, // 23: controlplane.config.v1.CA.ejbca_ca:type_name -> controlplane.config.v1.CA.EJBCA + 14, // 24: controlplane.config.v1.Bootstrap.Observability.sentry:type_name -> controlplane.config.v1.Bootstrap.Observability.Sentry + 17, // 25: controlplane.config.v1.Bootstrap.CASServer.grpc:type_name -> controlplane.config.v1.Server.GRPC + 25, // 26: controlplane.config.v1.Server.HTTP.timeout:type_name -> google.protobuf.Duration + 25, // 27: controlplane.config.v1.Server.GRPC.timeout:type_name -> google.protobuf.Duration + 16, // 28: controlplane.config.v1.Server.GRPC.tls_config:type_name -> controlplane.config.v1.Server.TLS + 25, // 29: controlplane.config.v1.Data.Database.max_conn_idle_time:type_name -> google.protobuf.Duration + 30, // [30:30] is the sub-list for method output_type + 30, // [30:30] is the sub-list for method input_type + 30, // [30:30] is the sub-list for extension type_name + 30, // [30:30] is the sub-list for extension extendee + 0, // [0:30] is the sub-list for field type_name } func init() { file_controlplane_config_v1_conf_proto_init() } @@ -1777,11 +1846,11 @@ func file_controlplane_config_v1_conf_proto_init() { if File_controlplane_config_v1_conf_proto != nil { return } - file_controlplane_config_v1_conf_proto_msgTypes[8].OneofWrappers = []any{ + file_controlplane_config_v1_conf_proto_msgTypes[9].OneofWrappers = []any{ (*CA_FileCa)(nil), (*CA_EjbcaCa)(nil), } - file_controlplane_config_v1_conf_proto_msgTypes[12].OneofWrappers = []any{ + file_controlplane_config_v1_conf_proto_msgTypes[13].OneofWrappers = []any{ (*Bootstrap_NatsServer_Token)(nil), } type x struct{} @@ -1790,7 +1859,7 @@ func file_controlplane_config_v1_conf_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_controlplane_config_v1_conf_proto_rawDesc), len(file_controlplane_config_v1_conf_proto_rawDesc)), NumEnums: 0, - NumMessages: 21, + NumMessages: 22, NumExtensions: 0, NumServices: 0, }, diff --git a/app/controlplane/internal/conf/controlplane/config/v1/conf.proto b/app/controlplane/internal/conf/controlplane/config/v1/conf.proto index bb4aa0514..22b6032e1 100644 --- a/app/controlplane/internal/conf/controlplane/config/v1/conf.proto +++ b/app/controlplane/internal/conf/controlplane/config/v1/conf.proto @@ -108,6 +108,16 @@ message Bootstrap { // External URL of the platform UI, if available string ui_dashboard_url = 19; + + // Optional external operation authorization provider + OperationAuthorizationProvider operation_authorization_provider = 20; +} + +message OperationAuthorizationProvider { + // URL of the authorization endpoint + string url = 1 [(buf.validate.field).string.uri = true]; + // Whether to enable the operation authorization + bool enabled = 2; } message FederatedAuthentication { diff --git a/app/controlplane/internal/server/grpc.go b/app/controlplane/internal/server/grpc.go index 109464a59..97c7960bc 100644 --- a/app/controlplane/internal/server/grpc.go +++ b/app/controlplane/internal/server/grpc.go @@ -92,8 +92,9 @@ type Opts struct { Logger log.Logger ServerConfig *conf.Server AuthConfig *conf.Auth - FederatedConfig *conf.FederatedAuthentication - BootstrapConfig *conf.Bootstrap + FederatedConfig *conf.FederatedAuthentication + OperationAuthConfig *conf.OperationAuthorizationProvider + BootstrapConfig *conf.Bootstrap Credentials credentials.ReaderWriter Validator protovalidate.Validator } @@ -208,6 +209,8 @@ func craftMiddleware(opts *Opts) []middleware.Middleware { usercontext.WithCurrentUserMiddleware(opts.UserUseCase, logHelper), // Store all memberships in the context usercontext.WithCurrentMembershipsMiddleware(opts.MembershipUseCase, opts.MembershipsCache), + // Operation authorization forward + usercontext.WithOperationAuthorizationMiddleware(opts.OperationAuthConfig, logHelper), selector.Server( // 2.d- Set its organization usercontext.WithCurrentOrganizationMiddleware(opts.UserUseCase, opts.OrganizationUseCase, logHelper), diff --git a/app/controlplane/internal/usercontext/operation_authorization_middleware.go b/app/controlplane/internal/usercontext/operation_authorization_middleware.go new file mode 100644 index 000000000..d8328badc --- /dev/null +++ b/app/controlplane/internal/usercontext/operation_authorization_middleware.go @@ -0,0 +1,160 @@ +// +// Copyright 2026 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// 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 ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "time" + + conf "github.com/chainloop-dev/chainloop/app/controlplane/internal/conf/controlplane/config/v1" + "github.com/chainloop-dev/chainloop/app/controlplane/internal/usercontext/entities" + errorsAPI "github.com/go-kratos/kratos/v2/errors" + "github.com/go-kratos/kratos/v2/log" + "github.com/go-kratos/kratos/v2/middleware" + "github.com/go-kratos/kratos/v2/transport" + + "github.com/chainloop-dev/chainloop/pkg/cache" +) + +// Operations that require authorization checks +var operationAuthTargets = map[string]bool{ + "/controlplane.v1.OrganizationService/Create": true, +} + +type operationAuthRequest struct { + Operation string `json:"operation"` + UserID string `json:"user_id"` + OrganizationID string `json:"organization_id,omitempty"` +} + +type operationAuthResponse struct { + Allowed bool `json:"allowed"` + Reason string `json:"reason"` +} + +// WithOperationAuthorizationMiddleware forwards specific operations to an external authorization. +// If the external call fails, the request is denied. +func WithOperationAuthorizationMiddleware(conf *conf.OperationAuthorizationProvider, logger *log.Helper) middleware.Middleware { + if conf == nil || !conf.GetEnabled() || conf.GetUrl() == "" { + return func(handler middleware.Handler) middleware.Handler { + return handler + } + } + + client := &http.Client{Timeout: 5 * time.Second} + url := conf.GetUrl() + + // LRU cache with 30s TTL keyed by "user_id:operation" + authCache, err := cache.New[*operationAuthResponse]( + cache.WithTTL(30 * time.Second), + cache.WithDescription("Operation authorization cache"), + ) + if err != nil { + logger.Warnw("msg", "failed to create operation auth cache, proceeding without cache", "error", err) + authCache = nil + } + + return func(handler middleware.Handler) middleware.Handler { + return func(ctx context.Context, req interface{}) (interface{}, error) { + info, ok := transport.FromServerContext(ctx) + if !ok { + return handler(ctx, req) + } + + operation := info.Operation() + if !operationAuthTargets[operation] { + return handler(ctx, req) + } + + user := entities.CurrentUser(ctx) + if user == nil { + return handler(ctx, req) + } + + var orgID string + if org := entities.CurrentOrg(ctx); org != nil { + orgID = org.ID + } + + cacheKey := fmt.Sprintf("%s:%s", user.ID, operation) + + // Check cache + if authCache != nil { + if cached, found, _ := authCache.Get(ctx, cacheKey); found { + if !cached.Allowed { + return nil, errorsAPI.Forbidden("operation_denied", cached.Reason) + } + return handler(ctx, req) + } + } + + result, err := callAuthorizationEndpoint(ctx, client, url, &operationAuthRequest{ + Operation: operation, + UserID: user.ID, + OrganizationID: orgID, + }) + if err != nil { + logger.Errorw("msg", "operation authorization call failed, denying request (fail-closed)", "error", err, "operation", operation) + return nil, errorsAPI.Forbidden("operation_denied", "unable to verify operation authorization") + } + + // Cache the result + if authCache != nil { + _ = authCache.Set(ctx, cacheKey, result) + } + + if !result.Allowed { + return nil, errorsAPI.Forbidden("operation_denied", result.Reason) + } + + return handler(ctx, req) + } + } +} + +func callAuthorizationEndpoint(ctx context.Context, client *http.Client, url string, authReq *operationAuthRequest) (*operationAuthResponse, error) { + body, err := json.Marshal(authReq) + if err != nil { + return nil, fmt.Errorf("marshaling request: %w", err) + } + + httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) + if err != nil { + return nil, fmt.Errorf("creating request: %w", err) + } + httpReq.Header.Set("Content-Type", "application/json") + + resp, err := client.Do(httpReq) + if err != nil { + return nil, fmt.Errorf("calling authorization endpoint: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("authorization endpoint returned status %d", resp.StatusCode) + } + + var authResp operationAuthResponse + if err := json.NewDecoder(resp.Body).Decode(&authResp); err != nil { + return nil, fmt.Errorf("decoding response: %w", err) + } + + return &authResp, nil +} diff --git a/app/controlplane/internal/usercontext/operation_authorization_middleware_test.go b/app/controlplane/internal/usercontext/operation_authorization_middleware_test.go new file mode 100644 index 000000000..bd718c32d --- /dev/null +++ b/app/controlplane/internal/usercontext/operation_authorization_middleware_test.go @@ -0,0 +1,184 @@ +// +// Copyright 2026 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// 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" + "encoding/json" + "net/http" + "net/http/httptest" + "sync/atomic" + "testing" + + conf "github.com/chainloop-dev/chainloop/app/controlplane/internal/conf/controlplane/config/v1" + "github.com/chainloop-dev/chainloop/app/controlplane/internal/usercontext/entities" + "github.com/go-kratos/kratos/v2/log" + "github.com/go-kratos/kratos/v2/transport" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// fakeTransport implements transport.Transporter for testing middleware operation matching. +type fakeTransport struct { + operation string +} + +func (f *fakeTransport) Kind() transport.Kind { return transport.KindGRPC } +func (f *fakeTransport) Endpoint() string { return "" } +func (f *fakeTransport) Operation() string { return f.operation } +func (f *fakeTransport) RequestHeader() transport.Header { return nil } +func (f *fakeTransport) ReplyHeader() transport.Header { return nil } + +func ctxWithOperation(ctx context.Context, op string) context.Context { + return transport.NewServerContext(ctx, &fakeTransport{operation: op}) +} + +func TestWithOperationAuthorizationMiddleware(t *testing.T) { + logHelper := log.NewHelper(log.DefaultLogger) + + t.Run("disabled config is passthrough", func(t *testing.T) { + m := WithOperationAuthorizationMiddleware(nil, logHelper) + result, err := m(passHandler)(context.Background(), nil) + require.NoError(t, err) + assert.Equal(t, "ok", result) + + m = WithOperationAuthorizationMiddleware(&conf.OperationAuthorizationProvider{Enabled: false}, logHelper) + result, err = m(passHandler)(context.Background(), nil) + require.NoError(t, err) + assert.Equal(t, "ok", result) + }) + + t.Run("non-target operation is passthrough", func(t *testing.T) { + var callCount atomic.Int32 + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + callCount.Add(1) + w.WriteHeader(http.StatusOK) + })) + defer srv.Close() + + cfg := &conf.OperationAuthorizationProvider{Enabled: true, Url: srv.URL} + m := WithOperationAuthorizationMiddleware(cfg, logHelper) + + ctx := ctxWithOperation(context.Background(), "/controlplane.v1.WorkflowService/List") + ctx = entities.WithCurrentUser(ctx, &entities.User{ID: "user-1"}) + + result, err := m(passHandler)(ctx, nil) + require.NoError(t, err) + assert.Equal(t, "ok", result) + assert.Equal(t, int32(0), callCount.Load()) + }) + + t.Run("target operation allowed", func(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var req operationAuthRequest + require.NoError(t, json.NewDecoder(r.Body).Decode(&req)) + assert.Equal(t, "/controlplane.v1.OrganizationService/Create", req.Operation) + assert.Equal(t, "user-1", req.UserID) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(operationAuthResponse{Allowed: true}) //nolint:errcheck + })) + defer srv.Close() + + cfg := &conf.OperationAuthorizationProvider{Enabled: true, Url: srv.URL} + m := WithOperationAuthorizationMiddleware(cfg, logHelper) + + ctx := ctxWithOperation(context.Background(), "/controlplane.v1.OrganizationService/Create") + ctx = entities.WithCurrentUser(ctx, &entities.User{ID: "user-1"}) + + result, err := m(passHandler)(ctx, nil) + require.NoError(t, err) + assert.Equal(t, "ok", result) + }) + + t.Run("target operation denied", func(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(operationAuthResponse{Allowed: false, Reason: "org limit reached"}) //nolint:errcheck + })) + defer srv.Close() + + cfg := &conf.OperationAuthorizationProvider{Enabled: true, Url: srv.URL} + m := WithOperationAuthorizationMiddleware(cfg, logHelper) + + ctx := ctxWithOperation(context.Background(), "/controlplane.v1.OrganizationService/Create") + ctx = entities.WithCurrentUser(ctx, &entities.User{ID: "user-1"}) + + result, err := m(passHandler)(ctx, nil) + require.Error(t, err) + assert.Nil(t, result) + assert.Contains(t, err.Error(), "org limit reached") + }) + + t.Run("provider unreachable is fail-closed", func(t *testing.T) { + cfg := &conf.OperationAuthorizationProvider{Enabled: true, Url: "http://127.0.0.1:1"} + m := WithOperationAuthorizationMiddleware(cfg, logHelper) + + ctx := ctxWithOperation(context.Background(), "/controlplane.v1.OrganizationService/Create") + ctx = entities.WithCurrentUser(ctx, &entities.User{ID: "user-1"}) + + result, err := m(passHandler)(ctx, nil) + require.Error(t, err) + assert.Nil(t, result) + assert.Contains(t, err.Error(), "unable to verify operation authorization") + }) + + t.Run("provider returns 500 is fail-closed", func(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + })) + defer srv.Close() + + cfg := &conf.OperationAuthorizationProvider{Enabled: true, Url: srv.URL} + m := WithOperationAuthorizationMiddleware(cfg, logHelper) + + ctx := ctxWithOperation(context.Background(), "/controlplane.v1.OrganizationService/Create") + ctx = entities.WithCurrentUser(ctx, &entities.User{ID: "user-2"}) + + result, err := m(passHandler)(ctx, nil) + require.Error(t, err) + assert.Nil(t, result) + assert.Contains(t, err.Error(), "unable to verify operation authorization") + }) + + t.Run("cache hit skips HTTP call", func(t *testing.T) { + var callCount atomic.Int32 + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + callCount.Add(1) + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(operationAuthResponse{Allowed: true}) //nolint:errcheck + })) + defer srv.Close() + + cfg := &conf.OperationAuthorizationProvider{Enabled: true, Url: srv.URL} + m := WithOperationAuthorizationMiddleware(cfg, logHelper) + + ctx := ctxWithOperation(context.Background(), "/controlplane.v1.OrganizationService/Create") + ctx = entities.WithCurrentUser(ctx, &entities.User{ID: "user-cache"}) + + // First call hits the server + result, err := m(passHandler)(ctx, nil) + require.NoError(t, err) + assert.Equal(t, "ok", result) + assert.Equal(t, int32(1), callCount.Load()) + + // Second call should use cache + result, err = m(passHandler)(ctx, nil) + require.NoError(t, err) + assert.Equal(t, "ok", result) + assert.Equal(t, int32(1), callCount.Load()) + }) +} diff --git a/deployment/chainloop/templates/controlplane/configmap.yaml b/deployment/chainloop/templates/controlplane/configmap.yaml index 4ba11e7fc..97182bcf5 100644 --- a/deployment/chainloop/templates/controlplane/configmap.yaml +++ b/deployment/chainloop/templates/controlplane/configmap.yaml @@ -67,6 +67,10 @@ data: federated_authentication: {{- toYaml .Values.controlplane.federatedAuthentication | nindent 6 }} {{- end }} + {{ if .Values.controlplane.operationAuthorizationProvider.enabled }} + operation_authorization_provider: + {{- toYaml .Values.controlplane.operationAuthorizationProvider | nindent 6 }} + {{- end }} {{- if .Values.controlplane.timestampAuthorities }} tsa.yaml: | timestamp_authorities: diff --git a/deployment/chainloop/values.yaml b/deployment/chainloop/values.yaml index 3b70f1a06..2cbf6d3fe 100644 --- a/deployment/chainloop/values.yaml +++ b/deployment/chainloop/values.yaml @@ -177,6 +177,13 @@ controlplane: enabled: false url: "" + ## @extra controlplane.operationAuthorizationProvider Enable external operation authorization + ## @param controlplane.operationAuthorizationProvider.enabled Enable operation authorization + ## @param controlplane.operationAuthorizationProvider.url URL of the authorization endpoint + operationAuthorizationProvider: + enabled: false + url: "" + ## @skip controlplane.restrictOrgCreation Restrict organization creation to instance admins restrictOrgCreation: false From da3bfbe56920c7114c260a2c5ef4e25afe411f7c Mon Sep 17 00:00:00 2001 From: Sylwester Piskozub Date: Fri, 10 Apr 2026 10:38:08 +0200 Subject: [PATCH 2/7] lint Signed-off-by: Sylwester Piskozub --- .../usercontext/operation_authorization_middleware.go | 2 +- .../operation_authorization_middleware_test.go | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/controlplane/internal/usercontext/operation_authorization_middleware.go b/app/controlplane/internal/usercontext/operation_authorization_middleware.go index d8328badc..cf5535a4b 100644 --- a/app/controlplane/internal/usercontext/operation_authorization_middleware.go +++ b/app/controlplane/internal/usercontext/operation_authorization_middleware.go @@ -63,7 +63,7 @@ func WithOperationAuthorizationMiddleware(conf *conf.OperationAuthorizationProvi // LRU cache with 30s TTL keyed by "user_id:operation" authCache, err := cache.New[*operationAuthResponse]( - cache.WithTTL(30 * time.Second), + cache.WithTTL(30*time.Second), cache.WithDescription("Operation authorization cache"), ) if err != nil { diff --git a/app/controlplane/internal/usercontext/operation_authorization_middleware_test.go b/app/controlplane/internal/usercontext/operation_authorization_middleware_test.go index bd718c32d..bcd4417ef 100644 --- a/app/controlplane/internal/usercontext/operation_authorization_middleware_test.go +++ b/app/controlplane/internal/usercontext/operation_authorization_middleware_test.go @@ -36,11 +36,11 @@ type fakeTransport struct { operation string } -func (f *fakeTransport) Kind() transport.Kind { return transport.KindGRPC } -func (f *fakeTransport) Endpoint() string { return "" } -func (f *fakeTransport) Operation() string { return f.operation } -func (f *fakeTransport) RequestHeader() transport.Header { return nil } -func (f *fakeTransport) ReplyHeader() transport.Header { return nil } +func (f *fakeTransport) Kind() transport.Kind { return transport.KindGRPC } +func (f *fakeTransport) Endpoint() string { return "" } +func (f *fakeTransport) Operation() string { return f.operation } +func (f *fakeTransport) RequestHeader() transport.Header { return nil } +func (f *fakeTransport) ReplyHeader() transport.Header { return nil } func ctxWithOperation(ctx context.Context, op string) context.Context { return transport.NewServerContext(ctx, &fakeTransport{operation: op}) From f00d4df0c4bcf7f07a2eab2e8d3eaad76f8eaa06 Mon Sep 17 00:00:00 2001 From: Sylwester Piskozub Date: Fri, 10 Apr 2026 11:06:21 +0200 Subject: [PATCH 3/7] fix cache key; change ttl Signed-off-by: Sylwester Piskozub --- .../operation_authorization_middleware.go | 6 ++-- ...operation_authorization_middleware_test.go | 35 +++++++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/app/controlplane/internal/usercontext/operation_authorization_middleware.go b/app/controlplane/internal/usercontext/operation_authorization_middleware.go index cf5535a4b..24ebae5f5 100644 --- a/app/controlplane/internal/usercontext/operation_authorization_middleware.go +++ b/app/controlplane/internal/usercontext/operation_authorization_middleware.go @@ -61,9 +61,9 @@ func WithOperationAuthorizationMiddleware(conf *conf.OperationAuthorizationProvi client := &http.Client{Timeout: 5 * time.Second} url := conf.GetUrl() - // LRU cache with 30s TTL keyed by "user_id:operation" + // LRU cache with 10s TTL keyed by "user_id:org_id:operation" authCache, err := cache.New[*operationAuthResponse]( - cache.WithTTL(30*time.Second), + cache.WithTTL(10*time.Second), cache.WithDescription("Operation authorization cache"), ) if err != nil { @@ -93,7 +93,7 @@ func WithOperationAuthorizationMiddleware(conf *conf.OperationAuthorizationProvi orgID = org.ID } - cacheKey := fmt.Sprintf("%s:%s", user.ID, operation) + cacheKey := fmt.Sprintf("%s:%s:%s", user.ID, orgID, operation) // Check cache if authCache != nil { diff --git a/app/controlplane/internal/usercontext/operation_authorization_middleware_test.go b/app/controlplane/internal/usercontext/operation_authorization_middleware_test.go index bcd4417ef..b496c2cfa 100644 --- a/app/controlplane/internal/usercontext/operation_authorization_middleware_test.go +++ b/app/controlplane/internal/usercontext/operation_authorization_middleware_test.go @@ -154,6 +154,41 @@ func TestWithOperationAuthorizationMiddleware(t *testing.T) { assert.Contains(t, err.Error(), "unable to verify operation authorization") }) + t.Run("cache key includes orgID so different orgs are not conflated", func(t *testing.T) { + var callCount atomic.Int32 + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + callCount.Add(1) + var req operationAuthRequest + require.NoError(t, json.NewDecoder(r.Body).Decode(&req)) + + allowed := req.OrganizationID == "org-allowed" + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(operationAuthResponse{Allowed: allowed, Reason: "denied for this org"}) //nolint:errcheck + })) + defer srv.Close() + + cfg := &conf.OperationAuthorizationProvider{Enabled: true, Url: srv.URL} + m := WithOperationAuthorizationMiddleware(cfg, logHelper) + + baseCtx := ctxWithOperation(context.Background(), "/controlplane.v1.OrganizationService/Create") + baseCtx = entities.WithCurrentUser(baseCtx, &entities.User{ID: "user-org-test"}) + + // Call with org-allowed -> should succeed and be cached + ctxAllowed := entities.WithCurrentOrg(baseCtx, &entities.Org{ID: "org-allowed"}) + result, err := m(passHandler)(ctxAllowed, nil) + require.NoError(t, err) + assert.Equal(t, "ok", result) + assert.Equal(t, int32(1), callCount.Load()) + + // Call with org-denied -> must NOT reuse the cached "allowed" result + ctxDenied := entities.WithCurrentOrg(baseCtx, &entities.Org{ID: "org-denied"}) + result, err = m(passHandler)(ctxDenied, nil) + require.Error(t, err) + assert.Nil(t, result) + assert.Contains(t, err.Error(), "denied for this org") + assert.Equal(t, int32(2), callCount.Load(), "expected a second HTTP call for a different org") + }) + t.Run("cache hit skips HTTP call", func(t *testing.T) { var callCount atomic.Int32 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { From 93f4640714a28e0e35682671838c3688f032e7f2 Mon Sep 17 00:00:00 2001 From: Sylwester Piskozub Date: Mon, 13 Apr 2026 23:30:35 +0200 Subject: [PATCH 4/7] remove cache; refactor Signed-off-by: Sylwester Piskozub --- .../operation_authorization_middleware.go | 42 ++----- ...operation_authorization_middleware_test.go | 70 ++++++------ app/controlplane/pkg/authz/authz.go | 105 ++++++++++-------- .../pkg/authz/middleware/middleware.go | 10 +- 4 files changed, 107 insertions(+), 120 deletions(-) diff --git a/app/controlplane/internal/usercontext/operation_authorization_middleware.go b/app/controlplane/internal/usercontext/operation_authorization_middleware.go index 24ebae5f5..e77bccab5 100644 --- a/app/controlplane/internal/usercontext/operation_authorization_middleware.go +++ b/app/controlplane/internal/usercontext/operation_authorization_middleware.go @@ -25,19 +25,13 @@ import ( conf "github.com/chainloop-dev/chainloop/app/controlplane/internal/conf/controlplane/config/v1" "github.com/chainloop-dev/chainloop/app/controlplane/internal/usercontext/entities" + "github.com/chainloop-dev/chainloop/app/controlplane/pkg/authz" errorsAPI "github.com/go-kratos/kratos/v2/errors" "github.com/go-kratos/kratos/v2/log" "github.com/go-kratos/kratos/v2/middleware" "github.com/go-kratos/kratos/v2/transport" - - "github.com/chainloop-dev/chainloop/pkg/cache" ) -// Operations that require authorization checks -var operationAuthTargets = map[string]bool{ - "/controlplane.v1.OrganizationService/Create": true, -} - type operationAuthRequest struct { Operation string `json:"operation"` UserID string `json:"user_id"` @@ -61,16 +55,6 @@ func WithOperationAuthorizationMiddleware(conf *conf.OperationAuthorizationProvi client := &http.Client{Timeout: 5 * time.Second} url := conf.GetUrl() - // LRU cache with 10s TTL keyed by "user_id:org_id:operation" - authCache, err := cache.New[*operationAuthResponse]( - cache.WithTTL(10*time.Second), - cache.WithDescription("Operation authorization cache"), - ) - if err != nil { - logger.Warnw("msg", "failed to create operation auth cache, proceeding without cache", "error", err) - authCache = nil - } - return func(handler middleware.Handler) middleware.Handler { return func(ctx context.Context, req interface{}) (interface{}, error) { info, ok := transport.FromServerContext(ctx) @@ -79,7 +63,7 @@ func WithOperationAuthorizationMiddleware(conf *conf.OperationAuthorizationProvi } operation := info.Operation() - if !operationAuthTargets[operation] { + if !authz.RequiresExternalAuthz(operation) { return handler(ctx, req) } @@ -93,18 +77,6 @@ func WithOperationAuthorizationMiddleware(conf *conf.OperationAuthorizationProvi orgID = org.ID } - cacheKey := fmt.Sprintf("%s:%s:%s", user.ID, orgID, operation) - - // Check cache - if authCache != nil { - if cached, found, _ := authCache.Get(ctx, cacheKey); found { - if !cached.Allowed { - return nil, errorsAPI.Forbidden("operation_denied", cached.Reason) - } - return handler(ctx, req) - } - } - result, err := callAuthorizationEndpoint(ctx, client, url, &operationAuthRequest{ Operation: operation, UserID: user.ID, @@ -115,11 +87,6 @@ func WithOperationAuthorizationMiddleware(conf *conf.OperationAuthorizationProvi return nil, errorsAPI.Forbidden("operation_denied", "unable to verify operation authorization") } - // Cache the result - if authCache != nil { - _ = authCache.Set(ctx, cacheKey, result) - } - if !result.Allowed { return nil, errorsAPI.Forbidden("operation_denied", result.Reason) } @@ -141,6 +108,11 @@ func callAuthorizationEndpoint(ctx context.Context, client *http.Client, url str } httpReq.Header.Set("Content-Type", "application/json") + // Forward the Bearer token from the incoming request + if rawToken, err := entities.GetRawToken(ctx); err == nil && rawToken != "" { + httpReq.Header.Set("Authorization", fmt.Sprintf("Bearer %s", rawToken)) + } + resp, err := client.Do(httpReq) if err != nil { return nil, fmt.Errorf("calling authorization endpoint: %w", err) diff --git a/app/controlplane/internal/usercontext/operation_authorization_middleware_test.go b/app/controlplane/internal/usercontext/operation_authorization_middleware_test.go index b496c2cfa..0280fed64 100644 --- a/app/controlplane/internal/usercontext/operation_authorization_middleware_test.go +++ b/app/controlplane/internal/usercontext/operation_authorization_middleware_test.go @@ -34,16 +34,31 @@ import ( // fakeTransport implements transport.Transporter for testing middleware operation matching. type fakeTransport struct { operation string + header headerCarrier } +type headerCarrier http.Header + +func (h headerCarrier) Get(key string) string { return http.Header(h).Get(key) } +func (h headerCarrier) Set(key, value string) { http.Header(h).Set(key, value) } +func (h headerCarrier) Add(key, value string) { http.Header(h).Add(key, value) } +func (h headerCarrier) Keys() []string { + keys := make([]string, 0, len(h)) + for k := range h { + keys = append(keys, k) + } + return keys +} +func (h headerCarrier) Values(key string) []string { return http.Header(h).Values(key) } + func (f *fakeTransport) Kind() transport.Kind { return transport.KindGRPC } func (f *fakeTransport) Endpoint() string { return "" } func (f *fakeTransport) Operation() string { return f.operation } -func (f *fakeTransport) RequestHeader() transport.Header { return nil } +func (f *fakeTransport) RequestHeader() transport.Header { return f.header } func (f *fakeTransport) ReplyHeader() transport.Header { return nil } func ctxWithOperation(ctx context.Context, op string) context.Context { - return transport.NewServerContext(ctx, &fakeTransport{operation: op}) + return transport.NewServerContext(ctx, &fakeTransport{operation: op, header: headerCarrier{}}) } func TestWithOperationAuthorizationMiddleware(t *testing.T) { @@ -154,45 +169,33 @@ func TestWithOperationAuthorizationMiddleware(t *testing.T) { assert.Contains(t, err.Error(), "unable to verify operation authorization") }) - t.Run("cache key includes orgID so different orgs are not conflated", func(t *testing.T) { - var callCount atomic.Int32 + t.Run("organization ID is forwarded", func(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - callCount.Add(1) var req operationAuthRequest require.NoError(t, json.NewDecoder(r.Body).Decode(&req)) + assert.Equal(t, "org-123", req.OrganizationID) - allowed := req.OrganizationID == "org-allowed" w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(operationAuthResponse{Allowed: allowed, Reason: "denied for this org"}) //nolint:errcheck + json.NewEncoder(w).Encode(operationAuthResponse{Allowed: true}) //nolint:errcheck })) defer srv.Close() cfg := &conf.OperationAuthorizationProvider{Enabled: true, Url: srv.URL} m := WithOperationAuthorizationMiddleware(cfg, logHelper) - baseCtx := ctxWithOperation(context.Background(), "/controlplane.v1.OrganizationService/Create") - baseCtx = entities.WithCurrentUser(baseCtx, &entities.User{ID: "user-org-test"}) + ctx := ctxWithOperation(context.Background(), "/controlplane.v1.OrganizationService/Create") + ctx = entities.WithCurrentUser(ctx, &entities.User{ID: "user-1"}) + ctx = entities.WithCurrentOrg(ctx, &entities.Org{ID: "org-123"}) - // Call with org-allowed -> should succeed and be cached - ctxAllowed := entities.WithCurrentOrg(baseCtx, &entities.Org{ID: "org-allowed"}) - result, err := m(passHandler)(ctxAllowed, nil) + result, err := m(passHandler)(ctx, nil) require.NoError(t, err) assert.Equal(t, "ok", result) - assert.Equal(t, int32(1), callCount.Load()) - - // Call with org-denied -> must NOT reuse the cached "allowed" result - ctxDenied := entities.WithCurrentOrg(baseCtx, &entities.Org{ID: "org-denied"}) - result, err = m(passHandler)(ctxDenied, nil) - require.Error(t, err) - assert.Nil(t, result) - assert.Contains(t, err.Error(), "denied for this org") - assert.Equal(t, int32(2), callCount.Load(), "expected a second HTTP call for a different org") }) - t.Run("cache hit skips HTTP call", func(t *testing.T) { - var callCount atomic.Int32 - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - callCount.Add(1) + t.Run("bearer token is forwarded", func(t *testing.T) { + var gotAuth string + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotAuth = r.Header.Get("Authorization") w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(operationAuthResponse{Allowed: true}) //nolint:errcheck })) @@ -201,19 +204,16 @@ func TestWithOperationAuthorizationMiddleware(t *testing.T) { cfg := &conf.OperationAuthorizationProvider{Enabled: true, Url: srv.URL} m := WithOperationAuthorizationMiddleware(cfg, logHelper) - ctx := ctxWithOperation(context.Background(), "/controlplane.v1.OrganizationService/Create") - ctx = entities.WithCurrentUser(ctx, &entities.User{ID: "user-cache"}) + ft := &fakeTransport{ + operation: "/controlplane.v1.OrganizationService/Create", + header: headerCarrier(http.Header{"Authorization": []string{"Bearer test-token-123"}}), + } + ctx := transport.NewServerContext(context.Background(), ft) + ctx = entities.WithCurrentUser(ctx, &entities.User{ID: "user-1"}) - // First call hits the server result, err := m(passHandler)(ctx, nil) require.NoError(t, err) assert.Equal(t, "ok", result) - assert.Equal(t, int32(1), callCount.Load()) - - // Second call should use cache - result, err = m(passHandler)(ctx, nil) - require.NoError(t, err) - assert.Equal(t, "ok", result) - assert.Equal(t, int32(1), callCount.Load()) + assert.Equal(t, "Bearer test-token-123", gotAuth) }) } diff --git a/app/controlplane/pkg/authz/authz.go b/app/controlplane/pkg/authz/authz.go index 3a2adee97..d6e9aa228 100644 --- a/app/controlplane/pkg/authz/authz.go +++ b/app/controlplane/pkg/authz/authz.go @@ -22,6 +22,13 @@ type Policy struct { Action string } +// OperationPolicy describes both the local Casbin policies required for an operation +// and whether the operation also requires external authorization. +type OperationPolicy struct { + Policies []*Policy + ExternalAuthz bool +} + type Role string // RBACEnabled returns whether an org-scoped role has RBAC enabled and needs resource-scoped enforcement. @@ -351,67 +358,67 @@ var RolesMap = map[Role][]*Policy{ }, } -// ServerOperationsMap is a map of server operations to the ResourceAction tuples that are -// required to perform the operation -// If it contains more than one policy, all of them need to be true -var ServerOperationsMap = map[string][]*Policy{ +// ServerOperationsMap is a map of server operations to their authorization requirements. +// Each entry specifies the Casbin policies required (all must pass) and whether the +// operation also requires external authorization. +var ServerOperationsMap = map[string]*OperationPolicy{ // Discover endpoint - "/controlplane.v1.ReferrerService/DiscoverPrivate": {PolicyReferrerRead}, + "/controlplane.v1.ReferrerService/DiscoverPrivate": {Policies: []*Policy{PolicyReferrerRead}}, // Download/Uploading artifacts // There are no policies for the download endpoint, we do a manual check in the service layer // to differentiate between upload and download requests "/controlplane.v1.CASCredentialsService/Get": {}, // We have an endpoint to generate a download URL - "/controlplane.v1.CASRedirectService/DownloadRedirect": {PolicyArtifactDownload}, + "/controlplane.v1.CASRedirectService/DownloadRedirect": {Policies: []*Policy{PolicyArtifactDownload}}, // Or to retrieve a download url - "/controlplane.v1.CASRedirectService/GetDownloadURL": {PolicyArtifactDownload}, + "/controlplane.v1.CASRedirectService/GetDownloadURL": {Policies: []*Policy{PolicyArtifactDownload}}, // CAS Backend listing - "/controlplane.v1.CASBackendService/List": {PolicyCASBackendList}, - "/controlplane.v1.CASBackendService/Revalidate": {PolicyCASBackendUpdate}, - "/controlplane.v1.CASBackendService/Create": {PolicyCASBackendCreate}, + "/controlplane.v1.CASBackendService/List": {Policies: []*Policy{PolicyCASBackendList}}, + "/controlplane.v1.CASBackendService/Revalidate": {Policies: []*Policy{PolicyCASBackendUpdate}}, + "/controlplane.v1.CASBackendService/Create": {Policies: []*Policy{PolicyCASBackendCreate}}, // Available integrations - "/controlplane.v1.IntegrationsService/ListAvailable": {PolicyAvailableIntegrationList, PolicyAvailableIntegrationRead}, + "/controlplane.v1.IntegrationsService/ListAvailable": {Policies: []*Policy{PolicyAvailableIntegrationList, PolicyAvailableIntegrationRead}}, // Registered integrations - "/controlplane.v1.IntegrationsService/ListRegistrations": {PolicyRegisteredIntegrationList}, - "/controlplane.v1.IntegrationsService/DescribeRegistration": {PolicyRegisteredIntegrationRead}, - "/controlplane.v1.IntegrationsService/Register": {PolicyRegisteredIntegrationAdd}, + "/controlplane.v1.IntegrationsService/ListRegistrations": {Policies: []*Policy{PolicyRegisteredIntegrationList}}, + "/controlplane.v1.IntegrationsService/DescribeRegistration": {Policies: []*Policy{PolicyRegisteredIntegrationRead}}, + "/controlplane.v1.IntegrationsService/Register": {Policies: []*Policy{PolicyRegisteredIntegrationAdd}}, // Attached integrations - "/controlplane.v1.IntegrationsService/ListAttachments": {PolicyAttachedIntegrationList}, - "/controlplane.v1.IntegrationsService/Attach": {PolicyAttachedIntegrationAttach}, - "/controlplane.v1.IntegrationsService/Detach": {PolicyAttachedIntegrationDetach}, + "/controlplane.v1.IntegrationsService/ListAttachments": {Policies: []*Policy{PolicyAttachedIntegrationList}}, + "/controlplane.v1.IntegrationsService/Attach": {Policies: []*Policy{PolicyAttachedIntegrationAttach}}, + "/controlplane.v1.IntegrationsService/Detach": {Policies: []*Policy{PolicyAttachedIntegrationDetach}}, // Metrics - "/controlplane.v1.OrgMetricsService/.*": {PolicyOrgMetricsRead}, + "/controlplane.v1.OrgMetricsService/.*": {Policies: []*Policy{PolicyOrgMetricsRead}}, // Robot Account - "/controlplane.v1.RobotAccountService/List": {PolicyRobotAccountList}, - "/controlplane.v1.RobotAccountService/Create": {PolicyRobotAccountCreate}, + "/controlplane.v1.RobotAccountService/List": {Policies: []*Policy{PolicyRobotAccountList}}, + "/controlplane.v1.RobotAccountService/Create": {Policies: []*Policy{PolicyRobotAccountCreate}}, // Workflows - "/controlplane.v1.WorkflowService/List": {PolicyWorkflowList}, - "/controlplane.v1.WorkflowService/View": {PolicyWorkflowRead}, - "/controlplane.v1.WorkflowService/Create": {PolicyWorkflowCreate}, - "/controlplane.v1.WorkflowService/Update": {PolicyWorkflowUpdate}, - "/controlplane.v1.WorkflowService/Delete": {PolicyWorkflowDelete}, + "/controlplane.v1.WorkflowService/List": {Policies: []*Policy{PolicyWorkflowList}}, + "/controlplane.v1.WorkflowService/View": {Policies: []*Policy{PolicyWorkflowRead}}, + "/controlplane.v1.WorkflowService/Create": {Policies: []*Policy{PolicyWorkflowCreate}}, + "/controlplane.v1.WorkflowService/Update": {Policies: []*Policy{PolicyWorkflowUpdate}}, + "/controlplane.v1.WorkflowService/Delete": {Policies: []*Policy{PolicyWorkflowDelete}}, // WorkflowRun - "/controlplane.v1.WorkflowRunService/List": {PolicyWorkflowRunList}, - "/controlplane.v1.WorkflowRunService/View": {PolicyWorkflowRunRead}, + "/controlplane.v1.WorkflowRunService/List": {Policies: []*Policy{PolicyWorkflowRunList}}, + "/controlplane.v1.WorkflowRunService/View": {Policies: []*Policy{PolicyWorkflowRunRead}}, // Workflow Contracts - "/controlplane.v1.WorkflowContractService/List": {PolicyWorkflowContractList}, - "/controlplane.v1.WorkflowContractService/Describe": {PolicyWorkflowContractRead}, - "/controlplane.v1.WorkflowContractService/Update": {PolicyWorkflowContractUpdate}, - "/controlplane.v1.WorkflowContractService/Create": {PolicyWorkflowContractCreate}, - "/controlplane.v1.WorkflowContractService/Delete": {PolicyWorkflowContractDelete}, + "/controlplane.v1.WorkflowContractService/List": {Policies: []*Policy{PolicyWorkflowContractList}}, + "/controlplane.v1.WorkflowContractService/Describe": {Policies: []*Policy{PolicyWorkflowContractRead}}, + "/controlplane.v1.WorkflowContractService/Update": {Policies: []*Policy{PolicyWorkflowContractUpdate}}, + "/controlplane.v1.WorkflowContractService/Create": {Policies: []*Policy{PolicyWorkflowContractCreate}}, + "/controlplane.v1.WorkflowContractService/Delete": {Policies: []*Policy{PolicyWorkflowContractDelete}}, // Get current information about an organization - "/controlplane.v1.ContextService/Current": {PolicyOrganizationRead}, + "/controlplane.v1.ContextService/Current": {Policies: []*Policy{PolicyOrganizationRead}}, // Listing, create or selecting an organization does not have any required permissions, // since all the permissions here are in the context of an organization // Create new organization. No user required at middleware level. The endpoint will handle - // it based on diverse conditions - "/controlplane.v1.OrganizationService/Create": {}, + // it based on diverse conditions. Requires external authorization when configured. + "/controlplane.v1.OrganizationService/Create": {ExternalAuthz: true}, // Delete an organization makes checks at the service level since the // user can explicitly set the org they want to delete and might not be the current one "/controlplane.v1.OrganizationService/Delete": {}, // List global memberships - "/controlplane.v1.OrganizationService/ListMemberships": {PolicyOrganizationListMemberships}, + "/controlplane.v1.OrganizationService/ListMemberships": {Policies: []*Policy{PolicyOrganizationListMemberships}}, // NOTE: this is about listing my own memberships, not about listing all the memberships in the organization "/controlplane.v1.UserService/ListMemberships": {}, @@ -436,19 +443,27 @@ var ServerOperationsMap = map[string][]*Policy{ "/controlplane.v1.GroupService/UpdateMemberMaintainerStatus": {}, // Project Memberships - "/controlplane.v1.ProjectService/ListMembers": {PolicyProjectListMemberships}, - "/controlplane.v1.ProjectService/AddMember": {PolicyProjectAddMemberships}, - "/controlplane.v1.ProjectService/RemoveMember": {PolicyProjectRemoveMemberships}, - "/controlplane.v1.ProjectService/UpdateMemberRole": {PolicyProjectUpdateMemberships}, - "/controlplane.v1.ProjectService/ListPendingInvitations": {PolicyProjectListMemberships}, + "/controlplane.v1.ProjectService/ListMembers": {Policies: []*Policy{PolicyProjectListMemberships}}, + "/controlplane.v1.ProjectService/AddMember": {Policies: []*Policy{PolicyProjectAddMemberships}}, + "/controlplane.v1.ProjectService/RemoveMember": {Policies: []*Policy{PolicyProjectRemoveMemberships}}, + "/controlplane.v1.ProjectService/UpdateMemberRole": {Policies: []*Policy{PolicyProjectUpdateMemberships}}, + "/controlplane.v1.ProjectService/ListPendingInvitations": {Policies: []*Policy{PolicyProjectListMemberships}}, // API tokens RBAC are handled at the service level - "/controlplane.v1.APITokenService/List": {PolicyAPITokenList}, - "/controlplane.v1.APITokenService/Create": {PolicyAPITokenCreate}, - "/controlplane.v1.APITokenService/Revoke": {PolicyAPITokenRevoke}, + "/controlplane.v1.APITokenService/List": {Policies: []*Policy{PolicyAPITokenList}}, + "/controlplane.v1.APITokenService/Create": {Policies: []*Policy{PolicyAPITokenCreate}}, + "/controlplane.v1.APITokenService/Revoke": {Policies: []*Policy{PolicyAPITokenRevoke}}, // Org invitations - "/controlplane.v1.OrgInvitationService/Create": {PolicyOrganizationInvitationsCreate}, + "/controlplane.v1.OrgInvitationService/Create": {Policies: []*Policy{PolicyOrganizationInvitationsCreate}}, +} + +// RequiresExternalAuthz returns whether the given operation requires external authorization. +func RequiresExternalAuthz(operation string) bool { + if entry, ok := ServerOperationsMap[operation]; ok { + return entry.ExternalAuthz + } + return false } // Implements https://pkg.go.dev/entgo.io/ent/schema/field#EnumValues diff --git a/app/controlplane/pkg/authz/middleware/middleware.go b/app/controlplane/pkg/authz/middleware/middleware.go index a55afb294..107625d3d 100644 --- a/app/controlplane/pkg/authz/middleware/middleware.go +++ b/app/controlplane/pkg/authz/middleware/middleware.go @@ -1,5 +1,5 @@ // -// Copyright 2024-2025 The Chainloop Authors. +// Copyright 2024-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -103,17 +103,17 @@ func checkPolicies(ctx context.Context, subject, apiOperation string, enforcer E // 2 - if there is not, it runs a regex match in each key in case one of those keys contains a regex func policiesLookup(apiOperation string) ([]*authz.Policy, error) { // Direct match - policies, found := authz.ServerOperationsMap[apiOperation] + entry, found := authz.ServerOperationsMap[apiOperation] if found { - return policies, nil + return entry.Policies, nil } // second pass trying to match a regex // i.e "/controlplane.v1.OrgMetricsService/.*" -> "/controlplane.v1.OrgMetricsService/Totals" - for k, policies := range authz.ServerOperationsMap { + for k, entry := range authz.ServerOperationsMap { found, _ := regexp.MatchString(k, apiOperation) if found { - return policies, nil + return entry.Policies, nil } } From 7d9fa7a022fc4ef591f2e7661d5642d7b22a3207 Mon Sep 17 00:00:00 2001 From: Sylwester Piskozub Date: Mon, 13 Apr 2026 23:54:33 +0200 Subject: [PATCH 5/7] bump chart Signed-off-by: Sylwester Piskozub --- deployment/chainloop/Chart.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/chainloop/Chart.yaml b/deployment/chainloop/Chart.yaml index cb4081948..ab8f68da2 100644 --- a/deployment/chainloop/Chart.yaml +++ b/deployment/chainloop/Chart.yaml @@ -7,7 +7,7 @@ description: Chainloop is an open source software supply chain control plane, a type: application # Bump the patch (not minor, not major) version on each change in the Chart Source code -version: 1.365.0 +version: 1.365.1 # Do not update appVersion, this is handled automatically by the release process appVersion: v1.91.0 From 4c893db435028ac7f42ae6a67a2dcbf0392da2a2 Mon Sep 17 00:00:00 2001 From: Sylwester Piskozub Date: Mon, 13 Apr 2026 23:59:24 +0200 Subject: [PATCH 6/7] lint Signed-off-by: Sylwester Piskozub --- .../usercontext/operation_authorization_middleware_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/controlplane/internal/usercontext/operation_authorization_middleware_test.go b/app/controlplane/internal/usercontext/operation_authorization_middleware_test.go index 0280fed64..d56e19e33 100644 --- a/app/controlplane/internal/usercontext/operation_authorization_middleware_test.go +++ b/app/controlplane/internal/usercontext/operation_authorization_middleware_test.go @@ -39,9 +39,9 @@ type fakeTransport struct { type headerCarrier http.Header -func (h headerCarrier) Get(key string) string { return http.Header(h).Get(key) } -func (h headerCarrier) Set(key, value string) { http.Header(h).Set(key, value) } -func (h headerCarrier) Add(key, value string) { http.Header(h).Add(key, value) } +func (h headerCarrier) Get(key string) string { return http.Header(h).Get(key) } +func (h headerCarrier) Set(key, value string) { http.Header(h).Set(key, value) } +func (h headerCarrier) Add(key, value string) { http.Header(h).Add(key, value) } func (h headerCarrier) Keys() []string { keys := make([]string, 0, len(h)) for k := range h { From f4a24935e433823eb38d6f684bab223dd36b5636 Mon Sep 17 00:00:00 2001 From: Sylwester Piskozub Date: Tue, 14 Apr 2026 07:25:20 +0200 Subject: [PATCH 7/7] remove user and org from req Signed-off-by: Sylwester Piskozub --- .../operation_authorization_middleware.go | 18 ++--------- ...operation_authorization_middleware_test.go | 31 ------------------- 2 files changed, 2 insertions(+), 47 deletions(-) diff --git a/app/controlplane/internal/usercontext/operation_authorization_middleware.go b/app/controlplane/internal/usercontext/operation_authorization_middleware.go index e77bccab5..490acb2fc 100644 --- a/app/controlplane/internal/usercontext/operation_authorization_middleware.go +++ b/app/controlplane/internal/usercontext/operation_authorization_middleware.go @@ -33,9 +33,7 @@ import ( ) type operationAuthRequest struct { - Operation string `json:"operation"` - UserID string `json:"user_id"` - OrganizationID string `json:"organization_id,omitempty"` + Operation string `json:"operation"` } type operationAuthResponse struct { @@ -67,20 +65,8 @@ func WithOperationAuthorizationMiddleware(conf *conf.OperationAuthorizationProvi return handler(ctx, req) } - user := entities.CurrentUser(ctx) - if user == nil { - return handler(ctx, req) - } - - var orgID string - if org := entities.CurrentOrg(ctx); org != nil { - orgID = org.ID - } - result, err := callAuthorizationEndpoint(ctx, client, url, &operationAuthRequest{ - Operation: operation, - UserID: user.ID, - OrganizationID: orgID, + Operation: operation, }) if err != nil { logger.Errorw("msg", "operation authorization call failed, denying request (fail-closed)", "error", err, "operation", operation) diff --git a/app/controlplane/internal/usercontext/operation_authorization_middleware_test.go b/app/controlplane/internal/usercontext/operation_authorization_middleware_test.go index d56e19e33..b6afe706f 100644 --- a/app/controlplane/internal/usercontext/operation_authorization_middleware_test.go +++ b/app/controlplane/internal/usercontext/operation_authorization_middleware_test.go @@ -24,7 +24,6 @@ import ( "testing" conf "github.com/chainloop-dev/chainloop/app/controlplane/internal/conf/controlplane/config/v1" - "github.com/chainloop-dev/chainloop/app/controlplane/internal/usercontext/entities" "github.com/go-kratos/kratos/v2/log" "github.com/go-kratos/kratos/v2/transport" "github.com/stretchr/testify/assert" @@ -88,7 +87,6 @@ func TestWithOperationAuthorizationMiddleware(t *testing.T) { m := WithOperationAuthorizationMiddleware(cfg, logHelper) ctx := ctxWithOperation(context.Background(), "/controlplane.v1.WorkflowService/List") - ctx = entities.WithCurrentUser(ctx, &entities.User{ID: "user-1"}) result, err := m(passHandler)(ctx, nil) require.NoError(t, err) @@ -101,7 +99,6 @@ func TestWithOperationAuthorizationMiddleware(t *testing.T) { var req operationAuthRequest require.NoError(t, json.NewDecoder(r.Body).Decode(&req)) assert.Equal(t, "/controlplane.v1.OrganizationService/Create", req.Operation) - assert.Equal(t, "user-1", req.UserID) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(operationAuthResponse{Allowed: true}) //nolint:errcheck @@ -112,7 +109,6 @@ func TestWithOperationAuthorizationMiddleware(t *testing.T) { m := WithOperationAuthorizationMiddleware(cfg, logHelper) ctx := ctxWithOperation(context.Background(), "/controlplane.v1.OrganizationService/Create") - ctx = entities.WithCurrentUser(ctx, &entities.User{ID: "user-1"}) result, err := m(passHandler)(ctx, nil) require.NoError(t, err) @@ -130,7 +126,6 @@ func TestWithOperationAuthorizationMiddleware(t *testing.T) { m := WithOperationAuthorizationMiddleware(cfg, logHelper) ctx := ctxWithOperation(context.Background(), "/controlplane.v1.OrganizationService/Create") - ctx = entities.WithCurrentUser(ctx, &entities.User{ID: "user-1"}) result, err := m(passHandler)(ctx, nil) require.Error(t, err) @@ -143,7 +138,6 @@ func TestWithOperationAuthorizationMiddleware(t *testing.T) { m := WithOperationAuthorizationMiddleware(cfg, logHelper) ctx := ctxWithOperation(context.Background(), "/controlplane.v1.OrganizationService/Create") - ctx = entities.WithCurrentUser(ctx, &entities.User{ID: "user-1"}) result, err := m(passHandler)(ctx, nil) require.Error(t, err) @@ -161,7 +155,6 @@ func TestWithOperationAuthorizationMiddleware(t *testing.T) { m := WithOperationAuthorizationMiddleware(cfg, logHelper) ctx := ctxWithOperation(context.Background(), "/controlplane.v1.OrganizationService/Create") - ctx = entities.WithCurrentUser(ctx, &entities.User{ID: "user-2"}) result, err := m(passHandler)(ctx, nil) require.Error(t, err) @@ -169,29 +162,6 @@ func TestWithOperationAuthorizationMiddleware(t *testing.T) { assert.Contains(t, err.Error(), "unable to verify operation authorization") }) - t.Run("organization ID is forwarded", func(t *testing.T) { - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - var req operationAuthRequest - require.NoError(t, json.NewDecoder(r.Body).Decode(&req)) - assert.Equal(t, "org-123", req.OrganizationID) - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(operationAuthResponse{Allowed: true}) //nolint:errcheck - })) - defer srv.Close() - - cfg := &conf.OperationAuthorizationProvider{Enabled: true, Url: srv.URL} - m := WithOperationAuthorizationMiddleware(cfg, logHelper) - - ctx := ctxWithOperation(context.Background(), "/controlplane.v1.OrganizationService/Create") - ctx = entities.WithCurrentUser(ctx, &entities.User{ID: "user-1"}) - ctx = entities.WithCurrentOrg(ctx, &entities.Org{ID: "org-123"}) - - result, err := m(passHandler)(ctx, nil) - require.NoError(t, err) - assert.Equal(t, "ok", result) - }) - t.Run("bearer token is forwarded", func(t *testing.T) { var gotAuth string srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -209,7 +179,6 @@ func TestWithOperationAuthorizationMiddleware(t *testing.T) { header: headerCarrier(http.Header{"Authorization": []string{"Bearer test-token-123"}}), } ctx := transport.NewServerContext(context.Background(), ft) - ctx = entities.WithCurrentUser(ctx, &entities.User{ID: "user-1"}) result, err := m(passHandler)(ctx, nil) require.NoError(t, err)