From 7248767cac9e7355d93a6ebc3a4634722efbbe59 Mon Sep 17 00:00:00 2001 From: Sylwester Piskozub Date: Fri, 10 Apr 2026 09:49:47 +0200 Subject: [PATCH 1/3] 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 124e1302f..0e37ab264 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 { @@ -953,7 +1016,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) } @@ -965,7 +1028,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 { @@ -1025,7 +1088,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) } @@ -1037,7 +1100,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 { @@ -1081,7 +1144,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) } @@ -1093,7 +1156,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 { @@ -1106,7 +1169,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 { @@ -1148,7 +1211,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) } @@ -1160,7 +1223,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 { @@ -1173,7 +1236,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 { @@ -1202,7 +1265,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) } @@ -1214,7 +1277,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 { @@ -1227,7 +1290,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 { @@ -1274,7 +1337,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) } @@ -1286,7 +1349,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 { @@ -1299,7 +1362,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 { @@ -1351,7 +1414,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) } @@ -1363,7 +1426,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 { @@ -1376,7 +1439,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 { @@ -1418,7 +1481,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) } @@ -1430,7 +1493,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 { @@ -1443,7 +1506,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 { @@ -1484,7 +1547,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) } @@ -1496,7 +1559,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 { @@ -1509,7 +1572,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 { @@ -1565,7 +1628,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\"\x9c\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\"\x9f\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" + @@ -1591,7 +1654,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" + @@ -1606,7 +1670,10 @@ const file_controlplane_config_v1_conf_proto_rawDesc = "" + "NatsServer\x12\x19\n" + "\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\x05tokenB\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" + @@ -1696,69 +1763,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() } @@ -1766,11 +1835,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{} @@ -1779,7 +1848,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 1cf70681d..cdcaa5453 100644 --- a/app/controlplane/internal/conf/controlplane/config/v1/conf.proto +++ b/app/controlplane/internal/conf/controlplane/config/v1/conf.proto @@ -105,6 +105,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 701f692dd..5dcaaaa88 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 580a095bda112417342d5ba15f194c3a94f6d4be Mon Sep 17 00:00:00 2001 From: Sylwester Piskozub Date: Fri, 10 Apr 2026 10:38:08 +0200 Subject: [PATCH 2/3] 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 0984be83d53d323aca7cb39e36a1ed0f623ed30a Mon Sep 17 00:00:00 2001 From: Sylwester Piskozub Date: Fri, 10 Apr 2026 11:06:21 +0200 Subject: [PATCH 3/3] 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) {