From f41b682036e22fd5771de107a9fff365d75d3ebb Mon Sep 17 00:00:00 2001 From: Joshua Blanch Date: Wed, 13 May 2026 20:21:33 +0000 Subject: [PATCH] add authenticated whoami RPC Expose whoami through the existing protobuf/gRPC auth service so identity inspection uses the same auth path as the rest of the API Signed-off-by: Joshua Blanch --- api/auth.proto | 9 ++ api/gen/grpc/go/auth.pb.go | 228 ++++++++++++++++++++++------- api/gen/grpc/go/auth.pb.gw.go | 57 ++++++++ api/gen/grpc/go/auth_grpc.pb.go | 38 +++++ api/http.yaml | 3 + api/openapi/ceph-api.swagger.json | 51 +++++++ pkg/api/auth_api_handlers.go | 36 ++++- pkg/api/auth_api_handlers_test.go | 45 ++++++ pkg/auth/discovery_handler_test.go | 3 +- pkg/auth/grpc_interceptor.go | 3 + pkg/ctx/context.go | 33 +++++ 11 files changed, 442 insertions(+), 64 deletions(-) create mode 100644 pkg/api/auth_api_handlers_test.go diff --git a/api/auth.proto b/api/auth.proto index a5af393..91479cc 100644 --- a/api/auth.proto +++ b/api/auth.proto @@ -13,6 +13,7 @@ service Auth { rpc Login(LoginReq) returns (LoginResp); rpc Logout(google.protobuf.Empty) returns(google.protobuf.Empty); rpc Check(TokenCheckReq)returns(TokenCheckResp); + rpc Whoami(google.protobuf.Empty) returns(WhoamiResp); } message LoginReq{ @@ -41,6 +42,14 @@ message TokenCheckResp{ map permissions=5 ; } +message WhoamiResp{ + string subject =1; + string auth_type =2; + string api_key_id =3; + repeated string roles =4; + map permissions=5 ; +} + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { info: { title: "Ceph management API"; diff --git a/api/gen/grpc/go/auth.pb.go b/api/gen/grpc/go/auth.pb.go index 8f0e6cb..fd78d40 100644 --- a/api/gen/grpc/go/auth.pb.go +++ b/api/gen/grpc/go/auth.pb.go @@ -292,6 +292,85 @@ func (x *TokenCheckResp) GetPermissions() map[string]*structpb.ListValue { return nil } +type WhoamiResp struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Subject string `protobuf:"bytes,1,opt,name=subject,proto3" json:"subject,omitempty"` + AuthType string `protobuf:"bytes,2,opt,name=auth_type,json=authType,proto3" json:"auth_type,omitempty"` + ApiKeyId string `protobuf:"bytes,3,opt,name=api_key_id,json=apiKeyId,proto3" json:"api_key_id,omitempty"` + Roles []string `protobuf:"bytes,4,rep,name=roles,proto3" json:"roles,omitempty"` + Permissions map[string]*structpb.ListValue `protobuf:"bytes,5,rep,name=permissions,proto3" json:"permissions,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` +} + +func (x *WhoamiResp) Reset() { + *x = WhoamiResp{} + if protoimpl.UnsafeEnabled { + mi := &file_auth_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *WhoamiResp) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WhoamiResp) ProtoMessage() {} + +func (x *WhoamiResp) ProtoReflect() protoreflect.Message { + mi := &file_auth_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WhoamiResp.ProtoReflect.Descriptor instead. +func (*WhoamiResp) Descriptor() ([]byte, []int) { + return file_auth_proto_rawDescGZIP(), []int{4} +} + +func (x *WhoamiResp) GetSubject() string { + if x != nil { + return x.Subject + } + return "" +} + +func (x *WhoamiResp) GetAuthType() string { + if x != nil { + return x.AuthType + } + return "" +} + +func (x *WhoamiResp) GetApiKeyId() string { + if x != nil { + return x.ApiKeyId + } + return "" +} + +func (x *WhoamiResp) GetRoles() []string { + if x != nil { + return x.Roles + } + return nil +} + +func (x *WhoamiResp) GetPermissions() map[string]*structpb.ListValue { + if x != nil { + return x.Permissions + } + return nil +} + var File_auth_proto protoreflect.FileDescriptor var file_auth_proto_rawDesc = []byte{ @@ -360,38 +439,59 @@ var file_auth_proto_rawDesc = []byte{ 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x16, 0x0a, 0x14, 0x5f, 0x70, 0x77, 0x64, 0x5f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x64, - 0x61, 0x74, 0x65, 0x32, 0x9e, 0x01, 0x0a, 0x04, 0x41, 0x75, 0x74, 0x68, 0x12, 0x28, 0x0a, 0x05, - 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x0e, 0x2e, 0x63, 0x65, 0x70, 0x68, 0x2e, 0x4c, 0x6f, 0x67, - 0x69, 0x6e, 0x52, 0x65, 0x71, 0x1a, 0x0f, 0x2e, 0x63, 0x65, 0x70, 0x68, 0x2e, 0x4c, 0x6f, 0x67, - 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x12, 0x38, 0x0a, 0x06, 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, - 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, - 0x12, 0x32, 0x0a, 0x05, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x12, 0x13, 0x2e, 0x63, 0x65, 0x70, 0x68, - 0x2e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x1a, 0x14, - 0x2e, 0x63, 0x65, 0x70, 0x68, 0x2e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x43, 0x68, 0x65, 0x63, 0x6b, - 0x52, 0x65, 0x73, 0x70, 0x42, 0xcc, 0x02, 0x92, 0x41, 0xa1, 0x02, 0x12, 0x8c, 0x01, 0x0a, 0x13, - 0x43, 0x65, 0x70, 0x68, 0x20, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x20, - 0x41, 0x50, 0x49, 0x22, 0x2d, 0x0a, 0x08, 0x43, 0x65, 0x70, 0x68, 0x20, 0x41, 0x50, 0x49, 0x12, - 0x21, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, - 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6c, 0x79, 0x73, 0x6f, 0x2f, 0x63, 0x65, 0x70, 0x68, 0x2d, 0x61, - 0x70, 0x69, 0x2a, 0x46, 0x0a, 0x0f, 0x47, 0x50, 0x4c, 0x2d, 0x33, 0x2e, 0x30, 0x20, 0x6c, 0x69, - 0x63, 0x65, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x67, - 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6c, 0x79, 0x73, 0x6f, 0x2f, - 0x63, 0x65, 0x70, 0x68, 0x2d, 0x61, 0x70, 0x69, 0x2f, 0x62, 0x6c, 0x6f, 0x62, 0x2f, 0x6d, 0x61, - 0x69, 0x6e, 0x2f, 0x4c, 0x49, 0x43, 0x45, 0x4e, 0x53, 0x45, 0x2a, 0x02, 0x01, 0x02, 0x32, 0x10, - 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x6a, 0x73, 0x6f, 0x6e, - 0x3a, 0x10, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x6a, 0x73, - 0x6f, 0x6e, 0x5a, 0x52, 0x0a, 0x50, 0x0a, 0x06, 0x4f, 0x41, 0x75, 0x74, 0x68, 0x32, 0x12, 0x46, - 0x08, 0x03, 0x28, 0x02, 0x3a, 0x25, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x6c, 0x6f, 0x63, - 0x61, 0x6c, 0x68, 0x6f, 0x73, 0x74, 0x3a, 0x39, 0x39, 0x36, 0x39, 0x2f, 0x61, 0x70, 0x69, 0x2f, - 0x6f, 0x61, 0x75, 0x74, 0x68, 0x2f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x42, 0x19, 0x0a, 0x17, 0x0a, - 0x06, 0x6f, 0x70, 0x65, 0x6e, 0x69, 0x64, 0x12, 0x0d, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, - 0x20, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x62, 0x14, 0x0a, 0x12, 0x0a, 0x06, 0x4f, 0x41, 0x75, 0x74, - 0x68, 0x32, 0x12, 0x08, 0x0a, 0x06, 0x6f, 0x70, 0x65, 0x6e, 0x69, 0x64, 0x5a, 0x25, 0x67, 0x69, + 0x61, 0x74, 0x65, 0x22, 0x98, 0x02, 0x0a, 0x0a, 0x57, 0x68, 0x6f, 0x61, 0x6d, 0x69, 0x52, 0x65, + 0x73, 0x70, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x1b, 0x0a, 0x09, + 0x61, 0x75, 0x74, 0x68, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x61, 0x75, 0x74, 0x68, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1c, 0x0a, 0x0a, 0x61, 0x70, 0x69, + 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, + 0x70, 0x69, 0x4b, 0x65, 0x79, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x6f, 0x6c, 0x65, 0x73, + 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x72, 0x6f, 0x6c, 0x65, 0x73, 0x12, 0x43, 0x0a, + 0x0b, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x05, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x63, 0x65, 0x70, 0x68, 0x2e, 0x57, 0x68, 0x6f, 0x61, 0x6d, 0x69, + 0x52, 0x65, 0x73, 0x70, 0x2e, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0b, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, + 0x6e, 0x73, 0x1a, 0x5a, 0x0a, 0x10, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, + 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x30, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x32, 0xd2, + 0x01, 0x0a, 0x04, 0x41, 0x75, 0x74, 0x68, 0x12, 0x28, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, + 0x12, 0x0e, 0x2e, 0x63, 0x65, 0x70, 0x68, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, + 0x1a, 0x0f, 0x2e, 0x63, 0x65, 0x70, 0x68, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, + 0x70, 0x12, 0x38, 0x0a, 0x06, 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x12, 0x16, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, + 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x32, 0x0a, 0x05, 0x43, + 0x68, 0x65, 0x63, 0x6b, 0x12, 0x13, 0x2e, 0x63, 0x65, 0x70, 0x68, 0x2e, 0x54, 0x6f, 0x6b, 0x65, + 0x6e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x1a, 0x14, 0x2e, 0x63, 0x65, 0x70, 0x68, + 0x2e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x12, + 0x32, 0x0a, 0x06, 0x57, 0x68, 0x6f, 0x61, 0x6d, 0x69, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, + 0x79, 0x1a, 0x10, 0x2e, 0x63, 0x65, 0x70, 0x68, 0x2e, 0x57, 0x68, 0x6f, 0x61, 0x6d, 0x69, 0x52, + 0x65, 0x73, 0x70, 0x42, 0xcc, 0x02, 0x92, 0x41, 0xa1, 0x02, 0x12, 0x8c, 0x01, 0x0a, 0x13, 0x43, + 0x65, 0x70, 0x68, 0x20, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x20, 0x41, + 0x50, 0x49, 0x22, 0x2d, 0x0a, 0x08, 0x43, 0x65, 0x70, 0x68, 0x20, 0x41, 0x50, 0x49, 0x12, 0x21, + 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, + 0x6f, 0x6d, 0x2f, 0x63, 0x6c, 0x79, 0x73, 0x6f, 0x2f, 0x63, 0x65, 0x70, 0x68, 0x2d, 0x61, 0x70, + 0x69, 0x2a, 0x46, 0x0a, 0x0f, 0x47, 0x50, 0x4c, 0x2d, 0x33, 0x2e, 0x30, 0x20, 0x6c, 0x69, 0x63, + 0x65, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6c, 0x79, 0x73, 0x6f, 0x2f, 0x63, - 0x65, 0x70, 0x68, 0x2d, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x63, 0x65, 0x70, 0x68, - 0x3b, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x65, 0x70, 0x68, 0x2d, 0x61, 0x70, 0x69, 0x2f, 0x62, 0x6c, 0x6f, 0x62, 0x2f, 0x6d, 0x61, 0x69, + 0x6e, 0x2f, 0x4c, 0x49, 0x43, 0x45, 0x4e, 0x53, 0x45, 0x2a, 0x02, 0x01, 0x02, 0x32, 0x10, 0x61, + 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x6a, 0x73, 0x6f, 0x6e, 0x3a, + 0x10, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x6a, 0x73, 0x6f, + 0x6e, 0x5a, 0x52, 0x0a, 0x50, 0x0a, 0x06, 0x4f, 0x41, 0x75, 0x74, 0x68, 0x32, 0x12, 0x46, 0x08, + 0x03, 0x28, 0x02, 0x3a, 0x25, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x6c, 0x6f, 0x63, 0x61, + 0x6c, 0x68, 0x6f, 0x73, 0x74, 0x3a, 0x39, 0x39, 0x36, 0x39, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x6f, + 0x61, 0x75, 0x74, 0x68, 0x2f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x42, 0x19, 0x0a, 0x17, 0x0a, 0x06, + 0x6f, 0x70, 0x65, 0x6e, 0x69, 0x64, 0x12, 0x0d, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x20, + 0x73, 0x63, 0x6f, 0x70, 0x65, 0x62, 0x14, 0x0a, 0x12, 0x0a, 0x06, 0x4f, 0x41, 0x75, 0x74, 0x68, + 0x32, 0x12, 0x08, 0x0a, 0x06, 0x6f, 0x70, 0x65, 0x6e, 0x69, 0x64, 0x5a, 0x25, 0x67, 0x69, 0x74, + 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6c, 0x79, 0x73, 0x6f, 0x2f, 0x63, 0x65, + 0x70, 0x68, 0x2d, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x63, 0x65, 0x70, 0x68, 0x3b, + 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -406,36 +506,42 @@ func file_auth_proto_rawDescGZIP() []byte { return file_auth_proto_rawDescData } -var file_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 6) +var file_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 8) var file_auth_proto_goTypes = []interface{}{ (*LoginReq)(nil), // 0: ceph.LoginReq (*LoginResp)(nil), // 1: ceph.LoginResp (*TokenCheckReq)(nil), // 2: ceph.TokenCheckReq (*TokenCheckResp)(nil), // 3: ceph.TokenCheckResp - nil, // 4: ceph.LoginResp.PermissionsEntry - nil, // 5: ceph.TokenCheckResp.PermissionsEntry - (*timestamppb.Timestamp)(nil), // 6: google.protobuf.Timestamp - (*structpb.ListValue)(nil), // 7: google.protobuf.ListValue - (*emptypb.Empty)(nil), // 8: google.protobuf.Empty + (*WhoamiResp)(nil), // 4: ceph.WhoamiResp + nil, // 5: ceph.LoginResp.PermissionsEntry + nil, // 6: ceph.TokenCheckResp.PermissionsEntry + nil, // 7: ceph.WhoamiResp.PermissionsEntry + (*timestamppb.Timestamp)(nil), // 8: google.protobuf.Timestamp + (*structpb.ListValue)(nil), // 9: google.protobuf.ListValue + (*emptypb.Empty)(nil), // 10: google.protobuf.Empty } var file_auth_proto_depIdxs = []int32{ - 6, // 0: ceph.LoginResp.pwd_expiration_date:type_name -> google.protobuf.Timestamp - 4, // 1: ceph.LoginResp.permissions:type_name -> ceph.LoginResp.PermissionsEntry - 6, // 2: ceph.TokenCheckResp.pwd_expiration_date:type_name -> google.protobuf.Timestamp - 5, // 3: ceph.TokenCheckResp.permissions:type_name -> ceph.TokenCheckResp.PermissionsEntry - 7, // 4: ceph.LoginResp.PermissionsEntry.value:type_name -> google.protobuf.ListValue - 7, // 5: ceph.TokenCheckResp.PermissionsEntry.value:type_name -> google.protobuf.ListValue - 0, // 6: ceph.Auth.Login:input_type -> ceph.LoginReq - 8, // 7: ceph.Auth.Logout:input_type -> google.protobuf.Empty - 2, // 8: ceph.Auth.Check:input_type -> ceph.TokenCheckReq - 1, // 9: ceph.Auth.Login:output_type -> ceph.LoginResp - 8, // 10: ceph.Auth.Logout:output_type -> google.protobuf.Empty - 3, // 11: ceph.Auth.Check:output_type -> ceph.TokenCheckResp - 9, // [9:12] is the sub-list for method output_type - 6, // [6:9] is the sub-list for method input_type - 6, // [6:6] is the sub-list for extension type_name - 6, // [6:6] is the sub-list for extension extendee - 0, // [0:6] is the sub-list for field type_name + 8, // 0: ceph.LoginResp.pwd_expiration_date:type_name -> google.protobuf.Timestamp + 5, // 1: ceph.LoginResp.permissions:type_name -> ceph.LoginResp.PermissionsEntry + 8, // 2: ceph.TokenCheckResp.pwd_expiration_date:type_name -> google.protobuf.Timestamp + 6, // 3: ceph.TokenCheckResp.permissions:type_name -> ceph.TokenCheckResp.PermissionsEntry + 7, // 4: ceph.WhoamiResp.permissions:type_name -> ceph.WhoamiResp.PermissionsEntry + 9, // 5: ceph.LoginResp.PermissionsEntry.value:type_name -> google.protobuf.ListValue + 9, // 6: ceph.TokenCheckResp.PermissionsEntry.value:type_name -> google.protobuf.ListValue + 9, // 7: ceph.WhoamiResp.PermissionsEntry.value:type_name -> google.protobuf.ListValue + 0, // 8: ceph.Auth.Login:input_type -> ceph.LoginReq + 10, // 9: ceph.Auth.Logout:input_type -> google.protobuf.Empty + 2, // 10: ceph.Auth.Check:input_type -> ceph.TokenCheckReq + 10, // 11: ceph.Auth.Whoami:input_type -> google.protobuf.Empty + 1, // 12: ceph.Auth.Login:output_type -> ceph.LoginResp + 10, // 13: ceph.Auth.Logout:output_type -> google.protobuf.Empty + 3, // 14: ceph.Auth.Check:output_type -> ceph.TokenCheckResp + 4, // 15: ceph.Auth.Whoami:output_type -> ceph.WhoamiResp + 12, // [12:16] is the sub-list for method output_type + 8, // [8:12] is the sub-list for method input_type + 8, // [8:8] is the sub-list for extension type_name + 8, // [8:8] is the sub-list for extension extendee + 0, // [0:8] is the sub-list for field type_name } func init() { file_auth_proto_init() } @@ -492,6 +598,18 @@ func file_auth_proto_init() { return nil } } + file_auth_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*WhoamiResp); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } file_auth_proto_msgTypes[1].OneofWrappers = []interface{}{} file_auth_proto_msgTypes[3].OneofWrappers = []interface{}{} @@ -501,7 +619,7 @@ func file_auth_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_auth_proto_rawDesc, NumEnums: 0, - NumMessages: 6, + NumMessages: 8, NumExtensions: 0, NumServices: 1, }, diff --git a/api/gen/grpc/go/auth.pb.gw.go b/api/gen/grpc/go/auth.pb.gw.go index 77d7a3f..f707ec9 100644 --- a/api/gen/grpc/go/auth.pb.gw.go +++ b/api/gen/grpc/go/auth.pb.gw.go @@ -108,6 +108,24 @@ func local_request_Auth_Check_0(ctx context.Context, marshaler runtime.Marshaler return msg, metadata, err } +func request_Auth_Whoami_0(ctx context.Context, marshaler runtime.Marshaler, client AuthClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq emptypb.Empty + metadata runtime.ServerMetadata + ) + msg, err := client.Whoami(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_Auth_Whoami_0(ctx context.Context, marshaler runtime.Marshaler, server AuthServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq emptypb.Empty + metadata runtime.ServerMetadata + ) + msg, err := server.Whoami(ctx, &protoReq) + return msg, metadata, err +} + // RegisterAuthHandlerServer registers the http handlers for service Auth to "mux". // UnaryRPC :call AuthServer directly. // StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. @@ -174,6 +192,26 @@ func RegisterAuthHandlerServer(ctx context.Context, mux *runtime.ServeMux, serve } forward_Auth_Check_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) + mux.Handle(http.MethodGet, pattern_Auth_Whoami_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/ceph.Auth/Whoami", runtime.WithHTTPPathPattern("/api/v1/auth/whoami")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_Auth_Whoami_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_Auth_Whoami_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) return nil } @@ -265,6 +303,23 @@ func RegisterAuthHandlerClient(ctx context.Context, mux *runtime.ServeMux, clien } forward_Auth_Check_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) + mux.Handle(http.MethodGet, pattern_Auth_Whoami_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/ceph.Auth/Whoami", runtime.WithHTTPPathPattern("/api/v1/auth/whoami")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_Auth_Whoami_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_Auth_Whoami_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) return nil } @@ -272,10 +327,12 @@ var ( pattern_Auth_Login_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"api", "auth"}, "")) pattern_Auth_Logout_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "auth", "logout"}, "")) pattern_Auth_Check_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "auth", "check"}, "")) + pattern_Auth_Whoami_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"api", "v1", "auth", "whoami"}, "")) ) var ( forward_Auth_Login_0 = runtime.ForwardResponseMessage forward_Auth_Logout_0 = runtime.ForwardResponseMessage forward_Auth_Check_0 = runtime.ForwardResponseMessage + forward_Auth_Whoami_0 = runtime.ForwardResponseMessage ) diff --git a/api/gen/grpc/go/auth_grpc.pb.go b/api/gen/grpc/go/auth_grpc.pb.go index ac8d19b..1b2be61 100644 --- a/api/gen/grpc/go/auth_grpc.pb.go +++ b/api/gen/grpc/go/auth_grpc.pb.go @@ -23,6 +23,7 @@ const ( Auth_Login_FullMethodName = "/ceph.Auth/Login" Auth_Logout_FullMethodName = "/ceph.Auth/Logout" Auth_Check_FullMethodName = "/ceph.Auth/Check" + Auth_Whoami_FullMethodName = "/ceph.Auth/Whoami" ) // AuthClient is the client API for Auth service. @@ -32,6 +33,7 @@ type AuthClient interface { Login(ctx context.Context, in *LoginReq, opts ...grpc.CallOption) (*LoginResp, error) Logout(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) Check(ctx context.Context, in *TokenCheckReq, opts ...grpc.CallOption) (*TokenCheckResp, error) + Whoami(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*WhoamiResp, error) } type authClient struct { @@ -72,6 +74,16 @@ func (c *authClient) Check(ctx context.Context, in *TokenCheckReq, opts ...grpc. return out, nil } +func (c *authClient) Whoami(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*WhoamiResp, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(WhoamiResp) + err := c.cc.Invoke(ctx, Auth_Whoami_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // AuthServer is the server API for Auth service. // All implementations should embed UnimplementedAuthServer // for forward compatibility. @@ -79,6 +91,7 @@ type AuthServer interface { Login(context.Context, *LoginReq) (*LoginResp, error) Logout(context.Context, *emptypb.Empty) (*emptypb.Empty, error) Check(context.Context, *TokenCheckReq) (*TokenCheckResp, error) + Whoami(context.Context, *emptypb.Empty) (*WhoamiResp, error) } // UnimplementedAuthServer should be embedded to have @@ -97,6 +110,9 @@ func (UnimplementedAuthServer) Logout(context.Context, *emptypb.Empty) (*emptypb func (UnimplementedAuthServer) Check(context.Context, *TokenCheckReq) (*TokenCheckResp, error) { return nil, status.Errorf(codes.Unimplemented, "method Check not implemented") } +func (UnimplementedAuthServer) Whoami(context.Context, *emptypb.Empty) (*WhoamiResp, error) { + return nil, status.Errorf(codes.Unimplemented, "method Whoami not implemented") +} func (UnimplementedAuthServer) testEmbeddedByValue() {} // UnsafeAuthServer may be embedded to opt out of forward compatibility for this service. @@ -171,6 +187,24 @@ func _Auth_Check_Handler(srv interface{}, ctx context.Context, dec func(interfac return interceptor(ctx, in, info, handler) } +func _Auth_Whoami_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(emptypb.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AuthServer).Whoami(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Auth_Whoami_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AuthServer).Whoami(ctx, req.(*emptypb.Empty)) + } + return interceptor(ctx, in, info, handler) +} + // Auth_ServiceDesc is the grpc.ServiceDesc for Auth service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -190,6 +224,10 @@ var Auth_ServiceDesc = grpc.ServiceDesc{ MethodName: "Check", Handler: _Auth_Check_Handler, }, + { + MethodName: "Whoami", + Handler: _Auth_Whoami_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "auth.proto", diff --git a/api/http.yaml b/api/http.yaml index d5ee55a..d7dc63b 100644 --- a/api/http.yaml +++ b/api/http.yaml @@ -68,6 +68,9 @@ http: - selector: ceph.Auth.Check post: /api/auth/check body: "*" + - selector: ceph.Auth.Whoami + get: /api/v1/auth/whoami + response_body: "*" # CRUSH rules - selector: ceph.CrushRule.ListRules get: /api/crush_rule diff --git a/api/openapi/ceph-api.swagger.json b/api/openapi/ceph-api.swagger.json index 005369c..631ecab 100644 --- a/api/openapi/ceph-api.swagger.json +++ b/api/openapi/ceph-api.swagger.json @@ -1046,6 +1046,28 @@ "Users" ] } + }, + "/api/v1/auth/whoami": { + "get": { + "operationId": "Auth_Whoami", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/cephWhoamiResp" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/googlerpcStatus" + } + } + }, + "tags": [ + "Auth" + ] + } } }, "definitions": { @@ -4463,6 +4485,35 @@ } } }, + "cephWhoamiResp": { + "type": "object", + "properties": { + "subject": { + "type": "string" + }, + "authType": { + "type": "string" + }, + "apiKeyId": { + "type": "string" + }, + "roles": { + "type": "array", + "items": { + "type": "string" + } + }, + "permissions": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "object" + } + } + } + } + }, "googlerpcStatus": { "type": "object", "properties": { diff --git a/pkg/api/auth_api_handlers.go b/pkg/api/auth_api_handlers.go index 32d748b..ecdb157 100644 --- a/pkg/api/auth_api_handlers.go +++ b/pkg/api/auth_api_handlers.go @@ -5,6 +5,7 @@ import ( pb "github.com/clyso/ceph-api/api/gen/grpc/go" "github.com/clyso/ceph-api/pkg/auth" + xctx "github.com/clyso/ceph-api/pkg/ctx" "github.com/clyso/ceph-api/pkg/types" "google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/structpb" @@ -29,20 +30,13 @@ func (a *authAPI) Login(ctx context.Context, req *pb.LoginReq) (*pb.LoginResp, e if err != nil { return nil, err } - permissions := make(map[string]*structpb.ListValue, len(res.Permissions)) - for p, vals := range res.Permissions { - permissions[p] = &structpb.ListValue{} - for _, v := range vals { - permissions[p].Values = append(permissions[p].Values, structpb.NewStringValue(v)) - } - } return &pb.LoginResp{ Token: res.Token, Username: res.User.Username, PwdUpdateRequired: res.User.PwdUpdateRequired, PwdExpirationDate: tsToPb(res.User.PwdExpirationDate), Sso: false, - Permissions: permissions, + Permissions: permissionsToPB(res.Permissions), }, nil } @@ -53,3 +47,29 @@ func (a *authAPI) Logout(ctx context.Context, _ *emptypb.Empty) (*emptypb.Empty, } return &emptypb.Empty{}, nil } + +func (a *authAPI) Whoami(ctx context.Context, _ *emptypb.Empty) (*pb.WhoamiResp, error) { + username := xctx.GetUsername(ctx) + if username == "" { + return nil, types.ErrUnauthenticated + } + + return &pb.WhoamiResp{ + Subject: username, + AuthType: xctx.GetAuthType(ctx), + ApiKeyId: xctx.GetAPIKeyID(ctx), + Roles: xctx.GetRoles(ctx), + Permissions: permissionsToPB(xctx.GetPermissions(ctx)), + }, nil +} + +func permissionsToPB(in map[string][]string) map[string]*structpb.ListValue { + permissions := make(map[string]*structpb.ListValue, len(in)) + for p, vals := range in { + permissions[p] = &structpb.ListValue{} + for _, v := range vals { + permissions[p].Values = append(permissions[p].Values, structpb.NewStringValue(v)) + } + } + return permissions +} diff --git a/pkg/api/auth_api_handlers_test.go b/pkg/api/auth_api_handlers_test.go new file mode 100644 index 0000000..f3fb2e0 --- /dev/null +++ b/pkg/api/auth_api_handlers_test.go @@ -0,0 +1,45 @@ +package api + +import ( + "context" + "errors" + "testing" + + xctx "github.com/clyso/ceph-api/pkg/ctx" + "github.com/clyso/ceph-api/pkg/types" + "google.golang.org/protobuf/types/known/emptypb" +) + +func TestAuthAPIWhoami(t *testing.T) { + ctx := context.Background() + ctx = xctx.SetUsername(ctx, "admin") + ctx = xctx.SetRoles(ctx, []string{"administrator"}) + ctx = xctx.SetPermissions(ctx, map[string][]string{"pool": {"read", "create"}}) + ctx = xctx.SetAuthType(ctx, "jwt") + ctx = xctx.SetAPIKeyID(ctx, "") + + resp, err := (&authAPI{}).Whoami(ctx, &emptypb.Empty{}) + if err != nil { + t.Fatalf("Whoami() error = %v", err) + } + if resp.Subject != "admin" { + t.Fatalf("subject = %q, want admin", resp.Subject) + } + if resp.AuthType != "jwt" { + t.Fatalf("auth_type = %q, want jwt", resp.AuthType) + } + if len(resp.Roles) != 1 || resp.Roles[0] != "administrator" { + t.Fatalf("roles = %v, want [administrator]", resp.Roles) + } + poolPerms := resp.Permissions["pool"].GetValues() + if len(poolPerms) != 2 || poolPerms[0].GetStringValue() != "read" || poolPerms[1].GetStringValue() != "create" { + t.Fatalf("pool permissions = %v, want [read create]", poolPerms) + } +} + +func TestAuthAPIWhoamiRequiresUsername(t *testing.T) { + _, err := (&authAPI{}).Whoami(context.Background(), &emptypb.Empty{}) + if !errors.Is(err, types.ErrUnauthenticated) { + t.Fatalf("Whoami() error = %v, want %v", err, types.ErrUnauthenticated) + } +} diff --git a/pkg/auth/discovery_handler_test.go b/pkg/auth/discovery_handler_test.go index c19939c..d4f9212 100644 --- a/pkg/auth/discovery_handler_test.go +++ b/pkg/auth/discovery_handler_test.go @@ -93,7 +93,8 @@ func newTestServer(t *testing.T) *Server { if err != nil { t.Fatalf("compute kid: %v", err) } - server, err := NewServer(Config{ClientID: "ceph-api", Issuer: "http://issuer.example"}, nil, priv, kid) + globalSecret := []byte("0123456789abcdef0123456789abcdef") + server, err := NewServer(Config{ClientID: "ceph-api", Issuer: "http://issuer.example"}, nil, priv, kid, globalSecret) if err != nil { t.Fatalf("NewServer() error = %v", err) } diff --git a/pkg/auth/grpc_interceptor.go b/pkg/auth/grpc_interceptor.go index 8d956e1..987b3c4 100644 --- a/pkg/auth/grpc_interceptor.go +++ b/pkg/auth/grpc_interceptor.go @@ -65,7 +65,10 @@ func AuthFunc(userSvc *user.Service, provider fosite.OAuth2Provider, getKey func return nil, unauthenticated(types.ErrUnauthenticated) } ctx = log.WithUsername(ctx, usr.Username) + ctx = xctx.SetRoles(ctx, usr.Roles) ctx = xctx.SetPermissions(ctx, userSvc.GetPermissions(ctx, username)) + ctx = xctx.SetAuthType(ctx, "jwt") + ctx = xctx.SetAPIKeyID(ctx, "") return ctx, nil } diff --git a/pkg/ctx/context.go b/pkg/ctx/context.go index 0a288c8..65fb2d8 100644 --- a/pkg/ctx/context.go +++ b/pkg/ctx/context.go @@ -7,6 +7,9 @@ import ( type traceKey struct{} type userKey struct{} type permKey struct{} +type roleKey struct{} +type authTypeKey struct{} +type apiKeyIDKey struct{} func SetTraceID(ctx context.Context, in string) context.Context { return context.WithValue(ctx, traceKey{}, in) @@ -37,3 +40,33 @@ func GetPermissions(ctx context.Context) map[string][]string { res, _ := ctx.Value(permKey{}).(map[string][]string) return res } + +func SetRoles(ctx context.Context, in []string) context.Context { + if in == nil { + in = []string{} + } + return context.WithValue(ctx, roleKey{}, in) +} + +func GetRoles(ctx context.Context) []string { + res, _ := ctx.Value(roleKey{}).([]string) + return res +} + +func SetAuthType(ctx context.Context, in string) context.Context { + return context.WithValue(ctx, authTypeKey{}, in) +} + +func GetAuthType(ctx context.Context) string { + res, _ := ctx.Value(authTypeKey{}).(string) + return res +} + +func SetAPIKeyID(ctx context.Context, in string) context.Context { + return context.WithValue(ctx, apiKeyIDKey{}, in) +} + +func GetAPIKeyID(ctx context.Context) string { + res, _ := ctx.Value(apiKeyIDKey{}).(string) + return res +}