From d0943cac0781fa3836611e49fc9b45e734bd8d09 Mon Sep 17 00:00:00 2001 From: Edoardo Spadolini Date: Thu, 16 Jan 2025 02:30:07 +0100 Subject: [PATCH] auth API and auth-side business logic --- .../stableunixusers/v1/stableunixusers.pb.go | 603 ++++++++++++++++++ .../v1/stableunixusers_grpc.pb.go | 178 ++++++ .../stableunixusers/v1/stableunixusers.proto | 70 ++ api/types/constants.go | 4 + buf-go.gen.yaml | 1 + .../stableunixusers/v1/stableunixusers.pb.go | 191 ++++++ lib/auth/auth.go | 8 + lib/auth/grpcserver.go | 10 + lib/auth/init.go | 3 + lib/auth/stableunixusers.go | 259 ++++++++ lib/auth/stableunixusers_test.go | 164 +++++ lib/authz/permissions.go | 1 + 12 files changed, 1492 insertions(+) create mode 100644 api/gen/proto/go/teleport/stableunixusers/v1/stableunixusers.pb.go create mode 100644 api/gen/proto/go/teleport/stableunixusers/v1/stableunixusers_grpc.pb.go create mode 100644 api/proto/teleport/stableunixusers/v1/stableunixusers.proto create mode 100644 gen/proto/go/teleport/storage/local/stableunixusers/v1/stableunixusers.pb.go create mode 100644 lib/auth/stableunixusers.go create mode 100644 lib/auth/stableunixusers_test.go diff --git a/api/gen/proto/go/teleport/stableunixusers/v1/stableunixusers.pb.go b/api/gen/proto/go/teleport/stableunixusers/v1/stableunixusers.pb.go new file mode 100644 index 0000000000000..716d45b088338 --- /dev/null +++ b/api/gen/proto/go/teleport/stableunixusers/v1/stableunixusers.pb.go @@ -0,0 +1,603 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.3 +// protoc (unknown) +// source: teleport/stableunixusers/v1/stableunixusers.proto + +package stableunixusersv1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// request message for ObtainUIDForUsername +type ObtainUIDForUsernameRequest struct { + state protoimpl.MessageState `protogen:"opaque.v1"` + xxx_hidden_Username *string `protobuf:"bytes,1,opt,name=username"` + XXX_raceDetectHookData protoimpl.RaceDetectHookData + XXX_presence [1]uint32 + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ObtainUIDForUsernameRequest) Reset() { + *x = ObtainUIDForUsernameRequest{} + mi := &file_teleport_stableunixusers_v1_stableunixusers_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ObtainUIDForUsernameRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ObtainUIDForUsernameRequest) ProtoMessage() {} + +func (x *ObtainUIDForUsernameRequest) ProtoReflect() protoreflect.Message { + mi := &file_teleport_stableunixusers_v1_stableunixusers_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +func (x *ObtainUIDForUsernameRequest) GetUsername() string { + if x != nil { + if x.xxx_hidden_Username != nil { + return *x.xxx_hidden_Username + } + return "" + } + return "" +} + +func (x *ObtainUIDForUsernameRequest) SetUsername(v string) { + x.xxx_hidden_Username = &v + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 0, 1) +} + +func (x *ObtainUIDForUsernameRequest) HasUsername() bool { + if x == nil { + return false + } + return protoimpl.X.Present(&(x.XXX_presence[0]), 0) +} + +func (x *ObtainUIDForUsernameRequest) ClearUsername() { + protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 0) + x.xxx_hidden_Username = nil +} + +type ObtainUIDForUsernameRequest_builder struct { + _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. + + Username *string +} + +func (b0 ObtainUIDForUsernameRequest_builder) Build() *ObtainUIDForUsernameRequest { + m0 := &ObtainUIDForUsernameRequest{} + b, x := &b0, m0 + _, _ = b, x + if b.Username != nil { + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 0, 1) + x.xxx_hidden_Username = b.Username + } + return m0 +} + +// response message for ObtainUIDForUsername +type ObtainUIDForUsernameResponse struct { + state protoimpl.MessageState `protogen:"opaque.v1"` + xxx_hidden_Uid int32 `protobuf:"varint,1,opt,name=uid"` + XXX_raceDetectHookData protoimpl.RaceDetectHookData + XXX_presence [1]uint32 + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ObtainUIDForUsernameResponse) Reset() { + *x = ObtainUIDForUsernameResponse{} + mi := &file_teleport_stableunixusers_v1_stableunixusers_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ObtainUIDForUsernameResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ObtainUIDForUsernameResponse) ProtoMessage() {} + +func (x *ObtainUIDForUsernameResponse) ProtoReflect() protoreflect.Message { + mi := &file_teleport_stableunixusers_v1_stableunixusers_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) +} + +func (x *ObtainUIDForUsernameResponse) GetUid() int32 { + if x != nil { + return x.xxx_hidden_Uid + } + return 0 +} + +func (x *ObtainUIDForUsernameResponse) SetUid(v int32) { + x.xxx_hidden_Uid = v + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 0, 1) +} + +func (x *ObtainUIDForUsernameResponse) HasUid() bool { + if x == nil { + return false + } + return protoimpl.X.Present(&(x.XXX_presence[0]), 0) +} + +func (x *ObtainUIDForUsernameResponse) ClearUid() { + protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 0) + x.xxx_hidden_Uid = 0 +} + +type ObtainUIDForUsernameResponse_builder struct { + _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. + + Uid *int32 +} + +func (b0 ObtainUIDForUsernameResponse_builder) Build() *ObtainUIDForUsernameResponse { + m0 := &ObtainUIDForUsernameResponse{} + b, x := &b0, m0 + _, _ = b, x + if b.Uid != nil { + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 0, 1) + x.xxx_hidden_Uid = *b.Uid + } + return m0 +} + +// request message for ListStableUNIXUsers +type ListStableUNIXUsersRequest struct { + state protoimpl.MessageState `protogen:"opaque.v1"` + xxx_hidden_PageSize int32 `protobuf:"varint,1,opt,name=page_size,json=pageSize"` + xxx_hidden_PageToken *string `protobuf:"bytes,2,opt,name=page_token,json=pageToken"` + XXX_raceDetectHookData protoimpl.RaceDetectHookData + XXX_presence [1]uint32 + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListStableUNIXUsersRequest) Reset() { + *x = ListStableUNIXUsersRequest{} + mi := &file_teleport_stableunixusers_v1_stableunixusers_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListStableUNIXUsersRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListStableUNIXUsersRequest) ProtoMessage() {} + +func (x *ListStableUNIXUsersRequest) ProtoReflect() protoreflect.Message { + mi := &file_teleport_stableunixusers_v1_stableunixusers_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +func (x *ListStableUNIXUsersRequest) GetPageSize() int32 { + if x != nil { + return x.xxx_hidden_PageSize + } + return 0 +} + +func (x *ListStableUNIXUsersRequest) GetPageToken() string { + if x != nil { + if x.xxx_hidden_PageToken != nil { + return *x.xxx_hidden_PageToken + } + return "" + } + return "" +} + +func (x *ListStableUNIXUsersRequest) SetPageSize(v int32) { + x.xxx_hidden_PageSize = v + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 0, 2) +} + +func (x *ListStableUNIXUsersRequest) SetPageToken(v string) { + x.xxx_hidden_PageToken = &v + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 1, 2) +} + +func (x *ListStableUNIXUsersRequest) HasPageSize() bool { + if x == nil { + return false + } + return protoimpl.X.Present(&(x.XXX_presence[0]), 0) +} + +func (x *ListStableUNIXUsersRequest) HasPageToken() bool { + if x == nil { + return false + } + return protoimpl.X.Present(&(x.XXX_presence[0]), 1) +} + +func (x *ListStableUNIXUsersRequest) ClearPageSize() { + protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 0) + x.xxx_hidden_PageSize = 0 +} + +func (x *ListStableUNIXUsersRequest) ClearPageToken() { + protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 1) + x.xxx_hidden_PageToken = nil +} + +type ListStableUNIXUsersRequest_builder struct { + _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. + + // if left unset, the page size will default to a reasonable value chosen by + // the server + PageSize *int32 + // if unset or blank, the rpc will return the first page + PageToken *string +} + +func (b0 ListStableUNIXUsersRequest_builder) Build() *ListStableUNIXUsersRequest { + m0 := &ListStableUNIXUsersRequest{} + b, x := &b0, m0 + _, _ = b, x + if b.PageSize != nil { + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 0, 2) + x.xxx_hidden_PageSize = *b.PageSize + } + if b.PageToken != nil { + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 1, 2) + x.xxx_hidden_PageToken = b.PageToken + } + return m0 +} + +// a pair of stable UNIX username and UID +type StableUNIXUser struct { + state protoimpl.MessageState `protogen:"opaque.v1"` + xxx_hidden_Username *string `protobuf:"bytes,1,opt,name=username"` + xxx_hidden_Uid int32 `protobuf:"varint,2,opt,name=uid"` + XXX_raceDetectHookData protoimpl.RaceDetectHookData + XXX_presence [1]uint32 + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StableUNIXUser) Reset() { + *x = StableUNIXUser{} + mi := &file_teleport_stableunixusers_v1_stableunixusers_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StableUNIXUser) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StableUNIXUser) ProtoMessage() {} + +func (x *StableUNIXUser) ProtoReflect() protoreflect.Message { + mi := &file_teleport_stableunixusers_v1_stableunixusers_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +func (x *StableUNIXUser) GetUsername() string { + if x != nil { + if x.xxx_hidden_Username != nil { + return *x.xxx_hidden_Username + } + return "" + } + return "" +} + +func (x *StableUNIXUser) GetUid() int32 { + if x != nil { + return x.xxx_hidden_Uid + } + return 0 +} + +func (x *StableUNIXUser) SetUsername(v string) { + x.xxx_hidden_Username = &v + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 0, 2) +} + +func (x *StableUNIXUser) SetUid(v int32) { + x.xxx_hidden_Uid = v + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 1, 2) +} + +func (x *StableUNIXUser) HasUsername() bool { + if x == nil { + return false + } + return protoimpl.X.Present(&(x.XXX_presence[0]), 0) +} + +func (x *StableUNIXUser) HasUid() bool { + if x == nil { + return false + } + return protoimpl.X.Present(&(x.XXX_presence[0]), 1) +} + +func (x *StableUNIXUser) ClearUsername() { + protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 0) + x.xxx_hidden_Username = nil +} + +func (x *StableUNIXUser) ClearUid() { + protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 1) + x.xxx_hidden_Uid = 0 +} + +type StableUNIXUser_builder struct { + _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. + + Username *string + Uid *int32 +} + +func (b0 StableUNIXUser_builder) Build() *StableUNIXUser { + m0 := &StableUNIXUser{} + b, x := &b0, m0 + _, _ = b, x + if b.Username != nil { + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 0, 2) + x.xxx_hidden_Username = b.Username + } + if b.Uid != nil { + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 1, 2) + x.xxx_hidden_Uid = *b.Uid + } + return m0 +} + +// response message for ListStableUNIXUsers +type ListStableUNIXUsersResponse struct { + state protoimpl.MessageState `protogen:"opaque.v1"` + xxx_hidden_StableUnixUsers *[]*StableUNIXUser `protobuf:"bytes,1,rep,name=stable_unix_users,json=stableUnixUsers"` + xxx_hidden_NextPageToken *string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken"` + XXX_raceDetectHookData protoimpl.RaceDetectHookData + XXX_presence [1]uint32 + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListStableUNIXUsersResponse) Reset() { + *x = ListStableUNIXUsersResponse{} + mi := &file_teleport_stableunixusers_v1_stableunixusers_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListStableUNIXUsersResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListStableUNIXUsersResponse) ProtoMessage() {} + +func (x *ListStableUNIXUsersResponse) ProtoReflect() protoreflect.Message { + mi := &file_teleport_stableunixusers_v1_stableunixusers_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +func (x *ListStableUNIXUsersResponse) GetStableUnixUsers() []*StableUNIXUser { + if x != nil { + if x.xxx_hidden_StableUnixUsers != nil { + return *x.xxx_hidden_StableUnixUsers + } + } + return nil +} + +func (x *ListStableUNIXUsersResponse) GetNextPageToken() string { + if x != nil { + if x.xxx_hidden_NextPageToken != nil { + return *x.xxx_hidden_NextPageToken + } + return "" + } + return "" +} + +func (x *ListStableUNIXUsersResponse) SetStableUnixUsers(v []*StableUNIXUser) { + x.xxx_hidden_StableUnixUsers = &v +} + +func (x *ListStableUNIXUsersResponse) SetNextPageToken(v string) { + x.xxx_hidden_NextPageToken = &v + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 1, 2) +} + +func (x *ListStableUNIXUsersResponse) HasNextPageToken() bool { + if x == nil { + return false + } + return protoimpl.X.Present(&(x.XXX_presence[0]), 1) +} + +func (x *ListStableUNIXUsersResponse) ClearNextPageToken() { + protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 1) + x.xxx_hidden_NextPageToken = nil +} + +type ListStableUNIXUsersResponse_builder struct { + _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. + + StableUnixUsers []*StableUNIXUser + // if empty, the listing has reached the end; otherwise, ListStableUNIXUsers + // can be called again with the new page_token to get more data + NextPageToken *string +} + +func (b0 ListStableUNIXUsersResponse_builder) Build() *ListStableUNIXUsersResponse { + m0 := &ListStableUNIXUsersResponse{} + b, x := &b0, m0 + _, _ = b, x + x.xxx_hidden_StableUnixUsers = &b.StableUnixUsers + if b.NextPageToken != nil { + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 1, 2) + x.xxx_hidden_NextPageToken = b.NextPageToken + } + return m0 +} + +var File_teleport_stableunixusers_v1_stableunixusers_proto protoreflect.FileDescriptor + +var file_teleport_stableunixusers_v1_stableunixusers_proto_rawDesc = []byte{ + 0x0a, 0x31, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x73, 0x74, 0x61, 0x62, 0x6c, + 0x65, 0x75, 0x6e, 0x69, 0x78, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x74, + 0x61, 0x62, 0x6c, 0x65, 0x75, 0x6e, 0x69, 0x78, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x12, 0x1b, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x73, 0x74, + 0x61, 0x62, 0x6c, 0x65, 0x75, 0x6e, 0x69, 0x78, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2e, 0x76, 0x31, + 0x22, 0x39, 0x0a, 0x1b, 0x4f, 0x62, 0x74, 0x61, 0x69, 0x6e, 0x55, 0x49, 0x44, 0x46, 0x6f, 0x72, + 0x55, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x30, 0x0a, 0x1c, 0x4f, + 0x62, 0x74, 0x61, 0x69, 0x6e, 0x55, 0x49, 0x44, 0x46, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x6e, + 0x61, 0x6d, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x75, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x03, 0x75, 0x69, 0x64, 0x22, 0x58, 0x0a, + 0x1a, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x4e, 0x49, 0x58, 0x55, + 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x70, + 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, + 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x65, + 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x61, + 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x3e, 0x0a, 0x0e, 0x53, 0x74, 0x61, 0x62, 0x6c, + 0x65, 0x55, 0x4e, 0x49, 0x58, 0x55, 0x73, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, + 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, + 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x05, 0x52, 0x03, 0x75, 0x69, 0x64, 0x22, 0x9e, 0x01, 0x0a, 0x1b, 0x4c, 0x69, 0x73, 0x74, + 0x53, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x4e, 0x49, 0x58, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x57, 0x0a, 0x11, 0x73, 0x74, 0x61, 0x62, 0x6c, + 0x65, 0x5f, 0x75, 0x6e, 0x69, 0x78, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x73, 0x74, + 0x61, 0x62, 0x6c, 0x65, 0x75, 0x6e, 0x69, 0x78, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2e, 0x76, 0x31, + 0x2e, 0x53, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x4e, 0x49, 0x58, 0x55, 0x73, 0x65, 0x72, 0x52, + 0x0f, 0x73, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x6e, 0x69, 0x78, 0x55, 0x73, 0x65, 0x72, 0x73, + 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, + 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, + 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x32, 0xbb, 0x02, 0x0a, 0x16, 0x53, 0x74, 0x61, + 0x62, 0x6c, 0x65, 0x55, 0x4e, 0x49, 0x58, 0x55, 0x73, 0x65, 0x72, 0x73, 0x53, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x12, 0x90, 0x01, 0x0a, 0x14, 0x4f, 0x62, 0x74, 0x61, 0x69, 0x6e, 0x55, 0x49, + 0x44, 0x46, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x38, 0x2e, 0x74, + 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x73, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x75, 0x6e, + 0x69, 0x78, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x62, 0x74, 0x61, 0x69, + 0x6e, 0x55, 0x49, 0x44, 0x46, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x39, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, + 0x74, 0x2e, 0x73, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x75, 0x6e, 0x69, 0x78, 0x75, 0x73, 0x65, 0x72, + 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x62, 0x74, 0x61, 0x69, 0x6e, 0x55, 0x49, 0x44, 0x46, 0x6f, + 0x72, 0x55, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x03, 0x90, 0x02, 0x02, 0x12, 0x8d, 0x01, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x53, + 0x74, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x4e, 0x49, 0x58, 0x55, 0x73, 0x65, 0x72, 0x73, 0x12, 0x37, + 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x73, 0x74, 0x61, 0x62, 0x6c, 0x65, + 0x75, 0x6e, 0x69, 0x78, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, + 0x74, 0x53, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x4e, 0x49, 0x58, 0x55, 0x73, 0x65, 0x72, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x38, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, + 0x72, 0x74, 0x2e, 0x73, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x75, 0x6e, 0x69, 0x78, 0x75, 0x73, 0x65, + 0x72, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x62, 0x6c, 0x65, + 0x55, 0x4e, 0x49, 0x58, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x03, 0x90, 0x02, 0x01, 0x42, 0x62, 0x5a, 0x60, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, + 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x76, 0x69, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x61, 0x6c, 0x2f, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x61, 0x70, 0x69, 0x2f, + 0x67, 0x65, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x6f, 0x2f, 0x74, 0x65, 0x6c, + 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x73, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x75, 0x6e, 0x69, 0x78, + 0x75, 0x73, 0x65, 0x72, 0x73, 0x2f, 0x76, 0x31, 0x3b, 0x73, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x75, + 0x6e, 0x69, 0x78, 0x75, 0x73, 0x65, 0x72, 0x73, 0x76, 0x31, 0x62, 0x08, 0x65, 0x64, 0x69, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x70, 0xe8, 0x07, +} + +var file_teleport_stableunixusers_v1_stableunixusers_proto_msgTypes = make([]protoimpl.MessageInfo, 5) +var file_teleport_stableunixusers_v1_stableunixusers_proto_goTypes = []any{ + (*ObtainUIDForUsernameRequest)(nil), // 0: teleport.stableunixusers.v1.ObtainUIDForUsernameRequest + (*ObtainUIDForUsernameResponse)(nil), // 1: teleport.stableunixusers.v1.ObtainUIDForUsernameResponse + (*ListStableUNIXUsersRequest)(nil), // 2: teleport.stableunixusers.v1.ListStableUNIXUsersRequest + (*StableUNIXUser)(nil), // 3: teleport.stableunixusers.v1.StableUNIXUser + (*ListStableUNIXUsersResponse)(nil), // 4: teleport.stableunixusers.v1.ListStableUNIXUsersResponse +} +var file_teleport_stableunixusers_v1_stableunixusers_proto_depIdxs = []int32{ + 3, // 0: teleport.stableunixusers.v1.ListStableUNIXUsersResponse.stable_unix_users:type_name -> teleport.stableunixusers.v1.StableUNIXUser + 0, // 1: teleport.stableunixusers.v1.StableUNIXUsersService.ObtainUIDForUsername:input_type -> teleport.stableunixusers.v1.ObtainUIDForUsernameRequest + 2, // 2: teleport.stableunixusers.v1.StableUNIXUsersService.ListStableUNIXUsers:input_type -> teleport.stableunixusers.v1.ListStableUNIXUsersRequest + 1, // 3: teleport.stableunixusers.v1.StableUNIXUsersService.ObtainUIDForUsername:output_type -> teleport.stableunixusers.v1.ObtainUIDForUsernameResponse + 4, // 4: teleport.stableunixusers.v1.StableUNIXUsersService.ListStableUNIXUsers:output_type -> teleport.stableunixusers.v1.ListStableUNIXUsersResponse + 3, // [3:5] is the sub-list for method output_type + 1, // [1:3] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_teleport_stableunixusers_v1_stableunixusers_proto_init() } +func file_teleport_stableunixusers_v1_stableunixusers_proto_init() { + if File_teleport_stableunixusers_v1_stableunixusers_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_teleport_stableunixusers_v1_stableunixusers_proto_rawDesc, + NumEnums: 0, + NumMessages: 5, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_teleport_stableunixusers_v1_stableunixusers_proto_goTypes, + DependencyIndexes: file_teleport_stableunixusers_v1_stableunixusers_proto_depIdxs, + MessageInfos: file_teleport_stableunixusers_v1_stableunixusers_proto_msgTypes, + }.Build() + File_teleport_stableunixusers_v1_stableunixusers_proto = out.File + file_teleport_stableunixusers_v1_stableunixusers_proto_rawDesc = nil + file_teleport_stableunixusers_v1_stableunixusers_proto_goTypes = nil + file_teleport_stableunixusers_v1_stableunixusers_proto_depIdxs = nil +} diff --git a/api/gen/proto/go/teleport/stableunixusers/v1/stableunixusers_grpc.pb.go b/api/gen/proto/go/teleport/stableunixusers/v1/stableunixusers_grpc.pb.go new file mode 100644 index 0000000000000..b87de00014469 --- /dev/null +++ b/api/gen/proto/go/teleport/stableunixusers/v1/stableunixusers_grpc.pb.go @@ -0,0 +1,178 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc (unknown) +// source: teleport/stableunixusers/v1/stableunixusers.proto + +package stableunixusersv1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + StableUNIXUsersService_ObtainUIDForUsername_FullMethodName = "/teleport.stableunixusers.v1.StableUNIXUsersService/ObtainUIDForUsername" + StableUNIXUsersService_ListStableUNIXUsers_FullMethodName = "/teleport.stableunixusers.v1.StableUNIXUsersService/ListStableUNIXUsers" +) + +// StableUNIXUsersServiceClient is the client API for StableUNIXUsersService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// contains the RPCs related to the stable UNIX users functionality +type StableUNIXUsersServiceClient interface { + // returns the assigned stable UID for a given UNIX username, assigning one + // from the configured UID range if necessary; if the feature is disabled or + // the available UID range is full the returned error will be + // RESOURCE_EXHAUSTED (i.e. trace.LimitExceededError), whereas an error caused + // by high concurrent access to the storage will be (incorrectly, alas) + // signaled by a FAILED_PRECONDITION error (i.e. trace.CompareFailedError) + ObtainUIDForUsername(ctx context.Context, in *ObtainUIDForUsernameRequest, opts ...grpc.CallOption) (*ObtainUIDForUsernameResponse, error) + // returns a page of username/UID pairs from the collection of stable UID assignments + ListStableUNIXUsers(ctx context.Context, in *ListStableUNIXUsersRequest, opts ...grpc.CallOption) (*ListStableUNIXUsersResponse, error) +} + +type stableUNIXUsersServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewStableUNIXUsersServiceClient(cc grpc.ClientConnInterface) StableUNIXUsersServiceClient { + return &stableUNIXUsersServiceClient{cc} +} + +func (c *stableUNIXUsersServiceClient) ObtainUIDForUsername(ctx context.Context, in *ObtainUIDForUsernameRequest, opts ...grpc.CallOption) (*ObtainUIDForUsernameResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ObtainUIDForUsernameResponse) + err := c.cc.Invoke(ctx, StableUNIXUsersService_ObtainUIDForUsername_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *stableUNIXUsersServiceClient) ListStableUNIXUsers(ctx context.Context, in *ListStableUNIXUsersRequest, opts ...grpc.CallOption) (*ListStableUNIXUsersResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListStableUNIXUsersResponse) + err := c.cc.Invoke(ctx, StableUNIXUsersService_ListStableUNIXUsers_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// StableUNIXUsersServiceServer is the server API for StableUNIXUsersService service. +// All implementations must embed UnimplementedStableUNIXUsersServiceServer +// for forward compatibility. +// +// contains the RPCs related to the stable UNIX users functionality +type StableUNIXUsersServiceServer interface { + // returns the assigned stable UID for a given UNIX username, assigning one + // from the configured UID range if necessary; if the feature is disabled or + // the available UID range is full the returned error will be + // RESOURCE_EXHAUSTED (i.e. trace.LimitExceededError), whereas an error caused + // by high concurrent access to the storage will be (incorrectly, alas) + // signaled by a FAILED_PRECONDITION error (i.e. trace.CompareFailedError) + ObtainUIDForUsername(context.Context, *ObtainUIDForUsernameRequest) (*ObtainUIDForUsernameResponse, error) + // returns a page of username/UID pairs from the collection of stable UID assignments + ListStableUNIXUsers(context.Context, *ListStableUNIXUsersRequest) (*ListStableUNIXUsersResponse, error) + mustEmbedUnimplementedStableUNIXUsersServiceServer() +} + +// UnimplementedStableUNIXUsersServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedStableUNIXUsersServiceServer struct{} + +func (UnimplementedStableUNIXUsersServiceServer) ObtainUIDForUsername(context.Context, *ObtainUIDForUsernameRequest) (*ObtainUIDForUsernameResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ObtainUIDForUsername not implemented") +} +func (UnimplementedStableUNIXUsersServiceServer) ListStableUNIXUsers(context.Context, *ListStableUNIXUsersRequest) (*ListStableUNIXUsersResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListStableUNIXUsers not implemented") +} +func (UnimplementedStableUNIXUsersServiceServer) mustEmbedUnimplementedStableUNIXUsersServiceServer() { +} +func (UnimplementedStableUNIXUsersServiceServer) testEmbeddedByValue() {} + +// UnsafeStableUNIXUsersServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to StableUNIXUsersServiceServer will +// result in compilation errors. +type UnsafeStableUNIXUsersServiceServer interface { + mustEmbedUnimplementedStableUNIXUsersServiceServer() +} + +func RegisterStableUNIXUsersServiceServer(s grpc.ServiceRegistrar, srv StableUNIXUsersServiceServer) { + // If the following call pancis, it indicates UnimplementedStableUNIXUsersServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&StableUNIXUsersService_ServiceDesc, srv) +} + +func _StableUNIXUsersService_ObtainUIDForUsername_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ObtainUIDForUsernameRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StableUNIXUsersServiceServer).ObtainUIDForUsername(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StableUNIXUsersService_ObtainUIDForUsername_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StableUNIXUsersServiceServer).ObtainUIDForUsername(ctx, req.(*ObtainUIDForUsernameRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _StableUNIXUsersService_ListStableUNIXUsers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListStableUNIXUsersRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StableUNIXUsersServiceServer).ListStableUNIXUsers(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StableUNIXUsersService_ListStableUNIXUsers_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StableUNIXUsersServiceServer).ListStableUNIXUsers(ctx, req.(*ListStableUNIXUsersRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// StableUNIXUsersService_ServiceDesc is the grpc.ServiceDesc for StableUNIXUsersService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var StableUNIXUsersService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "teleport.stableunixusers.v1.StableUNIXUsersService", + HandlerType: (*StableUNIXUsersServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "ObtainUIDForUsername", + Handler: _StableUNIXUsersService_ObtainUIDForUsername_Handler, + }, + { + MethodName: "ListStableUNIXUsers", + Handler: _StableUNIXUsersService_ListStableUNIXUsers_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "teleport/stableunixusers/v1/stableunixusers.proto", +} diff --git a/api/proto/teleport/stableunixusers/v1/stableunixusers.proto b/api/proto/teleport/stableunixusers/v1/stableunixusers.proto new file mode 100644 index 0000000000000..a509f9cf71d4e --- /dev/null +++ b/api/proto/teleport/stableunixusers/v1/stableunixusers.proto @@ -0,0 +1,70 @@ +// Copyright 2025 Gravitational, Inc. +// +// 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. + +edition = "2023"; + +package teleport.stableunixusers.v1; + +option go_package = "github.com/gravitational/teleport/api/gen/proto/go/teleport/stableunixusers/v1;stableunixusersv1"; + +// contains the RPCs related to the stable UNIX users functionality +service StableUNIXUsersService { + // returns the assigned stable UID for a given UNIX username, assigning one + // from the configured UID range if necessary; if the feature is disabled or + // the available UID range is full the returned error will be + // RESOURCE_EXHAUSTED (i.e. trace.LimitExceededError), whereas an error caused + // by high concurrent access to the storage will be (incorrectly, alas) + // signaled by a FAILED_PRECONDITION error (i.e. trace.CompareFailedError) + rpc ObtainUIDForUsername(ObtainUIDForUsernameRequest) returns (ObtainUIDForUsernameResponse) { + option idempotency_level = IDEMPOTENT; + } + + // returns a page of username/UID pairs from the collection of stable UID assignments + rpc ListStableUNIXUsers(ListStableUNIXUsersRequest) returns (ListStableUNIXUsersResponse) { + option idempotency_level = NO_SIDE_EFFECTS; + } +} + +// request message for ObtainUIDForUsername +message ObtainUIDForUsernameRequest { + string username = 1; +} + +// response message for ObtainUIDForUsername +message ObtainUIDForUsernameResponse { + int32 uid = 1; +} + +// request message for ListStableUNIXUsers +message ListStableUNIXUsersRequest { + // if left unset, the page size will default to a reasonable value chosen by + // the server + int32 page_size = 1; + // if unset or blank, the rpc will return the first page + string page_token = 2; +} + +// a pair of stable UNIX username and UID +message StableUNIXUser { + string username = 1; + int32 uid = 2; +} + +// response message for ListStableUNIXUsers +message ListStableUNIXUsersResponse { + repeated StableUNIXUser stable_unix_users = 1; + // if empty, the listing has reached the end; otherwise, ListStableUNIXUsers + // can be called again with the new page_token to get more data + string next_page_token = 2; +} diff --git a/api/types/constants.go b/api/types/constants.go index 10aa2322998d3..f8b2e5c2473f1 100644 --- a/api/types/constants.go +++ b/api/types/constants.go @@ -610,6 +610,10 @@ const ( // SubKindGitHub specifies the GitHub subkind of a Git server. SubKindGitHub = "github" + // KindStableUNIXUser is the RBAC-only kind to refer to interactions with + // stable UNIX users. + KindStableUNIXUser = "stable_unix_user" + // MetaNameAccessGraphSettings is the exact name of the singleton resource holding // access graph settings. MetaNameAccessGraphSettings = "access-graph-settings" diff --git a/buf-go.gen.yaml b/buf-go.gen.yaml index 8c8648b70854b..34235a3deacc2 100644 --- a/buf-go.gen.yaml +++ b/buf-go.gen.yaml @@ -28,6 +28,7 @@ plugins: # managed mode for the go package name there - Mprehog/v1alpha/connect.proto=github.com/gravitational/teleport/gen/proto/go/prehog/v1alpha;prehogv1alpha # buf (1.49.0 and earlier) panics on lint when encountering the option in the file itself (https://github.com/bufbuild/buf/issues/3580) + - apilevelMteleport/stableunixusers/v1/stableunixusers.proto=API_OPAQUE - apilevelMteleport/storage/local/stableunixusers/v1/stableunixusers.proto=API_OPAQUE strategy: all - local: diff --git a/gen/proto/go/teleport/storage/local/stableunixusers/v1/stableunixusers.pb.go b/gen/proto/go/teleport/storage/local/stableunixusers/v1/stableunixusers.pb.go new file mode 100644 index 0000000000000..2f8eedcab0ca1 --- /dev/null +++ b/gen/proto/go/teleport/storage/local/stableunixusers/v1/stableunixusers.pb.go @@ -0,0 +1,191 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.3 +// protoc (unknown) +// source: teleport/storage/local/stableunixusers/v1/stableunixusers.proto + +package stableunixusersv1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// a pair of stable UNIX username and UID, stored in +// "/stable_unix_users/by_username/" and "/stable_unix_users/by_uid/" +type StableUNIXUser struct { + state protoimpl.MessageState `protogen:"opaque.v1"` + xxx_hidden_Username *string `protobuf:"bytes,1,opt,name=username"` + xxx_hidden_Uid int32 `protobuf:"varint,2,opt,name=uid"` + XXX_raceDetectHookData protoimpl.RaceDetectHookData + XXX_presence [1]uint32 + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StableUNIXUser) Reset() { + *x = StableUNIXUser{} + mi := &file_teleport_storage_local_stableunixusers_v1_stableunixusers_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StableUNIXUser) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StableUNIXUser) ProtoMessage() {} + +func (x *StableUNIXUser) ProtoReflect() protoreflect.Message { + mi := &file_teleport_storage_local_stableunixusers_v1_stableunixusers_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +func (x *StableUNIXUser) GetUsername() string { + if x != nil { + if x.xxx_hidden_Username != nil { + return *x.xxx_hidden_Username + } + return "" + } + return "" +} + +func (x *StableUNIXUser) GetUid() int32 { + if x != nil { + return x.xxx_hidden_Uid + } + return 0 +} + +func (x *StableUNIXUser) SetUsername(v string) { + x.xxx_hidden_Username = &v + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 0, 2) +} + +func (x *StableUNIXUser) SetUid(v int32) { + x.xxx_hidden_Uid = v + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 1, 2) +} + +func (x *StableUNIXUser) HasUsername() bool { + if x == nil { + return false + } + return protoimpl.X.Present(&(x.XXX_presence[0]), 0) +} + +func (x *StableUNIXUser) HasUid() bool { + if x == nil { + return false + } + return protoimpl.X.Present(&(x.XXX_presence[0]), 1) +} + +func (x *StableUNIXUser) ClearUsername() { + protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 0) + x.xxx_hidden_Username = nil +} + +func (x *StableUNIXUser) ClearUid() { + protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 1) + x.xxx_hidden_Uid = 0 +} + +type StableUNIXUser_builder struct { + _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. + + Username *string + Uid *int32 +} + +func (b0 StableUNIXUser_builder) Build() *StableUNIXUser { + m0 := &StableUNIXUser{} + b, x := &b0, m0 + _, _ = b, x + if b.Username != nil { + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 0, 2) + x.xxx_hidden_Username = b.Username + } + if b.Uid != nil { + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 1, 2) + x.xxx_hidden_Uid = *b.Uid + } + return m0 +} + +var File_teleport_storage_local_stableunixusers_v1_stableunixusers_proto protoreflect.FileDescriptor + +var file_teleport_storage_local_stableunixusers_v1_stableunixusers_proto_rawDesc = []byte{ + 0x0a, 0x3f, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x61, + 0x67, 0x65, 0x2f, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x2f, 0x73, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x75, + 0x6e, 0x69, 0x78, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x74, 0x61, 0x62, + 0x6c, 0x65, 0x75, 0x6e, 0x69, 0x78, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x12, 0x29, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x73, 0x74, 0x6f, 0x72, + 0x61, 0x67, 0x65, 0x2e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x2e, 0x73, 0x74, 0x61, 0x62, 0x6c, 0x65, + 0x75, 0x6e, 0x69, 0x78, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2e, 0x76, 0x31, 0x22, 0x3e, 0x0a, 0x0e, + 0x53, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x4e, 0x49, 0x58, 0x55, 0x73, 0x65, 0x72, 0x12, 0x1a, + 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x69, + 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x03, 0x75, 0x69, 0x64, 0x42, 0x6c, 0x5a, 0x6a, + 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x76, 0x69, + 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x2f, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, + 0x74, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x6f, 0x2f, 0x74, + 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2f, + 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x2f, 0x73, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x75, 0x6e, 0x69, 0x78, + 0x75, 0x73, 0x65, 0x72, 0x73, 0x2f, 0x76, 0x31, 0x3b, 0x73, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x75, + 0x6e, 0x69, 0x78, 0x75, 0x73, 0x65, 0x72, 0x73, 0x76, 0x31, 0x62, 0x08, 0x65, 0x64, 0x69, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x70, 0xe8, 0x07, +} + +var file_teleport_storage_local_stableunixusers_v1_stableunixusers_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_teleport_storage_local_stableunixusers_v1_stableunixusers_proto_goTypes = []any{ + (*StableUNIXUser)(nil), // 0: teleport.storage.local.stableunixusers.v1.StableUNIXUser +} +var file_teleport_storage_local_stableunixusers_v1_stableunixusers_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_teleport_storage_local_stableunixusers_v1_stableunixusers_proto_init() } +func file_teleport_storage_local_stableunixusers_v1_stableunixusers_proto_init() { + if File_teleport_storage_local_stableunixusers_v1_stableunixusers_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_teleport_storage_local_stableunixusers_v1_stableunixusers_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_teleport_storage_local_stableunixusers_v1_stableunixusers_proto_goTypes, + DependencyIndexes: file_teleport_storage_local_stableunixusers_v1_stableunixusers_proto_depIdxs, + MessageInfos: file_teleport_storage_local_stableunixusers_v1_stableunixusers_proto_msgTypes, + }.Build() + File_teleport_storage_local_stableunixusers_v1_stableunixusers_proto = out.File + file_teleport_storage_local_stableunixusers_v1_stableunixusers_proto_rawDesc = nil + file_teleport_storage_local_stableunixusers_v1_stableunixusers_proto_goTypes = nil + file_teleport_storage_local_stableunixusers_v1_stableunixusers_proto_depIdxs = nil +} diff --git a/lib/auth/auth.go b/lib/auth/auth.go index fbd69a4451655..20adfb53a435b 100644 --- a/lib/auth/auth.go +++ b/lib/auth/auth.go @@ -410,6 +410,12 @@ func NewServer(cfg *InitConfig, opts ...ServerOption) (*Server, error) { } cfg.WorkloadIdentity = workloadIdentity } + if cfg.StableUNIXUsers == nil { + cfg.StableUNIXUsers = &local.StableUNIXUsersService{ + Backend: cfg.Backend, + } + } + if cfg.Logger == nil { cfg.Logger = slog.With(teleport.ComponentKey, teleport.ComponentAuth) } @@ -509,6 +515,7 @@ func NewServer(cfg *InitConfig, opts ...ServerOption) (*Server, error) { PluginStaticCredentials: cfg.PluginStaticCredentials, GitServers: cfg.GitServers, WorkloadIdentities: cfg.WorkloadIdentity, + StableUNIXUsersInternal: cfg.StableUNIXUsers, } as := Server{ @@ -736,6 +743,7 @@ type Services struct { services.PluginStaticCredentials services.GitServers services.WorkloadIdentities + services.StableUNIXUsersInternal } // GetWebSession returns existing web session described by req. diff --git a/lib/auth/grpcserver.go b/lib/auth/grpcserver.go index 5e803857b5e5f..5e13b17299aa4 100644 --- a/lib/auth/grpcserver.go +++ b/lib/auth/grpcserver.go @@ -68,6 +68,7 @@ import ( notificationsv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/notifications/v1" presencev1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/presence/v1" provisioningv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/provisioning/v1" + stableunixusersv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/stableunixusers/v1" trustv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/trust/v1" userloginstatev1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/userloginstate/v1" userprovisioningv2pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/userprovisioning/v2" @@ -5280,6 +5281,15 @@ func NewGRPCServer(cfg GRPCServerConfig) (*GRPCServer, error) { } dbobjectv1pb.RegisterDatabaseObjectServiceServer(server, dbObjectService) + stableUNIXUsersServiceServerInstance, err := newStableUNIXUsersServiceServer(cfg.Authorizer, cfg.AuthServer) + if err != nil { + return nil, trace.Wrap(err, "creating stable UNIX user service") + } + stableunixusersv1.RegisterStableUNIXUsersServiceServer( + server, + stableUNIXUsersServiceServerInstance, + ) + authServer := &GRPCServer{ APIConfig: cfg.APIConfig, logger: logger, diff --git a/lib/auth/init.go b/lib/auth/init.go index 296a4725f2653..b56eadf344f03 100644 --- a/lib/auth/init.go +++ b/lib/auth/init.go @@ -345,6 +345,9 @@ type InitConfig struct { // GitServers manages git servers. GitServers services.GitServers + + // StableUNIXUsers handles the storage for stable UNIX users. + StableUNIXUsers services.StableUNIXUsersInternal } // Init instantiates and configures an instance of AuthServer diff --git a/lib/auth/stableunixusers.go b/lib/auth/stableunixusers.go new file mode 100644 index 0000000000000..434b20b62d748 --- /dev/null +++ b/lib/auth/stableunixusers.go @@ -0,0 +1,259 @@ +// Teleport +// Copyright (C) 2025 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package auth + +import ( + "context" + "time" + + "github.com/gravitational/trace" + "google.golang.org/protobuf/proto" + + stableunixusersv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/stableunixusers/v1" + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/lib/authz" + "github.com/gravitational/teleport/lib/backend" + "github.com/gravitational/teleport/lib/services" + "github.com/gravitational/teleport/lib/services/readonly" + "github.com/gravitational/teleport/lib/utils" +) + +// newStableUNIXUsersServiceServer returns a [stableUNIXUsersServiceServer] +// using the given authorizer and server. +func newStableUNIXUsersServiceServer(authorizer authz.Authorizer, server *Server) (*stableUNIXUsersServiceServer, error) { + uidCache, err := utils.NewFnCache(utils.FnCacheConfig{ + TTL: 30 * time.Second, + Clock: server.clock, + Context: server.closeCtx, + ReloadOnErr: true, + }) + if err != nil { + return nil, trace.Wrap(err) + } + + return &stableUNIXUsersServiceServer{ + authorizer: authorizer, + + backend: server.bk, + readOnlyCache: server.ReadOnlyCache, + + stableUNIXUsers: server.Services.StableUNIXUsersInternal, + clusterConfiguration: server.Services.ClusterConfiguration, + + uidCache: uidCache, + + writerSem: make(chan struct{}, 1), + }, nil +} + +// stableUNIXUsersServiceServer is the auth server implementation for the stable +// UNIX users service, including the gRPC interface, authz enforcement, and +// business logic. +type stableUNIXUsersServiceServer struct { + stableunixusersv1.UnsafeStableUNIXUsersServiceServer + + authorizer authz.Authorizer + + backend backend.Backend + readOnlyCache *readonly.Cache + + stableUNIXUsers services.StableUNIXUsersInternal + clusterConfiguration services.ClusterConfigurationInternal + + // uidCache caches the fetched or created UIDs for each given username with + // a short-ish TTL, and combines concurrent requests for the same username. + uidCache *utils.FnCache + + // writerSem is a 1-buffered channel acting as a semaphore for writes, since + // concurrent writes would be almost guaranteed to race against each other + // otherwise. + writerSem chan struct{} +} + +var _ stableunixusersv1.StableUNIXUsersServiceServer = (*stableUNIXUsersServiceServer)(nil) + +// ListStableUNIXUsers implements [stableunixusersv1.StableUNIXUsersServiceServer]. +func (s *stableUNIXUsersServiceServer) ListStableUNIXUsers(ctx context.Context, req *stableunixusersv1.ListStableUNIXUsersRequest) (*stableunixusersv1.ListStableUNIXUsersResponse, error) { + authCtx, err := s.authorizer.Authorize(ctx) + if err != nil { + return nil, trace.Wrap(err) + } + + if err := authCtx.CheckAccessToKind(types.KindStableUNIXUser, types.VerbRead, types.VerbList); err != nil { + return nil, trace.Wrap(err) + } + + return s.listStableUNIXUsers(ctx, req) +} + +func (s *stableUNIXUsersServiceServer) listStableUNIXUsers(ctx context.Context, req *stableunixusersv1.ListStableUNIXUsersRequest) (*stableunixusersv1.ListStableUNIXUsersResponse, error) { + users, nextPageToken, err := s.stableUNIXUsers.ListStableUNIXUsers(ctx, int(req.GetPageSize()), req.GetPageToken()) + if err != nil { + return nil, trace.Wrap(err) + } + + userspb := make([]*stableunixusersv1.StableUNIXUser, 0, len(users)) + for _, user := range users { + userspb = append(userspb, stableunixusersv1.StableUNIXUser_builder{ + Username: proto.String(user.Username), + Uid: proto.Int32(user.UID), + }.Build()) + } + + return stableunixusersv1.ListStableUNIXUsersResponse_builder{ + StableUnixUsers: userspb, + NextPageToken: proto.String(nextPageToken), + }.Build(), nil +} + +// ObtainUIDForUsername implements [stableunixusersv1.StableUNIXUsersServiceServer]. +func (s *stableUNIXUsersServiceServer) ObtainUIDForUsername(ctx context.Context, req *stableunixusersv1.ObtainUIDForUsernameRequest) (*stableunixusersv1.ObtainUIDForUsernameResponse, error) { + authCtx, err := s.authorizer.Authorize(ctx) + if err != nil { + return nil, trace.Wrap(err) + } + + if err := authCtx.CheckAccessToKind(types.KindStableUNIXUser, types.VerbCreate, types.VerbRead); err != nil { + return nil, trace.Wrap(err) + } + + uid, err := s.obtainUIDForUsernameCached(ctx, req.GetUsername()) + if err != nil { + return nil, trace.Wrap(err) + } + + return stableunixusersv1.ObtainUIDForUsernameResponse_builder{ + Uid: proto.Int32(uid), + }.Build(), nil +} + +// obtainUIDForUsernameCached calls [obtainUIDForUsernameUncached] through the UID FnCache. +func (s *stableUNIXUsersServiceServer) obtainUIDForUsernameCached(ctx context.Context, username string) (int32, error) { + uid, err := utils.FnCacheGet(ctx, s.uidCache, username, func(ctx context.Context) (int32, error) { + return s.obtainUIDForUsernameUncached(ctx, username) + }) + if err != nil { + return 0, trace.Wrap(err) + } + return uid, nil +} + +// obtainUIDForUsernameUncached reads or creates the stable UID for the given username. +func (s *stableUNIXUsersServiceServer) obtainUIDForUsernameUncached(ctx context.Context, username string) (int32, error) { + if username == "" { + return 0, trace.BadParameter("username must not be empty") + } + + // we should only ever race with different auth servers on the same cluster, + // since we have a semaphore for the local auth + const maxAttempts = 3 + for attempt := range maxAttempts { + if attempt > 0 { + select { + case <-ctx.Done(): + return 0, trace.Wrap(context.Cause(ctx)) + case <-time.After(time.Duration(attempt) * 100 * time.Millisecond): + } + } + + uid, err := s.stableUNIXUsers.GetUIDForUsername(ctx, username) + if err == nil { + // TODO(espadolini): _potentially_ emit an audit log event with + // username and UID (it might spam the audit log unnecessarily) + return uid, nil + } + if !trace.IsNotFound(err) { + return 0, trace.Wrap(err) + } + + var authPref readonly.AuthPreference + if attempt == 0 { + ap, err := s.readOnlyCache.GetReadOnlyAuthPreference(ctx) + if err != nil { + return 0, trace.Wrap(err) + } + authPref = ap + } else { + ap, err := s.clusterConfiguration.GetAuthPreference(ctx) + if err != nil { + return 0, trace.Wrap(err) + } + authPref = ap + } + + uid, err = s.createNewStableUNIXUser(ctx, username, authPref) + if err != nil { + if trace.IsCompareFailed(err) { + continue + } + // if the readOnlyCache is a bit stale we might end up not checking + // the revision of the cached AuthPreference because the feature + // might appear to be disabled or the range might be exhausted; + // since the readOnlyCache is fed by the auth cache, it could + // potentially be stale for longer than the 1600ms of its internal + // FnCache, so we can't help but fall back to a backend fetch in + // that case + if attempt == 0 && trace.IsLimitExceeded(err) { + continue + } + return 0, trace.Wrap(err) + } + + // TODO(espadolini): emit an audit log event with the username and UID + // that was just created + + return uid, nil + } + + return 0, trace.CompareFailed("exhausted attempts to obtain UID for username") +} + +// createNewStableUNIXUser will search the configured UID range for a free UID +// and it will store an entry for the given username with that UID. +func (s *stableUNIXUsersServiceServer) createNewStableUNIXUser(ctx context.Context, username string, authPref readonly.AuthPreference) (int32, error) { + cfg := authPref.GetStableUNIXUserConfig() + if cfg == nil || !cfg.Enabled { + return 0, trace.LimitExceeded("stable UNIX users are not enabled") + } + + select { + case s.writerSem <- struct{}{}: + defer func() { <-s.writerSem }() + case <-ctx.Done(): + return 0, trace.Wrap(context.Cause(ctx)) + } + + uid, err := s.stableUNIXUsers.SearchFreeUID(ctx, cfg.FirstUid, cfg.LastUid) + if err != nil { + return 0, trace.Wrap(err) + } + + actions, err := s.stableUNIXUsers.AppendCreateStableUNIXUser(nil, username, uid) + if err != nil { + return 0, trace.Wrap(err) + } + actions, err = s.clusterConfiguration.AppendCheckAuthPreferenceActions(actions, authPref.GetRevision()) + if err != nil { + return 0, trace.Wrap(err) + } + + if _, err := s.backend.AtomicWrite(ctx, actions); err != nil { + return 0, trace.Wrap(err) + } + + return uid, nil +} diff --git a/lib/auth/stableunixusers_test.go b/lib/auth/stableunixusers_test.go new file mode 100644 index 0000000000000..489978d9df3cb --- /dev/null +++ b/lib/auth/stableunixusers_test.go @@ -0,0 +1,164 @@ +// Teleport +// Copyright (C) 2025 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package auth + +import ( + "context" + "fmt" + "testing" + + "github.com/gravitational/trace" + "github.com/stretchr/testify/require" + "golang.org/x/sync/errgroup" + "google.golang.org/protobuf/proto" + + stableunixusersv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/stableunixusers/v1" + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/lib/backend/memory" + "github.com/gravitational/teleport/lib/services/local" + "github.com/gravitational/teleport/lib/services/readonly" +) + +func TestStableUNIXUsers(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + bk, err := memory.New(memory.Config{Context: ctx}) + require.NoError(t, err) + defer bk.Close() + + clusterConfiguration := &local.ClusterConfigurationService{ + Backend: bk, + } + + readOnlyCache, err := readonly.NewCache(readonly.CacheConfig{ + Upstream: clusterConfiguration, + ReloadOnErr: true, + }) + require.NoError(t, err) + + svc := &stableUNIXUsersServiceServer{ + authorizer: nil, + + backend: bk, + readOnlyCache: readOnlyCache, + + stableUNIXUsers: &local.StableUNIXUsersService{ + Backend: bk, + }, + clusterConfiguration: clusterConfiguration, + + writerSem: make(chan struct{}, 1), + } + + const firstUID int32 = 90000 + const lastUID int32 = 90003 + + authPref, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{ + StableUnixUserConfig: &types.StableUNIXUserConfig{ + Enabled: true, + FirstUid: firstUID, + LastUid: lastUID, + }, + }) + require.NoError(t, err) + _, err = clusterConfiguration.UpsertAuthPreference(ctx, authPref) + require.NoError(t, err) + + uid1, err := svc.obtainUIDForUsernameUncached(ctx, "user1") + require.NoError(t, err) + require.Equal(t, firstUID, uid1) + + uid1, err = svc.obtainUIDForUsernameUncached(ctx, "user1") + require.NoError(t, err) + require.Equal(t, firstUID, uid1) + + uid2, err := svc.obtainUIDForUsernameUncached(ctx, "user2") + require.NoError(t, err) + require.Equal(t, firstUID+1, uid2) + + uid1, err = svc.obtainUIDForUsernameUncached(ctx, "user1") + require.NoError(t, err) + require.Equal(t, firstUID, uid1) + + uid2, err = svc.obtainUIDForUsernameUncached(ctx, "user2") + require.NoError(t, err) + require.Equal(t, firstUID+1, uid2) + + uid3, err := svc.obtainUIDForUsernameUncached(ctx, "user3") + require.NoError(t, err) + require.Equal(t, firstUID+2, uid3) + + uid4, err := svc.obtainUIDForUsernameUncached(ctx, "user4") + require.NoError(t, err) + require.Equal(t, firstUID+3, uid4) + + _, err = svc.obtainUIDForUsernameUncached(ctx, "user5") + require.ErrorAs(t, err, new(*trace.NotFoundError)) + + resp, err := svc.listStableUNIXUsers(ctx, stableunixusersv1.ListStableUNIXUsersRequest_builder{ + PageSize: proto.Int32(2), + }.Build()) + require.NoError(t, err) + require.Equal(t, "user3", resp.GetNextPageToken()) + require.Len(t, resp.GetStableUnixUsers(), 2) + + require.Equal(t, "user1", resp.GetStableUnixUsers()[0].GetUsername()) + require.Equal(t, firstUID, resp.GetStableUnixUsers()[0].GetUid()) + require.Equal(t, "user2", resp.GetStableUnixUsers()[1].GetUsername()) + require.Equal(t, firstUID+1, resp.GetStableUnixUsers()[1].GetUid()) + + resp, err = svc.listStableUNIXUsers(ctx, stableunixusersv1.ListStableUNIXUsersRequest_builder{ + PageSize: proto.Int32(2), + PageToken: proto.String("user3"), + }.Build()) + require.NoError(t, err) + require.Empty(t, resp.GetNextPageToken()) + require.Len(t, resp.GetStableUnixUsers(), 2) + + require.Equal(t, "user3", resp.GetStableUnixUsers()[0].GetUsername()) + require.Equal(t, firstUID+2, resp.GetStableUnixUsers()[0].GetUid()) + require.Equal(t, "user4", resp.GetStableUnixUsers()[1].GetUsername()) + require.Equal(t, firstUID+3, resp.GetStableUnixUsers()[1].GetUid()) + + authPref, err = types.NewAuthPreference(types.AuthPreferenceSpecV2{ + StableUnixUserConfig: &types.StableUNIXUserConfig{ + Enabled: true, + FirstUid: firstUID, + LastUid: firstUID + 2000, + }, + }) + require.NoError(t, err) + _, err = clusterConfiguration.UpsertAuthPreference(ctx, authPref) + require.NoError(t, err) + + readOnlyCache, err = readonly.NewCache(readonly.CacheConfig{ + Upstream: clusterConfiguration, + ReloadOnErr: true, + }) + require.NoError(t, err) + svc.readOnlyCache = readOnlyCache + + eg, ctx := errgroup.WithContext(ctx) + for i := range 1000 { + eg.Go(func() error { + _, err := svc.obtainUIDForUsernameUncached(ctx, fmt.Sprintf("parallel%05d", i)) + return err + }) + } + require.NoError(t, eg.Wait()) +} diff --git a/lib/authz/permissions.go b/lib/authz/permissions.go index b935e1caeacd0..8b3a875a82e36 100644 --- a/lib/authz/permissions.go +++ b/lib/authz/permissions.go @@ -1019,6 +1019,7 @@ func definitionForBuiltinRole(clusterName string, recConfig readonly.SessionReco types.NewRule(types.KindNetworkRestrictions, services.RO()), types.NewRule(types.KindConnectionDiagnostic, services.RW()), types.NewRule(types.KindStaticHostUser, services.RO()), + types.NewRule(types.KindStableUNIXUser, []string{types.VerbCreate, types.VerbRead}), }, }, })