From a0540c5afe6c8903c6be960202e02124ca32af7d Mon Sep 17 00:00:00 2001 From: Maxim Dietz Date: Wed, 30 Oct 2024 16:00:49 -0400 Subject: [PATCH] feat: Update AccessList Proto with new fields, add utils (#47828) - Add Hierarchy utils for nested AccessList validation and handling - Update AccessList Protos with new fields - Update AccessList and AccessListMember types and converters with new fields --- .../teleport/accesslist/v1/accesslist.pb.go | 570 ++++++++------ .../teleport/accesslist/v1/accesslist.proto | 27 +- api/types/accesslist/accesslist.go | 44 +- api/types/accesslist/convert/v1/accesslist.go | 98 ++- api/types/accesslist/member.go | 8 + .../resources.teleport.dev_accesslists.mdx | 1 + .../data-sources/access_list.mdx | 1 + .../resources/access_list.mdx | 1 + .../resources.teleport.dev_accesslists.yaml | 4 + .../teleport/accesslist/v1/accesslist_pb.ts | 105 ++- .../resources.teleport.dev_accesslists.yaml | 4 + .../accesslist/v1/accesslist_terraform.go | 44 ++ lib/accesslists/hierarchy.go | 743 ++++++++++++++++++ lib/accesslists/hierarchy_test.go | 666 ++++++++++++++++ 14 files changed, 2065 insertions(+), 251 deletions(-) create mode 100644 lib/accesslists/hierarchy.go create mode 100644 lib/accesslists/hierarchy_test.go diff --git a/api/gen/proto/go/teleport/accesslist/v1/accesslist.pb.go b/api/gen/proto/go/teleport/accesslist/v1/accesslist.pb.go index 55ec345396c34..4634ef46f0f91 100644 --- a/api/gen/proto/go/teleport/accesslist/v1/accesslist.pb.go +++ b/api/gen/proto/go/teleport/accesslist/v1/accesslist.pb.go @@ -147,6 +147,60 @@ func (ReviewDayOfMonth) EnumDescriptor() ([]byte, []int) { return file_teleport_accesslist_v1_accesslist_proto_rawDescGZIP(), []int{1} } +// MembershipKind represents the different kinds of list membership +type MembershipKind int32 + +const ( + // MEMBERSHIP_KIND_UNSPECIFIED represents list members that are of + // unknown membership kind, defaulting to being treated as type USER + MembershipKind_MEMBERSHIP_KIND_UNSPECIFIED MembershipKind = 0 + // MEMBERSHIP_KIND_USER represents list members that are normal users + MembershipKind_MEMBERSHIP_KIND_USER MembershipKind = 1 + // MEMBERSHIP_KIND_LIST represents list members that are nested Access Lists + MembershipKind_MEMBERSHIP_KIND_LIST MembershipKind = 2 +) + +// Enum value maps for MembershipKind. +var ( + MembershipKind_name = map[int32]string{ + 0: "MEMBERSHIP_KIND_UNSPECIFIED", + 1: "MEMBERSHIP_KIND_USER", + 2: "MEMBERSHIP_KIND_LIST", + } + MembershipKind_value = map[string]int32{ + "MEMBERSHIP_KIND_UNSPECIFIED": 0, + "MEMBERSHIP_KIND_USER": 1, + "MEMBERSHIP_KIND_LIST": 2, + } +) + +func (x MembershipKind) Enum() *MembershipKind { + p := new(MembershipKind) + *p = x + return p +} + +func (x MembershipKind) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (MembershipKind) Descriptor() protoreflect.EnumDescriptor { + return file_teleport_accesslist_v1_accesslist_proto_enumTypes[2].Descriptor() +} + +func (MembershipKind) Type() protoreflect.EnumType { + return &file_teleport_accesslist_v1_accesslist_proto_enumTypes[2] +} + +func (x MembershipKind) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use MembershipKind.Descriptor instead. +func (MembershipKind) EnumDescriptor() ([]byte, []int) { + return file_teleport_accesslist_v1_accesslist_proto_rawDescGZIP(), []int{2} +} + // IneligibleStatus describes how the user is ineligible. type IneligibleStatus int32 @@ -196,11 +250,11 @@ func (x IneligibleStatus) String() string { } func (IneligibleStatus) Descriptor() protoreflect.EnumDescriptor { - return file_teleport_accesslist_v1_accesslist_proto_enumTypes[2].Descriptor() + return file_teleport_accesslist_v1_accesslist_proto_enumTypes[3].Descriptor() } func (IneligibleStatus) Type() protoreflect.EnumType { - return &file_teleport_accesslist_v1_accesslist_proto_enumTypes[2] + return &file_teleport_accesslist_v1_accesslist_proto_enumTypes[3] } func (x IneligibleStatus) Number() protoreflect.EnumNumber { @@ -209,7 +263,7 @@ func (x IneligibleStatus) Number() protoreflect.EnumNumber { // Deprecated: Use IneligibleStatus.Descriptor instead. func (IneligibleStatus) EnumDescriptor() ([]byte, []int) { - return file_teleport_accesslist_v1_accesslist_proto_rawDescGZIP(), []int{2} + return file_teleport_accesslist_v1_accesslist_proto_rawDescGZIP(), []int{3} } // AccessList describes the basic building block of access grants, which are @@ -409,6 +463,9 @@ type AccessListOwner struct { // ineligible_status describes if this owner is eligible or not // and if not, describes how they're lacking eligibility. IneligibleStatus IneligibleStatus `protobuf:"varint,3,opt,name=ineligible_status,json=ineligibleStatus,proto3,enum=teleport.accesslist.v1.IneligibleStatus" json:"ineligible_status,omitempty"` + // membership_kind describes the type of membership, either + // `MEMBERSHIP_KIND_USER` or `MEMBERSHIP_KIND_LIST`. + MembershipKind MembershipKind `protobuf:"varint,4,opt,name=membership_kind,json=membershipKind,proto3,enum=teleport.accesslist.v1.MembershipKind" json:"membership_kind,omitempty"` } func (x *AccessListOwner) Reset() { @@ -462,6 +519,13 @@ func (x *AccessListOwner) GetIneligibleStatus() IneligibleStatus { return IneligibleStatus_INELIGIBLE_STATUS_UNSPECIFIED } +func (x *AccessListOwner) GetMembershipKind() MembershipKind { + if x != nil { + return x.MembershipKind + } + return MembershipKind_MEMBERSHIP_KIND_UNSPECIFIED +} + // AccessListAudit describes the audit configuration for an Access List. type AccessListAudit struct { state protoimpl.MessageState @@ -829,6 +893,9 @@ type MemberSpec struct { // ineligible_status describes if this member is eligible or not // and if not, describes how they're lacking eligibility. IneligibleStatus IneligibleStatus `protobuf:"varint,7,opt,name=ineligible_status,json=ineligibleStatus,proto3,enum=teleport.accesslist.v1.IneligibleStatus" json:"ineligible_status,omitempty"` + // membership_kind describes the type of membership, either + // `MEMBERSHIP_KIND_USER` or `MEMBERSHIP_KIND_LIST`. + MembershipKind MembershipKind `protobuf:"varint,9,opt,name=membership_kind,json=membershipKind,proto3,enum=teleport.accesslist.v1.MembershipKind" json:"membership_kind,omitempty"` } func (x *MemberSpec) Reset() { @@ -910,6 +977,13 @@ func (x *MemberSpec) GetIneligibleStatus() IneligibleStatus { return IneligibleStatus_INELIGIBLE_STATUS_UNSPECIFIED } +func (x *MemberSpec) GetMembershipKind() MembershipKind { + if x != nil { + return x.MembershipKind + } + return MembershipKind_MEMBERSHIP_KIND_UNSPECIFIED +} + // Review is a review of an Access List. type Review struct { state protoimpl.MessageState @@ -1133,8 +1207,14 @@ type AccessListStatus struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - // member_count is the number of members in the in the Access List. + // member_count is the number of members in the Access List. MemberCount *uint32 `protobuf:"varint,1,opt,name=member_count,json=memberCount,proto3,oneof" json:"member_count,omitempty"` + // member_list_count is the number of nested list members in the Access List. + MemberListCount *uint32 `protobuf:"varint,2,opt,name=member_list_count,json=memberListCount,proto3,oneof" json:"member_list_count,omitempty"` + // owner_of describes Access Lists where this Access List is an explicit owner. + OwnerOf []string `protobuf:"bytes,3,rep,name=owner_of,json=ownerOf,proto3" json:"owner_of,omitempty"` + // member_of describes Access Lists where this Access List is an explicit member. + MemberOf []string `protobuf:"bytes,4,rep,name=member_of,json=memberOf,proto3" json:"member_of,omitempty"` } func (x *AccessListStatus) Reset() { @@ -1174,6 +1254,27 @@ func (x *AccessListStatus) GetMemberCount() uint32 { return 0 } +func (x *AccessListStatus) GetMemberListCount() uint32 { + if x != nil && x.MemberListCount != nil { + return *x.MemberListCount + } + return 0 +} + +func (x *AccessListStatus) GetOwnerOf() []string { + if x != nil { + return x.OwnerOf + } + return nil +} + +func (x *AccessListStatus) GetMemberOf() []string { + if x != nil { + return x.MemberOf + } + return nil +} + var File_teleport_accesslist_v1_accesslist_proto protoreflect.FileDescriptor var file_teleport_accesslist_v1_accesslist_proto_rawDesc = []byte{ @@ -1238,7 +1339,7 @@ var file_teleport_accesslist_v1_accesslist_proto_rawDesc = []byte{ 0x72, 0x61, 0x6e, 0x74, 0x73, 0x4a, 0x04, 0x08, 0x07, 0x10, 0x08, 0x4a, 0x04, 0x08, 0x09, 0x10, 0x0a, 0x4a, 0x04, 0x08, 0x0a, 0x10, 0x0b, 0x52, 0x07, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x52, 0x0a, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x52, 0x09, 0x6f, 0x77, - 0x6e, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x22, 0x9e, 0x01, 0x0a, 0x0f, 0x41, 0x63, 0x63, 0x65, + 0x6e, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x22, 0xef, 0x01, 0x0a, 0x0f, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, @@ -1248,169 +1349,193 @@ var file_teleport_accesslist_v1_accesslist_proto_rawDesc = []byte{ 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x6c, 0x69, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x6e, 0x65, 0x6c, 0x69, 0x67, 0x69, 0x62, 0x6c, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x10, 0x69, 0x6e, 0x65, 0x6c, 0x69, 0x67, 0x69, 0x62, - 0x6c, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0xf7, 0x01, 0x0a, 0x0f, 0x41, 0x63, 0x63, - 0x65, 0x73, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, 0x64, 0x69, 0x74, 0x12, 0x42, 0x0a, 0x0f, - 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x61, 0x75, 0x64, 0x69, 0x74, 0x5f, 0x64, 0x61, 0x74, 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, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, - 0x70, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x41, 0x75, 0x64, 0x69, 0x74, 0x44, 0x61, 0x74, 0x65, - 0x12, 0x42, 0x0a, 0x0a, 0x72, 0x65, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, - 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x6c, 0x69, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, - 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x0a, 0x72, 0x65, 0x63, 0x75, 0x72, 0x72, - 0x65, 0x6e, 0x63, 0x65, 0x12, 0x4b, 0x0a, 0x0d, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x74, 0x65, - 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x6c, 0x69, 0x73, - 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x73, 0x52, 0x0d, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x73, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x52, 0x09, 0x66, 0x72, 0x65, 0x71, 0x75, 0x65, 0x6e, - 0x63, 0x79, 0x22, 0x9f, 0x01, 0x0a, 0x0a, 0x52, 0x65, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, - 0x65, 0x12, 0x45, 0x0a, 0x09, 0x66, 0x72, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x79, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0e, 0x32, 0x27, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, - 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x6c, 0x69, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, - 0x76, 0x69, 0x65, 0x77, 0x46, 0x72, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x79, 0x52, 0x09, 0x66, - 0x72, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x79, 0x12, 0x4a, 0x0a, 0x0c, 0x64, 0x61, 0x79, 0x5f, - 0x6f, 0x66, 0x5f, 0x6d, 0x6f, 0x6e, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x28, - 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, - 0x6c, 0x69, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x76, 0x69, 0x65, 0x77, 0x44, 0x61, - 0x79, 0x4f, 0x66, 0x4d, 0x6f, 0x6e, 0x74, 0x68, 0x52, 0x0a, 0x64, 0x61, 0x79, 0x4f, 0x66, 0x4d, - 0x6f, 0x6e, 0x74, 0x68, 0x22, 0x40, 0x0a, 0x0d, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x2f, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, - 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x22, 0x5c, 0x0a, 0x12, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, - 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x73, 0x12, 0x14, 0x0a, 0x05, - 0x72, 0x6f, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x72, 0x6f, 0x6c, - 0x65, 0x73, 0x12, 0x30, 0x0a, 0x06, 0x74, 0x72, 0x61, 0x69, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x74, 0x72, - 0x61, 0x69, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x72, 0x61, 0x69, 0x74, 0x52, 0x06, 0x74, 0x72, - 0x61, 0x69, 0x74, 0x73, 0x22, 0x5a, 0x0a, 0x10, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4c, 0x69, - 0x73, 0x74, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x6f, 0x6c, 0x65, - 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x72, 0x6f, 0x6c, 0x65, 0x73, 0x12, 0x30, - 0x0a, 0x06, 0x74, 0x72, 0x61, 0x69, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, - 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x74, 0x72, 0x61, 0x69, 0x74, 0x2e, - 0x76, 0x31, 0x2e, 0x54, 0x72, 0x61, 0x69, 0x74, 0x52, 0x06, 0x74, 0x72, 0x61, 0x69, 0x74, 0x73, - 0x22, 0x7c, 0x0a, 0x06, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x3a, 0x0a, 0x06, 0x68, 0x65, - 0x61, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x74, 0x65, 0x6c, - 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, - 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x52, 0x06, - 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x12, 0x36, 0x0a, 0x04, 0x73, 0x70, 0x65, 0x63, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, - 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x6c, 0x69, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x65, - 0x6d, 0x62, 0x65, 0x72, 0x53, 0x70, 0x65, 0x63, 0x52, 0x04, 0x73, 0x70, 0x65, 0x63, 0x22, 0xc7, - 0x02, 0x0a, 0x0a, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x53, 0x70, 0x65, 0x63, 0x12, 0x1f, 0x0a, - 0x0b, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0a, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x12, - 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, - 0x6d, 0x65, 0x12, 0x32, 0x0a, 0x06, 0x6a, 0x6f, 0x69, 0x6e, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x06, - 0x6a, 0x6f, 0x69, 0x6e, 0x65, 0x64, 0x12, 0x34, 0x0a, 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, - 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, - 0x61, 0x6d, 0x70, 0x52, 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x12, 0x16, 0x0a, 0x06, - 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, - 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x64, 0x64, 0x65, 0x64, 0x5f, 0x62, 0x79, - 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x65, 0x64, 0x42, 0x79, 0x12, - 0x55, 0x0a, 0x11, 0x69, 0x6e, 0x65, 0x6c, 0x69, 0x67, 0x69, 0x62, 0x6c, 0x65, 0x5f, 0x73, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x28, 0x2e, 0x74, 0x65, 0x6c, - 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x6c, 0x69, 0x73, 0x74, - 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x6e, 0x65, 0x6c, 0x69, 0x67, 0x69, 0x62, 0x6c, 0x65, 0x53, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x52, 0x10, 0x69, 0x6e, 0x65, 0x6c, 0x69, 0x67, 0x69, 0x62, 0x6c, 0x65, - 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x4a, 0x04, 0x08, 0x08, 0x10, 0x09, 0x52, 0x0a, 0x6d, 0x65, - 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x22, 0x7c, 0x0a, 0x06, 0x52, 0x65, 0x76, 0x69, - 0x65, 0x77, 0x12, 0x3a, 0x0a, 0x06, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x68, 0x65, - 0x61, 0x64, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x52, 0x06, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x12, 0x36, - 0x0a, 0x04, 0x73, 0x70, 0x65, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x74, + 0x6c, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x4f, 0x0a, 0x0f, 0x6d, 0x65, 0x6d, 0x62, + 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x5f, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x0e, 0x32, 0x26, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x61, 0x63, 0x63, + 0x65, 0x73, 0x73, 0x6c, 0x69, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x65, 0x6d, 0x62, 0x65, + 0x72, 0x73, 0x68, 0x69, 0x70, 0x4b, 0x69, 0x6e, 0x64, 0x52, 0x0e, 0x6d, 0x65, 0x6d, 0x62, 0x65, + 0x72, 0x73, 0x68, 0x69, 0x70, 0x4b, 0x69, 0x6e, 0x64, 0x22, 0xf7, 0x01, 0x0a, 0x0f, 0x41, 0x63, + 0x63, 0x65, 0x73, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, 0x64, 0x69, 0x74, 0x12, 0x42, 0x0a, + 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x61, 0x75, 0x64, 0x69, 0x74, 0x5f, 0x64, 0x61, 0x74, 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, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, + 0x6d, 0x70, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x41, 0x75, 0x64, 0x69, 0x74, 0x44, 0x61, 0x74, + 0x65, 0x12, 0x42, 0x0a, 0x0a, 0x72, 0x65, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, + 0x2e, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x6c, 0x69, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, + 0x65, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x0a, 0x72, 0x65, 0x63, 0x75, 0x72, + 0x72, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x4b, 0x0a, 0x0d, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x6c, 0x69, - 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x76, 0x69, 0x65, 0x77, 0x53, 0x70, 0x65, 0x63, - 0x52, 0x04, 0x73, 0x70, 0x65, 0x63, 0x22, 0xdf, 0x01, 0x0a, 0x0a, 0x52, 0x65, 0x76, 0x69, 0x65, - 0x77, 0x53, 0x70, 0x65, 0x63, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, - 0x6c, 0x69, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x63, 0x63, 0x65, - 0x73, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, - 0x65, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x72, 0x65, 0x76, 0x69, 0x65, - 0x77, 0x65, 0x72, 0x73, 0x12, 0x3b, 0x0a, 0x0b, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x5f, 0x64, - 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, - 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, - 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x44, 0x61, 0x74, - 0x65, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x6f, 0x74, 0x65, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x05, 0x6e, 0x6f, 0x74, 0x65, 0x73, 0x12, 0x3f, 0x0a, 0x07, 0x63, 0x68, 0x61, 0x6e, 0x67, - 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, - 0x6f, 0x72, 0x74, 0x2e, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x6c, 0x69, 0x73, 0x74, 0x2e, 0x76, - 0x31, 0x2e, 0x52, 0x65, 0x76, 0x69, 0x65, 0x77, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x52, - 0x07, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x22, 0x90, 0x03, 0x0a, 0x0d, 0x52, 0x65, 0x76, - 0x69, 0x65, 0x77, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x12, 0x72, 0x0a, 0x1f, 0x6d, 0x65, - 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x61, - 0x63, 0x63, 0x65, 0x73, 0x73, 0x6c, 0x69, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x63, 0x63, - 0x65, 0x73, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x73, 0x52, - 0x1d, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x52, 0x65, 0x71, 0x75, 0x69, - 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x12, 0x27, - 0x0a, 0x0f, 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x64, 0x5f, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, - 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0e, 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x64, - 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x12, 0x61, 0x0a, 0x18, 0x72, 0x65, 0x76, 0x69, 0x65, - 0x77, 0x5f, 0x66, 0x72, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x79, 0x5f, 0x63, 0x68, 0x61, 0x6e, - 0x67, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x27, 0x2e, 0x74, 0x65, 0x6c, 0x65, - 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x6c, 0x69, 0x73, 0x74, 0x2e, - 0x76, 0x31, 0x2e, 0x52, 0x65, 0x76, 0x69, 0x65, 0x77, 0x46, 0x72, 0x65, 0x71, 0x75, 0x65, 0x6e, - 0x63, 0x79, 0x52, 0x16, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x46, 0x72, 0x65, 0x71, 0x75, 0x65, - 0x6e, 0x63, 0x79, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x12, 0x66, 0x0a, 0x1b, 0x72, 0x65, - 0x76, 0x69, 0x65, 0x77, 0x5f, 0x64, 0x61, 0x79, 0x5f, 0x6f, 0x66, 0x5f, 0x6d, 0x6f, 0x6e, 0x74, - 0x68, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0e, 0x32, + 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x52, 0x0d, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x52, 0x09, 0x66, 0x72, 0x65, 0x71, 0x75, 0x65, + 0x6e, 0x63, 0x79, 0x22, 0x9f, 0x01, 0x0a, 0x0a, 0x52, 0x65, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, + 0x63, 0x65, 0x12, 0x45, 0x0a, 0x09, 0x66, 0x72, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x79, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x27, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, + 0x2e, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x6c, 0x69, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, + 0x65, 0x76, 0x69, 0x65, 0x77, 0x46, 0x72, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x79, 0x52, 0x09, + 0x66, 0x72, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x79, 0x12, 0x4a, 0x0a, 0x0c, 0x64, 0x61, 0x79, + 0x5f, 0x6f, 0x66, 0x5f, 0x6d, 0x6f, 0x6e, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x28, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x6c, 0x69, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x76, 0x69, 0x65, 0x77, 0x44, - 0x61, 0x79, 0x4f, 0x66, 0x4d, 0x6f, 0x6e, 0x74, 0x68, 0x52, 0x17, 0x72, 0x65, 0x76, 0x69, 0x65, - 0x77, 0x44, 0x61, 0x79, 0x4f, 0x66, 0x4d, 0x6f, 0x6e, 0x74, 0x68, 0x43, 0x68, 0x61, 0x6e, 0x67, - 0x65, 0x64, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x52, 0x11, 0x66, 0x72, 0x65, 0x71, 0x75, 0x65, - 0x6e, 0x63, 0x79, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x22, 0x4b, 0x0a, 0x10, 0x41, - 0x63, 0x63, 0x65, 0x73, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, - 0x26, 0x0a, 0x0c, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x00, 0x52, 0x0b, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x43, - 0x6f, 0x75, 0x6e, 0x74, 0x88, 0x01, 0x01, 0x42, 0x0f, 0x0a, 0x0d, 0x5f, 0x6d, 0x65, 0x6d, 0x62, - 0x65, 0x72, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2a, 0xb6, 0x01, 0x0a, 0x0f, 0x52, 0x65, 0x76, - 0x69, 0x65, 0x77, 0x46, 0x72, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x79, 0x12, 0x20, 0x0a, 0x1c, - 0x52, 0x45, 0x56, 0x49, 0x45, 0x57, 0x5f, 0x46, 0x52, 0x45, 0x51, 0x55, 0x45, 0x4e, 0x43, 0x59, - 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1e, - 0x0a, 0x1a, 0x52, 0x45, 0x56, 0x49, 0x45, 0x57, 0x5f, 0x46, 0x52, 0x45, 0x51, 0x55, 0x45, 0x4e, - 0x43, 0x59, 0x5f, 0x4f, 0x4e, 0x45, 0x5f, 0x4d, 0x4f, 0x4e, 0x54, 0x48, 0x10, 0x01, 0x12, 0x21, - 0x0a, 0x1d, 0x52, 0x45, 0x56, 0x49, 0x45, 0x57, 0x5f, 0x46, 0x52, 0x45, 0x51, 0x55, 0x45, 0x4e, - 0x43, 0x59, 0x5f, 0x54, 0x48, 0x52, 0x45, 0x45, 0x5f, 0x4d, 0x4f, 0x4e, 0x54, 0x48, 0x53, 0x10, - 0x03, 0x12, 0x1f, 0x0a, 0x1b, 0x52, 0x45, 0x56, 0x49, 0x45, 0x57, 0x5f, 0x46, 0x52, 0x45, 0x51, - 0x55, 0x45, 0x4e, 0x43, 0x59, 0x5f, 0x53, 0x49, 0x58, 0x5f, 0x4d, 0x4f, 0x4e, 0x54, 0x48, 0x53, - 0x10, 0x06, 0x12, 0x1d, 0x0a, 0x19, 0x52, 0x45, 0x56, 0x49, 0x45, 0x57, 0x5f, 0x46, 0x52, 0x45, - 0x51, 0x55, 0x45, 0x4e, 0x43, 0x59, 0x5f, 0x4f, 0x4e, 0x45, 0x5f, 0x59, 0x45, 0x41, 0x52, 0x10, - 0x0c, 0x2a, 0x97, 0x01, 0x0a, 0x10, 0x52, 0x65, 0x76, 0x69, 0x65, 0x77, 0x44, 0x61, 0x79, 0x4f, - 0x66, 0x4d, 0x6f, 0x6e, 0x74, 0x68, 0x12, 0x23, 0x0a, 0x1f, 0x52, 0x45, 0x56, 0x49, 0x45, 0x57, - 0x5f, 0x44, 0x41, 0x59, 0x5f, 0x4f, 0x46, 0x5f, 0x4d, 0x4f, 0x4e, 0x54, 0x48, 0x5f, 0x55, 0x4e, - 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1d, 0x0a, 0x19, 0x52, - 0x45, 0x56, 0x49, 0x45, 0x57, 0x5f, 0x44, 0x41, 0x59, 0x5f, 0x4f, 0x46, 0x5f, 0x4d, 0x4f, 0x4e, - 0x54, 0x48, 0x5f, 0x46, 0x49, 0x52, 0x53, 0x54, 0x10, 0x01, 0x12, 0x21, 0x0a, 0x1d, 0x52, 0x45, - 0x56, 0x49, 0x45, 0x57, 0x5f, 0x44, 0x41, 0x59, 0x5f, 0x4f, 0x46, 0x5f, 0x4d, 0x4f, 0x4e, 0x54, - 0x48, 0x5f, 0x46, 0x49, 0x46, 0x54, 0x45, 0x45, 0x4e, 0x54, 0x48, 0x10, 0x0f, 0x12, 0x1c, 0x0a, - 0x18, 0x52, 0x45, 0x56, 0x49, 0x45, 0x57, 0x5f, 0x44, 0x41, 0x59, 0x5f, 0x4f, 0x46, 0x5f, 0x4d, - 0x4f, 0x4e, 0x54, 0x48, 0x5f, 0x4c, 0x41, 0x53, 0x54, 0x10, 0x1f, 0x2a, 0xc6, 0x01, 0x0a, 0x10, - 0x49, 0x6e, 0x65, 0x6c, 0x69, 0x67, 0x69, 0x62, 0x6c, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x12, 0x21, 0x0a, 0x1d, 0x49, 0x4e, 0x45, 0x4c, 0x49, 0x47, 0x49, 0x42, 0x4c, 0x45, 0x5f, 0x53, - 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, - 0x44, 0x10, 0x00, 0x12, 0x1e, 0x0a, 0x1a, 0x49, 0x4e, 0x45, 0x4c, 0x49, 0x47, 0x49, 0x42, 0x4c, - 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x45, 0x4c, 0x49, 0x47, 0x49, 0x42, 0x4c, - 0x45, 0x10, 0x01, 0x12, 0x24, 0x0a, 0x20, 0x49, 0x4e, 0x45, 0x4c, 0x49, 0x47, 0x49, 0x42, 0x4c, - 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x53, 0x45, 0x52, 0x5f, 0x4e, 0x4f, - 0x54, 0x5f, 0x45, 0x58, 0x49, 0x53, 0x54, 0x10, 0x02, 0x12, 0x2a, 0x0a, 0x26, 0x49, 0x4e, 0x45, - 0x4c, 0x49, 0x47, 0x49, 0x42, 0x4c, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x4d, - 0x49, 0x53, 0x53, 0x49, 0x4e, 0x47, 0x5f, 0x52, 0x45, 0x51, 0x55, 0x49, 0x52, 0x45, 0x4d, 0x45, - 0x4e, 0x54, 0x53, 0x10, 0x03, 0x12, 0x1d, 0x0a, 0x19, 0x49, 0x4e, 0x45, 0x4c, 0x49, 0x47, 0x49, - 0x42, 0x4c, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x45, 0x58, 0x50, 0x49, 0x52, - 0x45, 0x44, 0x10, 0x04, 0x42, 0x58, 0x5a, 0x56, 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, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x6c, 0x69, 0x73, 0x74, 0x2f, 0x76, - 0x31, 0x3b, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x6c, 0x69, 0x73, 0x74, 0x76, 0x31, 0x62, 0x06, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x61, 0x79, 0x4f, 0x66, 0x4d, 0x6f, 0x6e, 0x74, 0x68, 0x52, 0x0a, 0x64, 0x61, 0x79, 0x4f, 0x66, + 0x4d, 0x6f, 0x6e, 0x74, 0x68, 0x22, 0x40, 0x0a, 0x0d, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x2f, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x22, 0x5c, 0x0a, 0x12, 0x41, 0x63, 0x63, 0x65, 0x73, + 0x73, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x73, 0x12, 0x14, 0x0a, + 0x05, 0x72, 0x6f, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x72, 0x6f, + 0x6c, 0x65, 0x73, 0x12, 0x30, 0x0a, 0x06, 0x74, 0x72, 0x61, 0x69, 0x74, 0x73, 0x18, 0x02, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x74, + 0x72, 0x61, 0x69, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x72, 0x61, 0x69, 0x74, 0x52, 0x06, 0x74, + 0x72, 0x61, 0x69, 0x74, 0x73, 0x22, 0x5a, 0x0a, 0x10, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4c, + 0x69, 0x73, 0x74, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x6f, 0x6c, + 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x72, 0x6f, 0x6c, 0x65, 0x73, 0x12, + 0x30, 0x0a, 0x06, 0x74, 0x72, 0x61, 0x69, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x18, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x74, 0x72, 0x61, 0x69, 0x74, + 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x72, 0x61, 0x69, 0x74, 0x52, 0x06, 0x74, 0x72, 0x61, 0x69, 0x74, + 0x73, 0x22, 0x7c, 0x0a, 0x06, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x3a, 0x0a, 0x06, 0x68, + 0x65, 0x61, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x74, 0x65, + 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x2e, 0x76, 0x31, + 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x52, + 0x06, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x12, 0x36, 0x0a, 0x04, 0x73, 0x70, 0x65, 0x63, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, + 0x2e, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x6c, 0x69, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4d, + 0x65, 0x6d, 0x62, 0x65, 0x72, 0x53, 0x70, 0x65, 0x63, 0x52, 0x04, 0x73, 0x70, 0x65, 0x63, 0x22, + 0x98, 0x03, 0x0a, 0x0a, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x53, 0x70, 0x65, 0x63, 0x12, 0x1f, + 0x0a, 0x0b, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x12, + 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, + 0x61, 0x6d, 0x65, 0x12, 0x32, 0x0a, 0x06, 0x6a, 0x6f, 0x69, 0x6e, 0x65, 0x64, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, + 0x06, 0x6a, 0x6f, 0x69, 0x6e, 0x65, 0x64, 0x12, 0x34, 0x0a, 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, + 0x65, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, + 0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x12, 0x16, 0x0a, + 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, + 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x64, 0x64, 0x65, 0x64, 0x5f, 0x62, + 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x65, 0x64, 0x42, 0x79, + 0x12, 0x55, 0x0a, 0x11, 0x69, 0x6e, 0x65, 0x6c, 0x69, 0x67, 0x69, 0x62, 0x6c, 0x65, 0x5f, 0x73, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x28, 0x2e, 0x74, 0x65, + 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x6c, 0x69, 0x73, + 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x6e, 0x65, 0x6c, 0x69, 0x67, 0x69, 0x62, 0x6c, 0x65, 0x53, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x10, 0x69, 0x6e, 0x65, 0x6c, 0x69, 0x67, 0x69, 0x62, 0x6c, + 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x4f, 0x0a, 0x0f, 0x6d, 0x65, 0x6d, 0x62, 0x65, + 0x72, 0x73, 0x68, 0x69, 0x70, 0x5f, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x26, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x61, 0x63, 0x63, 0x65, + 0x73, 0x73, 0x6c, 0x69, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, + 0x73, 0x68, 0x69, 0x70, 0x4b, 0x69, 0x6e, 0x64, 0x52, 0x0e, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, + 0x73, 0x68, 0x69, 0x70, 0x4b, 0x69, 0x6e, 0x64, 0x4a, 0x04, 0x08, 0x08, 0x10, 0x09, 0x52, 0x0a, + 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x22, 0x7c, 0x0a, 0x06, 0x52, 0x65, + 0x76, 0x69, 0x65, 0x77, 0x12, 0x3a, 0x0a, 0x06, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, + 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x52, 0x06, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, + 0x12, 0x36, 0x0a, 0x04, 0x73, 0x70, 0x65, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, + 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, + 0x6c, 0x69, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x76, 0x69, 0x65, 0x77, 0x53, 0x70, + 0x65, 0x63, 0x52, 0x04, 0x73, 0x70, 0x65, 0x63, 0x22, 0xdf, 0x01, 0x0a, 0x0a, 0x52, 0x65, 0x76, + 0x69, 0x65, 0x77, 0x53, 0x70, 0x65, 0x63, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x63, 0x63, 0x65, 0x73, + 0x73, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x63, + 0x63, 0x65, 0x73, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x72, 0x65, 0x76, 0x69, + 0x65, 0x77, 0x65, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x72, 0x65, 0x76, + 0x69, 0x65, 0x77, 0x65, 0x72, 0x73, 0x12, 0x3b, 0x0a, 0x0b, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, + 0x5f, 0x64, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x44, + 0x61, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x6f, 0x74, 0x65, 0x73, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x6e, 0x6f, 0x74, 0x65, 0x73, 0x12, 0x3f, 0x0a, 0x07, 0x63, 0x68, 0x61, + 0x6e, 0x67, 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x74, 0x65, 0x6c, + 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x6c, 0x69, 0x73, 0x74, + 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x76, 0x69, 0x65, 0x77, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, + 0x73, 0x52, 0x07, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x22, 0x90, 0x03, 0x0a, 0x0d, 0x52, + 0x65, 0x76, 0x69, 0x65, 0x77, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x12, 0x72, 0x0a, 0x1f, + 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x69, + 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, + 0x2e, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x6c, 0x69, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x41, + 0x63, 0x63, 0x65, 0x73, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, + 0x73, 0x52, 0x1d, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x52, 0x65, 0x71, + 0x75, 0x69, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, + 0x12, 0x27, 0x0a, 0x0f, 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x64, 0x5f, 0x6d, 0x65, 0x6d, 0x62, + 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0e, 0x72, 0x65, 0x6d, 0x6f, 0x76, + 0x65, 0x64, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x12, 0x61, 0x0a, 0x18, 0x72, 0x65, 0x76, + 0x69, 0x65, 0x77, 0x5f, 0x66, 0x72, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x79, 0x5f, 0x63, 0x68, + 0x61, 0x6e, 0x67, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x27, 0x2e, 0x74, 0x65, + 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x6c, 0x69, 0x73, + 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x76, 0x69, 0x65, 0x77, 0x46, 0x72, 0x65, 0x71, 0x75, + 0x65, 0x6e, 0x63, 0x79, 0x52, 0x16, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x46, 0x72, 0x65, 0x71, + 0x75, 0x65, 0x6e, 0x63, 0x79, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x12, 0x66, 0x0a, 0x1b, + 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x5f, 0x64, 0x61, 0x79, 0x5f, 0x6f, 0x66, 0x5f, 0x6d, 0x6f, + 0x6e, 0x74, 0x68, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x0e, 0x32, 0x28, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x61, 0x63, 0x63, + 0x65, 0x73, 0x73, 0x6c, 0x69, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x76, 0x69, 0x65, + 0x77, 0x44, 0x61, 0x79, 0x4f, 0x66, 0x4d, 0x6f, 0x6e, 0x74, 0x68, 0x52, 0x17, 0x72, 0x65, 0x76, + 0x69, 0x65, 0x77, 0x44, 0x61, 0x79, 0x4f, 0x66, 0x4d, 0x6f, 0x6e, 0x74, 0x68, 0x43, 0x68, 0x61, + 0x6e, 0x67, 0x65, 0x64, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x52, 0x11, 0x66, 0x72, 0x65, 0x71, + 0x75, 0x65, 0x6e, 0x63, 0x79, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x22, 0xca, 0x01, + 0x0a, 0x10, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x12, 0x26, 0x0a, 0x0c, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x5f, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x00, 0x52, 0x0b, 0x6d, 0x65, 0x6d, 0x62, + 0x65, 0x72, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x88, 0x01, 0x01, 0x12, 0x2f, 0x0a, 0x11, 0x6d, 0x65, + 0x6d, 0x62, 0x65, 0x72, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x01, 0x52, 0x0f, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x4c, + 0x69, 0x73, 0x74, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x88, 0x01, 0x01, 0x12, 0x19, 0x0a, 0x08, 0x6f, + 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x6f, 0x66, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x6f, + 0x77, 0x6e, 0x65, 0x72, 0x4f, 0x66, 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, + 0x5f, 0x6f, 0x66, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x6d, 0x65, 0x6d, 0x62, 0x65, + 0x72, 0x4f, 0x66, 0x42, 0x0f, 0x0a, 0x0d, 0x5f, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x5f, 0x63, + 0x6f, 0x75, 0x6e, 0x74, 0x42, 0x14, 0x0a, 0x12, 0x5f, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x5f, + 0x6c, 0x69, 0x73, 0x74, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2a, 0xb6, 0x01, 0x0a, 0x0f, 0x52, + 0x65, 0x76, 0x69, 0x65, 0x77, 0x46, 0x72, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x79, 0x12, 0x20, + 0x0a, 0x1c, 0x52, 0x45, 0x56, 0x49, 0x45, 0x57, 0x5f, 0x46, 0x52, 0x45, 0x51, 0x55, 0x45, 0x4e, + 0x43, 0x59, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, + 0x12, 0x1e, 0x0a, 0x1a, 0x52, 0x45, 0x56, 0x49, 0x45, 0x57, 0x5f, 0x46, 0x52, 0x45, 0x51, 0x55, + 0x45, 0x4e, 0x43, 0x59, 0x5f, 0x4f, 0x4e, 0x45, 0x5f, 0x4d, 0x4f, 0x4e, 0x54, 0x48, 0x10, 0x01, + 0x12, 0x21, 0x0a, 0x1d, 0x52, 0x45, 0x56, 0x49, 0x45, 0x57, 0x5f, 0x46, 0x52, 0x45, 0x51, 0x55, + 0x45, 0x4e, 0x43, 0x59, 0x5f, 0x54, 0x48, 0x52, 0x45, 0x45, 0x5f, 0x4d, 0x4f, 0x4e, 0x54, 0x48, + 0x53, 0x10, 0x03, 0x12, 0x1f, 0x0a, 0x1b, 0x52, 0x45, 0x56, 0x49, 0x45, 0x57, 0x5f, 0x46, 0x52, + 0x45, 0x51, 0x55, 0x45, 0x4e, 0x43, 0x59, 0x5f, 0x53, 0x49, 0x58, 0x5f, 0x4d, 0x4f, 0x4e, 0x54, + 0x48, 0x53, 0x10, 0x06, 0x12, 0x1d, 0x0a, 0x19, 0x52, 0x45, 0x56, 0x49, 0x45, 0x57, 0x5f, 0x46, + 0x52, 0x45, 0x51, 0x55, 0x45, 0x4e, 0x43, 0x59, 0x5f, 0x4f, 0x4e, 0x45, 0x5f, 0x59, 0x45, 0x41, + 0x52, 0x10, 0x0c, 0x2a, 0x97, 0x01, 0x0a, 0x10, 0x52, 0x65, 0x76, 0x69, 0x65, 0x77, 0x44, 0x61, + 0x79, 0x4f, 0x66, 0x4d, 0x6f, 0x6e, 0x74, 0x68, 0x12, 0x23, 0x0a, 0x1f, 0x52, 0x45, 0x56, 0x49, + 0x45, 0x57, 0x5f, 0x44, 0x41, 0x59, 0x5f, 0x4f, 0x46, 0x5f, 0x4d, 0x4f, 0x4e, 0x54, 0x48, 0x5f, + 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1d, 0x0a, + 0x19, 0x52, 0x45, 0x56, 0x49, 0x45, 0x57, 0x5f, 0x44, 0x41, 0x59, 0x5f, 0x4f, 0x46, 0x5f, 0x4d, + 0x4f, 0x4e, 0x54, 0x48, 0x5f, 0x46, 0x49, 0x52, 0x53, 0x54, 0x10, 0x01, 0x12, 0x21, 0x0a, 0x1d, + 0x52, 0x45, 0x56, 0x49, 0x45, 0x57, 0x5f, 0x44, 0x41, 0x59, 0x5f, 0x4f, 0x46, 0x5f, 0x4d, 0x4f, + 0x4e, 0x54, 0x48, 0x5f, 0x46, 0x49, 0x46, 0x54, 0x45, 0x45, 0x4e, 0x54, 0x48, 0x10, 0x0f, 0x12, + 0x1c, 0x0a, 0x18, 0x52, 0x45, 0x56, 0x49, 0x45, 0x57, 0x5f, 0x44, 0x41, 0x59, 0x5f, 0x4f, 0x46, + 0x5f, 0x4d, 0x4f, 0x4e, 0x54, 0x48, 0x5f, 0x4c, 0x41, 0x53, 0x54, 0x10, 0x1f, 0x2a, 0x65, 0x0a, + 0x0e, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x4b, 0x69, 0x6e, 0x64, 0x12, + 0x1f, 0x0a, 0x1b, 0x4d, 0x45, 0x4d, 0x42, 0x45, 0x52, 0x53, 0x48, 0x49, 0x50, 0x5f, 0x4b, 0x49, + 0x4e, 0x44, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, + 0x12, 0x18, 0x0a, 0x14, 0x4d, 0x45, 0x4d, 0x42, 0x45, 0x52, 0x53, 0x48, 0x49, 0x50, 0x5f, 0x4b, + 0x49, 0x4e, 0x44, 0x5f, 0x55, 0x53, 0x45, 0x52, 0x10, 0x01, 0x12, 0x18, 0x0a, 0x14, 0x4d, 0x45, + 0x4d, 0x42, 0x45, 0x52, 0x53, 0x48, 0x49, 0x50, 0x5f, 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x4c, 0x49, + 0x53, 0x54, 0x10, 0x02, 0x2a, 0xc6, 0x01, 0x0a, 0x10, 0x49, 0x6e, 0x65, 0x6c, 0x69, 0x67, 0x69, + 0x62, 0x6c, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x21, 0x0a, 0x1d, 0x49, 0x4e, 0x45, + 0x4c, 0x49, 0x47, 0x49, 0x42, 0x4c, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, + 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1e, 0x0a, 0x1a, + 0x49, 0x4e, 0x45, 0x4c, 0x49, 0x47, 0x49, 0x42, 0x4c, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, + 0x53, 0x5f, 0x45, 0x4c, 0x49, 0x47, 0x49, 0x42, 0x4c, 0x45, 0x10, 0x01, 0x12, 0x24, 0x0a, 0x20, + 0x49, 0x4e, 0x45, 0x4c, 0x49, 0x47, 0x49, 0x42, 0x4c, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, + 0x53, 0x5f, 0x55, 0x53, 0x45, 0x52, 0x5f, 0x4e, 0x4f, 0x54, 0x5f, 0x45, 0x58, 0x49, 0x53, 0x54, + 0x10, 0x02, 0x12, 0x2a, 0x0a, 0x26, 0x49, 0x4e, 0x45, 0x4c, 0x49, 0x47, 0x49, 0x42, 0x4c, 0x45, + 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x4d, 0x49, 0x53, 0x53, 0x49, 0x4e, 0x47, 0x5f, + 0x52, 0x45, 0x51, 0x55, 0x49, 0x52, 0x45, 0x4d, 0x45, 0x4e, 0x54, 0x53, 0x10, 0x03, 0x12, 0x1d, + 0x0a, 0x19, 0x49, 0x4e, 0x45, 0x4c, 0x49, 0x47, 0x49, 0x42, 0x4c, 0x45, 0x5f, 0x53, 0x54, 0x41, + 0x54, 0x55, 0x53, 0x5f, 0x45, 0x58, 0x50, 0x49, 0x52, 0x45, 0x44, 0x10, 0x04, 0x42, 0x58, 0x5a, + 0x56, 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, 0x61, 0x63, 0x63, + 0x65, 0x73, 0x73, 0x6c, 0x69, 0x73, 0x74, 0x2f, 0x76, 0x31, 0x3b, 0x61, 0x63, 0x63, 0x65, 0x73, + 0x73, 0x6c, 0x69, 0x73, 0x74, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -1425,67 +1550,70 @@ func file_teleport_accesslist_v1_accesslist_proto_rawDescGZIP() []byte { return file_teleport_accesslist_v1_accesslist_proto_rawDescData } -var file_teleport_accesslist_v1_accesslist_proto_enumTypes = make([]protoimpl.EnumInfo, 3) +var file_teleport_accesslist_v1_accesslist_proto_enumTypes = make([]protoimpl.EnumInfo, 4) var file_teleport_accesslist_v1_accesslist_proto_msgTypes = make([]protoimpl.MessageInfo, 14) var file_teleport_accesslist_v1_accesslist_proto_goTypes = []any{ (ReviewFrequency)(0), // 0: teleport.accesslist.v1.ReviewFrequency (ReviewDayOfMonth)(0), // 1: teleport.accesslist.v1.ReviewDayOfMonth - (IneligibleStatus)(0), // 2: teleport.accesslist.v1.IneligibleStatus - (*AccessList)(nil), // 3: teleport.accesslist.v1.AccessList - (*AccessListSpec)(nil), // 4: teleport.accesslist.v1.AccessListSpec - (*AccessListOwner)(nil), // 5: teleport.accesslist.v1.AccessListOwner - (*AccessListAudit)(nil), // 6: teleport.accesslist.v1.AccessListAudit - (*Recurrence)(nil), // 7: teleport.accesslist.v1.Recurrence - (*Notifications)(nil), // 8: teleport.accesslist.v1.Notifications - (*AccessListRequires)(nil), // 9: teleport.accesslist.v1.AccessListRequires - (*AccessListGrants)(nil), // 10: teleport.accesslist.v1.AccessListGrants - (*Member)(nil), // 11: teleport.accesslist.v1.Member - (*MemberSpec)(nil), // 12: teleport.accesslist.v1.MemberSpec - (*Review)(nil), // 13: teleport.accesslist.v1.Review - (*ReviewSpec)(nil), // 14: teleport.accesslist.v1.ReviewSpec - (*ReviewChanges)(nil), // 15: teleport.accesslist.v1.ReviewChanges - (*AccessListStatus)(nil), // 16: teleport.accesslist.v1.AccessListStatus - (*v1.ResourceHeader)(nil), // 17: teleport.header.v1.ResourceHeader - (*timestamppb.Timestamp)(nil), // 18: google.protobuf.Timestamp - (*durationpb.Duration)(nil), // 19: google.protobuf.Duration - (*v11.Trait)(nil), // 20: teleport.trait.v1.Trait + (MembershipKind)(0), // 2: teleport.accesslist.v1.MembershipKind + (IneligibleStatus)(0), // 3: teleport.accesslist.v1.IneligibleStatus + (*AccessList)(nil), // 4: teleport.accesslist.v1.AccessList + (*AccessListSpec)(nil), // 5: teleport.accesslist.v1.AccessListSpec + (*AccessListOwner)(nil), // 6: teleport.accesslist.v1.AccessListOwner + (*AccessListAudit)(nil), // 7: teleport.accesslist.v1.AccessListAudit + (*Recurrence)(nil), // 8: teleport.accesslist.v1.Recurrence + (*Notifications)(nil), // 9: teleport.accesslist.v1.Notifications + (*AccessListRequires)(nil), // 10: teleport.accesslist.v1.AccessListRequires + (*AccessListGrants)(nil), // 11: teleport.accesslist.v1.AccessListGrants + (*Member)(nil), // 12: teleport.accesslist.v1.Member + (*MemberSpec)(nil), // 13: teleport.accesslist.v1.MemberSpec + (*Review)(nil), // 14: teleport.accesslist.v1.Review + (*ReviewSpec)(nil), // 15: teleport.accesslist.v1.ReviewSpec + (*ReviewChanges)(nil), // 16: teleport.accesslist.v1.ReviewChanges + (*AccessListStatus)(nil), // 17: teleport.accesslist.v1.AccessListStatus + (*v1.ResourceHeader)(nil), // 18: teleport.header.v1.ResourceHeader + (*timestamppb.Timestamp)(nil), // 19: google.protobuf.Timestamp + (*durationpb.Duration)(nil), // 20: google.protobuf.Duration + (*v11.Trait)(nil), // 21: teleport.trait.v1.Trait } var file_teleport_accesslist_v1_accesslist_proto_depIdxs = []int32{ - 17, // 0: teleport.accesslist.v1.AccessList.header:type_name -> teleport.header.v1.ResourceHeader - 4, // 1: teleport.accesslist.v1.AccessList.spec:type_name -> teleport.accesslist.v1.AccessListSpec - 16, // 2: teleport.accesslist.v1.AccessList.status:type_name -> teleport.accesslist.v1.AccessListStatus - 5, // 3: teleport.accesslist.v1.AccessListSpec.owners:type_name -> teleport.accesslist.v1.AccessListOwner - 6, // 4: teleport.accesslist.v1.AccessListSpec.audit:type_name -> teleport.accesslist.v1.AccessListAudit - 9, // 5: teleport.accesslist.v1.AccessListSpec.membership_requires:type_name -> teleport.accesslist.v1.AccessListRequires - 9, // 6: teleport.accesslist.v1.AccessListSpec.ownership_requires:type_name -> teleport.accesslist.v1.AccessListRequires - 10, // 7: teleport.accesslist.v1.AccessListSpec.grants:type_name -> teleport.accesslist.v1.AccessListGrants - 10, // 8: teleport.accesslist.v1.AccessListSpec.owner_grants:type_name -> teleport.accesslist.v1.AccessListGrants - 2, // 9: teleport.accesslist.v1.AccessListOwner.ineligible_status:type_name -> teleport.accesslist.v1.IneligibleStatus - 18, // 10: teleport.accesslist.v1.AccessListAudit.next_audit_date:type_name -> google.protobuf.Timestamp - 7, // 11: teleport.accesslist.v1.AccessListAudit.recurrence:type_name -> teleport.accesslist.v1.Recurrence - 8, // 12: teleport.accesslist.v1.AccessListAudit.notifications:type_name -> teleport.accesslist.v1.Notifications - 0, // 13: teleport.accesslist.v1.Recurrence.frequency:type_name -> teleport.accesslist.v1.ReviewFrequency - 1, // 14: teleport.accesslist.v1.Recurrence.day_of_month:type_name -> teleport.accesslist.v1.ReviewDayOfMonth - 19, // 15: teleport.accesslist.v1.Notifications.start:type_name -> google.protobuf.Duration - 20, // 16: teleport.accesslist.v1.AccessListRequires.traits:type_name -> teleport.trait.v1.Trait - 20, // 17: teleport.accesslist.v1.AccessListGrants.traits:type_name -> teleport.trait.v1.Trait - 17, // 18: teleport.accesslist.v1.Member.header:type_name -> teleport.header.v1.ResourceHeader - 12, // 19: teleport.accesslist.v1.Member.spec:type_name -> teleport.accesslist.v1.MemberSpec - 18, // 20: teleport.accesslist.v1.MemberSpec.joined:type_name -> google.protobuf.Timestamp - 18, // 21: teleport.accesslist.v1.MemberSpec.expires:type_name -> google.protobuf.Timestamp - 2, // 22: teleport.accesslist.v1.MemberSpec.ineligible_status:type_name -> teleport.accesslist.v1.IneligibleStatus - 17, // 23: teleport.accesslist.v1.Review.header:type_name -> teleport.header.v1.ResourceHeader - 14, // 24: teleport.accesslist.v1.Review.spec:type_name -> teleport.accesslist.v1.ReviewSpec - 18, // 25: teleport.accesslist.v1.ReviewSpec.review_date:type_name -> google.protobuf.Timestamp - 15, // 26: teleport.accesslist.v1.ReviewSpec.changes:type_name -> teleport.accesslist.v1.ReviewChanges - 9, // 27: teleport.accesslist.v1.ReviewChanges.membership_requirements_changed:type_name -> teleport.accesslist.v1.AccessListRequires - 0, // 28: teleport.accesslist.v1.ReviewChanges.review_frequency_changed:type_name -> teleport.accesslist.v1.ReviewFrequency - 1, // 29: teleport.accesslist.v1.ReviewChanges.review_day_of_month_changed:type_name -> teleport.accesslist.v1.ReviewDayOfMonth - 30, // [30:30] is the sub-list for method output_type - 30, // [30:30] is the sub-list for method input_type - 30, // [30:30] is the sub-list for extension type_name - 30, // [30:30] is the sub-list for extension extendee - 0, // [0:30] is the sub-list for field type_name + 18, // 0: teleport.accesslist.v1.AccessList.header:type_name -> teleport.header.v1.ResourceHeader + 5, // 1: teleport.accesslist.v1.AccessList.spec:type_name -> teleport.accesslist.v1.AccessListSpec + 17, // 2: teleport.accesslist.v1.AccessList.status:type_name -> teleport.accesslist.v1.AccessListStatus + 6, // 3: teleport.accesslist.v1.AccessListSpec.owners:type_name -> teleport.accesslist.v1.AccessListOwner + 7, // 4: teleport.accesslist.v1.AccessListSpec.audit:type_name -> teleport.accesslist.v1.AccessListAudit + 10, // 5: teleport.accesslist.v1.AccessListSpec.membership_requires:type_name -> teleport.accesslist.v1.AccessListRequires + 10, // 6: teleport.accesslist.v1.AccessListSpec.ownership_requires:type_name -> teleport.accesslist.v1.AccessListRequires + 11, // 7: teleport.accesslist.v1.AccessListSpec.grants:type_name -> teleport.accesslist.v1.AccessListGrants + 11, // 8: teleport.accesslist.v1.AccessListSpec.owner_grants:type_name -> teleport.accesslist.v1.AccessListGrants + 3, // 9: teleport.accesslist.v1.AccessListOwner.ineligible_status:type_name -> teleport.accesslist.v1.IneligibleStatus + 2, // 10: teleport.accesslist.v1.AccessListOwner.membership_kind:type_name -> teleport.accesslist.v1.MembershipKind + 19, // 11: teleport.accesslist.v1.AccessListAudit.next_audit_date:type_name -> google.protobuf.Timestamp + 8, // 12: teleport.accesslist.v1.AccessListAudit.recurrence:type_name -> teleport.accesslist.v1.Recurrence + 9, // 13: teleport.accesslist.v1.AccessListAudit.notifications:type_name -> teleport.accesslist.v1.Notifications + 0, // 14: teleport.accesslist.v1.Recurrence.frequency:type_name -> teleport.accesslist.v1.ReviewFrequency + 1, // 15: teleport.accesslist.v1.Recurrence.day_of_month:type_name -> teleport.accesslist.v1.ReviewDayOfMonth + 20, // 16: teleport.accesslist.v1.Notifications.start:type_name -> google.protobuf.Duration + 21, // 17: teleport.accesslist.v1.AccessListRequires.traits:type_name -> teleport.trait.v1.Trait + 21, // 18: teleport.accesslist.v1.AccessListGrants.traits:type_name -> teleport.trait.v1.Trait + 18, // 19: teleport.accesslist.v1.Member.header:type_name -> teleport.header.v1.ResourceHeader + 13, // 20: teleport.accesslist.v1.Member.spec:type_name -> teleport.accesslist.v1.MemberSpec + 19, // 21: teleport.accesslist.v1.MemberSpec.joined:type_name -> google.protobuf.Timestamp + 19, // 22: teleport.accesslist.v1.MemberSpec.expires:type_name -> google.protobuf.Timestamp + 3, // 23: teleport.accesslist.v1.MemberSpec.ineligible_status:type_name -> teleport.accesslist.v1.IneligibleStatus + 2, // 24: teleport.accesslist.v1.MemberSpec.membership_kind:type_name -> teleport.accesslist.v1.MembershipKind + 18, // 25: teleport.accesslist.v1.Review.header:type_name -> teleport.header.v1.ResourceHeader + 15, // 26: teleport.accesslist.v1.Review.spec:type_name -> teleport.accesslist.v1.ReviewSpec + 19, // 27: teleport.accesslist.v1.ReviewSpec.review_date:type_name -> google.protobuf.Timestamp + 16, // 28: teleport.accesslist.v1.ReviewSpec.changes:type_name -> teleport.accesslist.v1.ReviewChanges + 10, // 29: teleport.accesslist.v1.ReviewChanges.membership_requirements_changed:type_name -> teleport.accesslist.v1.AccessListRequires + 0, // 30: teleport.accesslist.v1.ReviewChanges.review_frequency_changed:type_name -> teleport.accesslist.v1.ReviewFrequency + 1, // 31: teleport.accesslist.v1.ReviewChanges.review_day_of_month_changed:type_name -> teleport.accesslist.v1.ReviewDayOfMonth + 32, // [32:32] is the sub-list for method output_type + 32, // [32:32] is the sub-list for method input_type + 32, // [32:32] is the sub-list for extension type_name + 32, // [32:32] is the sub-list for extension extendee + 0, // [0:32] is the sub-list for field type_name } func init() { file_teleport_accesslist_v1_accesslist_proto_init() } @@ -1499,7 +1627,7 @@ func file_teleport_accesslist_v1_accesslist_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_teleport_accesslist_v1_accesslist_proto_rawDesc, - NumEnums: 3, + NumEnums: 4, NumMessages: 14, NumExtensions: 0, NumServices: 0, diff --git a/api/proto/teleport/accesslist/v1/accesslist.proto b/api/proto/teleport/accesslist/v1/accesslist.proto index b83034160a9e7..373c325027d39 100644 --- a/api/proto/teleport/accesslist/v1/accesslist.proto +++ b/api/proto/teleport/accesslist/v1/accesslist.proto @@ -85,6 +85,10 @@ message AccessListOwner { // ineligible_status describes if this owner is eligible or not // and if not, describes how they're lacking eligibility. IneligibleStatus ineligible_status = 3; + + // membership_kind describes the type of membership, either + // `MEMBERSHIP_KIND_USER` or `MEMBERSHIP_KIND_LIST`. + MembershipKind membership_kind = 4; } // AccessListAudit describes the audit configuration for an Access List. @@ -197,6 +201,21 @@ message MemberSpec { // ineligible_status describes if this member is eligible or not // and if not, describes how they're lacking eligibility. IneligibleStatus ineligible_status = 7; + + // membership_kind describes the type of membership, either + // `MEMBERSHIP_KIND_USER` or `MEMBERSHIP_KIND_LIST`. + MembershipKind membership_kind = 9; +} + +// MembershipKind represents the different kinds of list membership +enum MembershipKind { + // MEMBERSHIP_KIND_UNSPECIFIED represents list members that are of + // unknown membership kind, defaulting to being treated as type USER + MEMBERSHIP_KIND_UNSPECIFIED = 0; + // MEMBERSHIP_KIND_USER represents list members that are normal users + MEMBERSHIP_KIND_USER = 1; + // MEMBERSHIP_KIND_LIST represents list members that are nested Access Lists + MEMBERSHIP_KIND_LIST = 2; } // IneligibleStatus describes how the user is ineligible. @@ -268,6 +287,12 @@ message ReviewChanges { // AccessListStatus contains dynamic fields calculated during retrieval. message AccessListStatus { - // member_count is the number of members in the in the Access List. + // member_count is the number of members in the Access List. optional uint32 member_count = 1; + // member_list_count is the number of nested list members in the Access List. + optional uint32 member_list_count = 2; + // owner_of describes Access Lists where this Access List is an explicit owner. + repeated string owner_of = 3; + // member_of describes Access Lists where this Access List is an explicit member. + repeated string member_of = 4; } diff --git a/api/types/accesslist/accesslist.go b/api/types/accesslist/accesslist.go index b9f42f553e1e4..72ad2b43a329e 100644 --- a/api/types/accesslist/accesslist.go +++ b/api/types/accesslist/accesslist.go @@ -24,6 +24,7 @@ import ( "github.com/gravitational/trace" "github.com/jonboulle/clockwork" + accesslistv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/accesslist/v1" "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/api/types/header" "github.com/gravitational/teleport/api/types/header/convert/legacy" @@ -75,6 +76,20 @@ func parseReviewFrequency(input string) ReviewFrequency { return 0 } +// MaxAllowedDepth is the maximum allowed depth for nested access lists. +const MaxAllowedDepth = 10 + +var ( + // MembershipKindUnspecified is the default membership kind (treated as 'user'). + MembershipKindUnspecified = accesslistv1.MembershipKind_MEMBERSHIP_KIND_UNSPECIFIED.String() + + // MembershipKindUser is the user membership kind. + MembershipKindUser = accesslistv1.MembershipKind_MEMBERSHIP_KIND_USER.String() + + // MembershipKindList is the list membership kind. + MembershipKindList = accesslistv1.MembershipKind_MEMBERSHIP_KIND_LIST.String() +) + // ReviewDayOfMonth is the day of month the review should be repeated on. type ReviewDayOfMonth int @@ -123,7 +138,7 @@ type AccessList struct { Spec Spec `json:"spec" yaml:"spec"` // Status contains dynamically calculated fields. - Status Status `json:"-" yaml:"-"` + Status Status `json:"status" yaml:"status"` } // Spec is the specification for an access list. @@ -167,6 +182,10 @@ type Owner struct { // IneligibleStatus describes the reason why this owner is not eligible. IneligibleStatus string `json:"ineligible_status" yaml:"ineligible_status"` + + // MembershipKind describes the kind of ownership, + // either "MEMBERSHIP_KIND_USER" or "MEMBERSHIP_KIND_LIST". + MembershipKind string `json:"membership_kind" yaml:"membership_kind"` } // Audit describes the audit configuration for an access list. @@ -224,7 +243,14 @@ type Grants struct { // Status contains dynamic fields calculated during retrieval. type Status struct { // MemberCount is the number of members in the access list. - MemberCount *uint32 + MemberCount *uint32 `json:"-" yaml:"-"` + // MemberListCount is the number of members in the access list that are lists themselves. + MemberListCount *uint32 `json:"-" yaml:"-"` + + // OwnerOf is a list of Access List UUIDs where this access list is an explicit owner. + OwnerOf []string `json:"owner_of" yaml:"owner_of"` + // MemberOf is a list of Access List UUIDs where this access list is an explicit member. + MemberOf []string `json:"member_of" yaml:"member_of"` } // NewAccessList will create a new access list. @@ -286,10 +312,6 @@ func (a *AccessList) CheckAndSetDefaults() error { a.Spec.Audit.Notifications.Start = twoWeeks } - if len(a.Spec.Grants.Roles) == 0 && len(a.Spec.Grants.Traits) == 0 { - return trace.BadParameter("grants must specify at least one role or trait") - } - // Deduplicate owners. The backend will currently prevent this, but it's possible that access lists // were created with duplicated owners before the backend checked for duplicate owners. In order to // ensure that these access lists are backwards compatible, we'll deduplicate them here. @@ -299,6 +321,9 @@ func (a *AccessList) CheckAndSetDefaults() error { if owner.Name == "" { return trace.BadParameter("owner name is missing") } + if owner.MembershipKind == "" { + owner.MembershipKind = MembershipKindUser + } if _, ok := ownerMap[owner.Name]; ok { continue @@ -317,7 +342,7 @@ func (a *AccessList) GetOwners() []Owner { return a.Spec.Owners } -// GetOwners returns the list of owners from the access list. +// SetOwners sets the owners of the access list. func (a *AccessList) SetOwners(owners []Owner) { a.Spec.Owners = owners } @@ -337,6 +362,11 @@ func (a *AccessList) GetGrants() Grants { return a.Spec.Grants } +// GetOwnerGrants returns the owner grants from the access list. +func (a *AccessList) GetOwnerGrants() Grants { + return a.Spec.OwnerGrants +} + // GetMetadata returns metadata. This is specifically for conforming to the Resource interface, // and should be removed when possible. func (a *AccessList) GetMetadata() types.Metadata { diff --git a/api/types/accesslist/convert/v1/accesslist.go b/api/types/accesslist/convert/v1/accesslist.go index fb0d00c8a090f..9ff3aa0e36b48 100644 --- a/api/types/accesslist/convert/v1/accesslist.go +++ b/api/types/accesslist/convert/v1/accesslist.go @@ -68,13 +68,10 @@ func FromProto(msg *accesslistv1.AccessList, opts ...AccessListOption) (*accessl owners := make([]accesslist.Owner, len(msg.Spec.Owners)) for i, owner := range msg.Spec.Owners { - owners[i] = accesslist.Owner{ - Name: owner.Name, - Description: owner.Description, - // Set it to empty as default. - // Must provide as options to set it with the provided value. - IneligibleStatus: "", - } + owners[i] = FromOwnerProto(owner) + // Set IneligibleStatus to empty as default. + // Must provide as options to set it with the provided value. + owners[i].IneligibleStatus = "" } var ownerGrants accesslist.Grants @@ -94,10 +91,24 @@ func FromProto(msg *accesslistv1.AccessList, opts ...AccessListOption) (*accessl } var memberCount *uint32 + var memberListCount *uint32 if msg.Status != nil && msg.Status.MemberCount != nil { memberCount = new(uint32) *memberCount = *msg.Status.MemberCount } + if msg.Status != nil && msg.Status.MemberListCount != nil { + memberListCount = new(uint32) + *memberListCount = *msg.Status.MemberListCount + } + + var ownerOf []string + var memberOf []string + if msg.Status != nil && msg.Status.OwnerOf != nil { + ownerOf = msg.Status.OwnerOf + } + if msg.Status != nil && msg.Status.MemberOf != nil { + memberOf = msg.Status.MemberOf + } accessList, err := accesslist.NewAccessList(headerv1.FromMetadataProto(msg.Header.Metadata), accesslist.Spec{ Title: msg.Spec.Title, @@ -126,7 +137,10 @@ func FromProto(msg *accesslistv1.AccessList, opts ...AccessListOption) (*accessl return nil, trace.Wrap(err) } accessList.Status = accesslist.Status{ - MemberCount: memberCount, + MemberCount: memberCount, + MemberListCount: memberListCount, + OwnerOf: ownerOf, + MemberOf: memberOf, } for _, opt := range opts { @@ -140,15 +154,7 @@ func FromProto(msg *accesslistv1.AccessList, opts ...AccessListOption) (*accessl func ToProto(accessList *accesslist.AccessList) *accesslistv1.AccessList { owners := make([]*accesslistv1.AccessListOwner, len(accessList.Spec.Owners)) for i, owner := range accessList.Spec.Owners { - var ineligibleStatus accesslistv1.IneligibleStatus - if enumVal, ok := accesslistv1.IneligibleStatus_value[owner.IneligibleStatus]; ok { - ineligibleStatus = accesslistv1.IneligibleStatus(enumVal) - } - owners[i] = &accesslistv1.AccessListOwner{ - Name: owner.Name, - Description: owner.Description, - IneligibleStatus: ineligibleStatus, - } + owners[i] = ToOwnerProto(owner) } var ownerGrants *accesslistv1.AccessListGrants @@ -173,10 +179,24 @@ func ToProto(accessList *accesslist.AccessList) *accesslistv1.AccessList { } var memberCount *uint32 + var memberListCount *uint32 if accessList.Status.MemberCount != nil { memberCount = new(uint32) *memberCount = *accessList.Status.MemberCount } + if accessList.Status.MemberListCount != nil { + memberListCount = new(uint32) + *memberListCount = *accessList.Status.MemberListCount + } + + var ownerOf []string + var memberOf []string + if accessList.Status.OwnerOf != nil { + ownerOf = accessList.Status.OwnerOf + } + if accessList.Status.MemberOf != nil { + memberOf = accessList.Status.MemberOf + } return &accesslistv1.AccessList{ Header: headerv1.ToResourceHeaderProto(accessList.ResourceHeader), @@ -209,11 +229,53 @@ func ToProto(accessList *accesslist.AccessList) *accesslistv1.AccessList { OwnerGrants: ownerGrants, }, Status: &accesslistv1.AccessListStatus{ - MemberCount: memberCount, + MemberCount: memberCount, + MemberListCount: memberListCount, + OwnerOf: ownerOf, + MemberOf: memberOf, }, } } +// ToOwnerProto converts an internal access list owner into a v1 access list owner object. +func ToOwnerProto(owner accesslist.Owner) *accesslistv1.AccessListOwner { + var ineligibleStatus accesslistv1.IneligibleStatus + if owner.IneligibleStatus != "" { + if enumVal, ok := accesslistv1.IneligibleStatus_value[owner.IneligibleStatus]; ok { + ineligibleStatus = accesslistv1.IneligibleStatus(enumVal) + } + } else { + ineligibleStatus = accesslistv1.IneligibleStatus_INELIGIBLE_STATUS_UNSPECIFIED + } + + var kind accesslistv1.MembershipKind + if enumVal, ok := accesslistv1.MembershipKind_value[owner.MembershipKind]; ok { + kind = accesslistv1.MembershipKind(enumVal) + } + + return &accesslistv1.AccessListOwner{ + Name: owner.Name, + Description: owner.Description, + IneligibleStatus: ineligibleStatus, + MembershipKind: kind, + } +} + +// FromOwnerProto converts a v1 access list owner into an internal access list owner object. +func FromOwnerProto(protoOwner *accesslistv1.AccessListOwner) accesslist.Owner { + ineligibleStatus := "" + if protoOwner.IneligibleStatus != accesslistv1.IneligibleStatus_INELIGIBLE_STATUS_UNSPECIFIED { + ineligibleStatus = protoOwner.IneligibleStatus.String() + } + + return accesslist.Owner{ + Name: protoOwner.Name, + Description: protoOwner.Description, + IneligibleStatus: ineligibleStatus, + MembershipKind: protoOwner.MembershipKind.String(), + } +} + // WithOwnersIneligibleStatusField sets the "ineligibleStatus" field to the provided proto value. func WithOwnersIneligibleStatusField(protoOwners []*accesslistv1.AccessListOwner) AccessListOption { return func(a *accesslist.AccessList) { diff --git a/api/types/accesslist/member.go b/api/types/accesslist/member.go index 1ffeeccca8342..28a71ac1bcf5e 100644 --- a/api/types/accesslist/member.go +++ b/api/types/accesslist/member.go @@ -61,6 +61,10 @@ type AccessListMemberSpec struct { // IneligibleStatus describes the reason why this member is not eligible. IneligibleStatus string `json:"ineligible_status" yaml:"ineligible_status"` + + // MembershipKind describes the kind of membership, + // either "MEMBERSHIP_KIND_USER" or "MEMBERSHIP_KIND_LIST". + MembershipKind string `json:"membership_kind" yaml:"membership_kind"` } // NewAccessListMember will create a new access listm member. @@ -86,6 +90,10 @@ func (a *AccessListMember) CheckAndSetDefaults() error { return trace.Wrap(err) } + if a.Spec.MembershipKind == "" { + a.Spec.MembershipKind = MembershipKindUser + } + if a.Spec.AccessList == "" { return trace.BadParameter("access list is missing") } diff --git a/docs/pages/reference/operator-resources/resources.teleport.dev_accesslists.mdx b/docs/pages/reference/operator-resources/resources.teleport.dev_accesslists.mdx index 009cf351b2980..5b9d66fc1ea3e 100644 --- a/docs/pages/reference/operator-resources/resources.teleport.dev_accesslists.mdx +++ b/docs/pages/reference/operator-resources/resources.teleport.dev_accesslists.mdx @@ -83,6 +83,7 @@ resource, which you can apply after installing the Teleport Kubernetes operator. |---|---|---| |description|string|description is the plaintext description of the owner and why they are an owner.| |ineligible_status|string or integer|ineligible_status describes if this owner is eligible or not and if not, describes how they're lacking eligibility. Can be either the string or the integer representation of each option.| +|membership_kind|string or integer|membership_kind describes the type of membership, either `MEMBERSHIP_KIND_USER` or `MEMBERSHIP_KIND_LIST`. Can be either the string or the integer representation of each option.| |name|string|name is the username of the owner.| ### spec.ownership_requires diff --git a/docs/pages/reference/terraform-provider/data-sources/access_list.mdx b/docs/pages/reference/terraform-provider/data-sources/access_list.mdx index b5ddd133fdb07..48cd5e48c0b51 100644 --- a/docs/pages/reference/terraform-provider/data-sources/access_list.mdx +++ b/docs/pages/reference/terraform-provider/data-sources/access_list.mdx @@ -113,6 +113,7 @@ Optional: Optional: - `description` (String) description is the plaintext description of the owner and why they are an owner. +- `membership_kind` (Number) membership_kind describes the type of membership, either `MEMBERSHIP_KIND_USER` or `MEMBERSHIP_KIND_LIST`. - `name` (String) name is the username of the owner. diff --git a/docs/pages/reference/terraform-provider/resources/access_list.mdx b/docs/pages/reference/terraform-provider/resources/access_list.mdx index 974584d3714ec..e45b98b603069 100644 --- a/docs/pages/reference/terraform-provider/resources/access_list.mdx +++ b/docs/pages/reference/terraform-provider/resources/access_list.mdx @@ -155,6 +155,7 @@ Optional: Optional: - `description` (String) description is the plaintext description of the owner and why they are an owner. +- `membership_kind` (Number) membership_kind describes the type of membership, either `MEMBERSHIP_KIND_USER` or `MEMBERSHIP_KIND_LIST`. - `name` (String) name is the username of the owner. diff --git a/examples/chart/teleport-cluster/charts/teleport-operator/operator-crds/resources.teleport.dev_accesslists.yaml b/examples/chart/teleport-cluster/charts/teleport-operator/operator-crds/resources.teleport.dev_accesslists.yaml index 802e2a4f13a11..2c595617b69d3 100644 --- a/examples/chart/teleport-cluster/charts/teleport-operator/operator-crds/resources.teleport.dev_accesslists.yaml +++ b/examples/chart/teleport-cluster/charts/teleport-operator/operator-crds/resources.teleport.dev_accesslists.yaml @@ -153,6 +153,10 @@ spec: description: ineligible_status describes if this owner is eligible or not and if not, describes how they're lacking eligibility. x-kubernetes-int-or-string: true + membership_kind: + description: membership_kind describes the type of membership, + either `MEMBERSHIP_KIND_USER` or `MEMBERSHIP_KIND_LIST`. + x-kubernetes-int-or-string: true name: description: name is the username of the owner. type: string diff --git a/gen/proto/ts/teleport/accesslist/v1/accesslist_pb.ts b/gen/proto/ts/teleport/accesslist/v1/accesslist_pb.ts index 11062df8a111e..58f76b98dd6e8 100644 --- a/gen/proto/ts/teleport/accesslist/v1/accesslist_pb.ts +++ b/gen/proto/ts/teleport/accesslist/v1/accesslist_pb.ts @@ -145,6 +145,13 @@ export interface AccessListOwner { * @generated from protobuf field: teleport.accesslist.v1.IneligibleStatus ineligible_status = 3; */ ineligibleStatus: IneligibleStatus; + /** + * membership_kind describes the type of membership, either + * `MEMBERSHIP_KIND_USER` or `MEMBERSHIP_KIND_LIST`. + * + * @generated from protobuf field: teleport.accesslist.v1.MembershipKind membership_kind = 4; + */ + membershipKind: MembershipKind; } /** * AccessListAudit describes the audit configuration for an Access List. @@ -319,6 +326,13 @@ export interface MemberSpec { * @generated from protobuf field: teleport.accesslist.v1.IneligibleStatus ineligible_status = 7; */ ineligibleStatus: IneligibleStatus; + /** + * membership_kind describes the type of membership, either + * `MEMBERSHIP_KIND_USER` or `MEMBERSHIP_KIND_LIST`. + * + * @generated from protobuf field: teleport.accesslist.v1.MembershipKind membership_kind = 9; + */ + membershipKind: MembershipKind; } /** * Review is a review of an Access List. @@ -418,11 +432,29 @@ export interface ReviewChanges { */ export interface AccessListStatus { /** - * member_count is the number of members in the in the Access List. + * member_count is the number of members in the Access List. * * @generated from protobuf field: optional uint32 member_count = 1; */ memberCount?: number; + /** + * member_list_count is the number of nested list members in the Access List. + * + * @generated from protobuf field: optional uint32 member_list_count = 2; + */ + memberListCount?: number; + /** + * owner_of describes Access Lists where this Access List is an explicit owner. + * + * @generated from protobuf field: repeated string owner_of = 3; + */ + ownerOf: string[]; + /** + * member_of describes Access Lists where this Access List is an explicit member. + * + * @generated from protobuf field: repeated string member_of = 4; + */ + memberOf: string[]; } /** * ReviewFrequency is the frequency of reviews. @@ -474,6 +506,32 @@ export enum ReviewDayOfMonth { */ LAST = 31 } +/** + * MembershipKind represents the different kinds of list membership + * + * @generated from protobuf enum teleport.accesslist.v1.MembershipKind + */ +export enum MembershipKind { + /** + * MEMBERSHIP_KIND_UNSPECIFIED represents list members that are of + * unknown membership kind, defaulting to being treated as type USER + * + * @generated from protobuf enum value: MEMBERSHIP_KIND_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + /** + * MEMBERSHIP_KIND_USER represents list members that are normal users + * + * @generated from protobuf enum value: MEMBERSHIP_KIND_USER = 1; + */ + USER = 1, + /** + * MEMBERSHIP_KIND_LIST represents list members that are nested Access Lists + * + * @generated from protobuf enum value: MEMBERSHIP_KIND_LIST = 2; + */ + LIST = 2 +} /** * IneligibleStatus describes how the user is ineligible. * @@ -679,7 +737,8 @@ class AccessListOwner$Type extends MessageType { super("teleport.accesslist.v1.AccessListOwner", [ { no: 1, name: "name", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, { no: 2, name: "description", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, - { no: 3, name: "ineligible_status", kind: "enum", T: () => ["teleport.accesslist.v1.IneligibleStatus", IneligibleStatus, "INELIGIBLE_STATUS_"] } + { no: 3, name: "ineligible_status", kind: "enum", T: () => ["teleport.accesslist.v1.IneligibleStatus", IneligibleStatus, "INELIGIBLE_STATUS_"] }, + { no: 4, name: "membership_kind", kind: "enum", T: () => ["teleport.accesslist.v1.MembershipKind", MembershipKind, "MEMBERSHIP_KIND_"] } ]); } create(value?: PartialMessage): AccessListOwner { @@ -687,6 +746,7 @@ class AccessListOwner$Type extends MessageType { message.name = ""; message.description = ""; message.ineligibleStatus = 0; + message.membershipKind = 0; if (value !== undefined) reflectionMergePartial(this, message, value); return message; @@ -705,6 +765,9 @@ class AccessListOwner$Type extends MessageType { case /* teleport.accesslist.v1.IneligibleStatus ineligible_status */ 3: message.ineligibleStatus = reader.int32(); break; + case /* teleport.accesslist.v1.MembershipKind membership_kind */ 4: + message.membershipKind = reader.int32(); + break; default: let u = options.readUnknownField; if (u === "throw") @@ -726,6 +789,9 @@ class AccessListOwner$Type extends MessageType { /* teleport.accesslist.v1.IneligibleStatus ineligible_status = 3; */ if (message.ineligibleStatus !== 0) writer.tag(3, WireType.Varint).int32(message.ineligibleStatus); + /* teleport.accesslist.v1.MembershipKind membership_kind = 4; */ + if (message.membershipKind !== 0) + writer.tag(4, WireType.Varint).int32(message.membershipKind); let u = options.writeUnknownFields; if (u !== false) (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); @@ -1070,7 +1136,8 @@ class MemberSpec$Type extends MessageType { { no: 4, name: "expires", kind: "message", T: () => Timestamp }, { no: 5, name: "reason", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, { no: 6, name: "added_by", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, - { no: 7, name: "ineligible_status", kind: "enum", T: () => ["teleport.accesslist.v1.IneligibleStatus", IneligibleStatus, "INELIGIBLE_STATUS_"] } + { no: 7, name: "ineligible_status", kind: "enum", T: () => ["teleport.accesslist.v1.IneligibleStatus", IneligibleStatus, "INELIGIBLE_STATUS_"] }, + { no: 9, name: "membership_kind", kind: "enum", T: () => ["teleport.accesslist.v1.MembershipKind", MembershipKind, "MEMBERSHIP_KIND_"] } ]); } create(value?: PartialMessage): MemberSpec { @@ -1080,6 +1147,7 @@ class MemberSpec$Type extends MessageType { message.reason = ""; message.addedBy = ""; message.ineligibleStatus = 0; + message.membershipKind = 0; if (value !== undefined) reflectionMergePartial(this, message, value); return message; @@ -1110,6 +1178,9 @@ class MemberSpec$Type extends MessageType { case /* teleport.accesslist.v1.IneligibleStatus ineligible_status */ 7: message.ineligibleStatus = reader.int32(); break; + case /* teleport.accesslist.v1.MembershipKind membership_kind */ 9: + message.membershipKind = reader.int32(); + break; default: let u = options.readUnknownField; if (u === "throw") @@ -1143,6 +1214,9 @@ class MemberSpec$Type extends MessageType { /* teleport.accesslist.v1.IneligibleStatus ineligible_status = 7; */ if (message.ineligibleStatus !== 0) writer.tag(7, WireType.Varint).int32(message.ineligibleStatus); + /* teleport.accesslist.v1.MembershipKind membership_kind = 9; */ + if (message.membershipKind !== 0) + writer.tag(9, WireType.Varint).int32(message.membershipKind); let u = options.writeUnknownFields; if (u !== false) (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); @@ -1357,11 +1431,16 @@ export const ReviewChanges = new ReviewChanges$Type(); class AccessListStatus$Type extends MessageType { constructor() { super("teleport.accesslist.v1.AccessListStatus", [ - { no: 1, name: "member_count", kind: "scalar", opt: true, T: 13 /*ScalarType.UINT32*/ } + { no: 1, name: "member_count", kind: "scalar", opt: true, T: 13 /*ScalarType.UINT32*/ }, + { no: 2, name: "member_list_count", kind: "scalar", opt: true, T: 13 /*ScalarType.UINT32*/ }, + { no: 3, name: "owner_of", kind: "scalar", repeat: 2 /*RepeatType.UNPACKED*/, T: 9 /*ScalarType.STRING*/ }, + { no: 4, name: "member_of", kind: "scalar", repeat: 2 /*RepeatType.UNPACKED*/, T: 9 /*ScalarType.STRING*/ } ]); } create(value?: PartialMessage): AccessListStatus { const message = globalThis.Object.create((this.messagePrototype!)); + message.ownerOf = []; + message.memberOf = []; if (value !== undefined) reflectionMergePartial(this, message, value); return message; @@ -1374,6 +1453,15 @@ class AccessListStatus$Type extends MessageType { case /* optional uint32 member_count */ 1: message.memberCount = reader.uint32(); break; + case /* optional uint32 member_list_count */ 2: + message.memberListCount = reader.uint32(); + break; + case /* repeated string owner_of */ 3: + message.ownerOf.push(reader.string()); + break; + case /* repeated string member_of */ 4: + message.memberOf.push(reader.string()); + break; default: let u = options.readUnknownField; if (u === "throw") @@ -1389,6 +1477,15 @@ class AccessListStatus$Type extends MessageType { /* optional uint32 member_count = 1; */ if (message.memberCount !== undefined) writer.tag(1, WireType.Varint).uint32(message.memberCount); + /* optional uint32 member_list_count = 2; */ + if (message.memberListCount !== undefined) + writer.tag(2, WireType.Varint).uint32(message.memberListCount); + /* repeated string owner_of = 3; */ + for (let i = 0; i < message.ownerOf.length; i++) + writer.tag(3, WireType.LengthDelimited).string(message.ownerOf[i]); + /* repeated string member_of = 4; */ + for (let i = 0; i < message.memberOf.length; i++) + writer.tag(4, WireType.LengthDelimited).string(message.memberOf[i]); let u = options.writeUnknownFields; if (u !== false) (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); diff --git a/integrations/operator/config/crd/bases/resources.teleport.dev_accesslists.yaml b/integrations/operator/config/crd/bases/resources.teleport.dev_accesslists.yaml index 802e2a4f13a11..2c595617b69d3 100644 --- a/integrations/operator/config/crd/bases/resources.teleport.dev_accesslists.yaml +++ b/integrations/operator/config/crd/bases/resources.teleport.dev_accesslists.yaml @@ -153,6 +153,10 @@ spec: description: ineligible_status describes if this owner is eligible or not and if not, describes how they're lacking eligibility. x-kubernetes-int-or-string: true + membership_kind: + description: membership_kind describes the type of membership, + either `MEMBERSHIP_KIND_USER` or `MEMBERSHIP_KIND_LIST`. + x-kubernetes-int-or-string: true name: description: name is the username of the owner. type: string diff --git a/integrations/terraform/tfschema/accesslist/v1/accesslist_terraform.go b/integrations/terraform/tfschema/accesslist/v1/accesslist_terraform.go index ef0bd9464ef7a..33058a3bad9a4 100644 --- a/integrations/terraform/tfschema/accesslist/v1/accesslist_terraform.go +++ b/integrations/terraform/tfschema/accesslist/v1/accesslist_terraform.go @@ -245,6 +245,11 @@ func GenSchemaAccessList(ctx context.Context) (github_com_hashicorp_terraform_pl Optional: true, Type: github_com_hashicorp_terraform_plugin_framework_types.StringType, }, + "membership_kind": { + Description: "membership_kind describes the type of membership, either `MEMBERSHIP_KIND_USER` or `MEMBERSHIP_KIND_LIST`.", + Optional: true, + Type: github_com_hashicorp_terraform_plugin_framework_types.Int64Type, + }, "name": { Description: "name is the username of the owner.", Optional: true, @@ -571,6 +576,23 @@ func CopyAccessListFromTerraform(_ context.Context, tf github_com_hashicorp_terr } } } + { + a, ok := tf.Attrs["membership_kind"] + if !ok { + diags.Append(attrReadMissingDiag{"AccessList.spec.owners.membership_kind"}) + } else { + v, ok := a.(github_com_hashicorp_terraform_plugin_framework_types.Int64) + if !ok { + diags.Append(attrReadConversionFailureDiag{"AccessList.spec.owners.membership_kind", "github.com/hashicorp/terraform-plugin-framework/types.Int64"}) + } else { + var t github_com_gravitational_teleport_api_gen_proto_go_teleport_accesslist_v1.MembershipKind + if !v.Null && !v.Unknown { + t = github_com_gravitational_teleport_api_gen_proto_go_teleport_accesslist_v1.MembershipKind(v.Value) + } + obj.MembershipKind = t + } + } + } } obj.Owners[k] = t } @@ -1599,6 +1621,28 @@ func CopyAccessListToTerraform(ctx context.Context, obj *github_com_gravitationa tf.Attrs["description"] = v } } + { + t, ok := tf.AttrTypes["membership_kind"] + if !ok { + diags.Append(attrWriteMissingDiag{"AccessList.spec.owners.membership_kind"}) + } else { + v, ok := tf.Attrs["membership_kind"].(github_com_hashicorp_terraform_plugin_framework_types.Int64) + if !ok { + i, err := t.ValueFromTerraform(ctx, github_com_hashicorp_terraform_plugin_go_tftypes.NewValue(t.TerraformType(ctx), nil)) + if err != nil { + diags.Append(attrWriteGeneralError{"AccessList.spec.owners.membership_kind", err}) + } + v, ok = i.(github_com_hashicorp_terraform_plugin_framework_types.Int64) + if !ok { + diags.Append(attrWriteConversionFailureDiag{"AccessList.spec.owners.membership_kind", "github.com/hashicorp/terraform-plugin-framework/types.Int64"}) + } + v.Null = int64(obj.MembershipKind) == 0 + } + v.Value = int64(obj.MembershipKind) + v.Unknown = false + tf.Attrs["membership_kind"] = v + } + } } v.Unknown = false c.Elems[k] = v diff --git a/lib/accesslists/hierarchy.go b/lib/accesslists/hierarchy.go new file mode 100644 index 0000000000000..5b7778aa91245 --- /dev/null +++ b/lib/accesslists/hierarchy.go @@ -0,0 +1,743 @@ +/* + * Teleport + * Copyright (C) 2024 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 accesslists + +import ( + "context" + "slices" + + "github.com/gravitational/trace" + "github.com/jonboulle/clockwork" + + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/api/types/accesslist" + "github.com/gravitational/teleport/api/types/trait" + "github.com/gravitational/teleport/lib/services" +) + +// RelationshipKind represents the type of relationship: member or owner. +type RelationshipKind int + +const ( + RelationshipKindMember RelationshipKind = iota + RelationshipKindOwner +) + +// MembershipOrOwnershipType represents the type of membership or ownership a User has for an Access List. +type MembershipOrOwnershipType int + +const ( + // MembershipOrOwnershipTypeNone indicates that the User lacks valid Membership or Ownership for the Access List. + MembershipOrOwnershipTypeNone MembershipOrOwnershipType = iota + // MembershipOrOwnershipTypeExplicit indicates that the User has explicit Membership or Ownership for the Access List. + MembershipOrOwnershipTypeExplicit + // MembershipOrOwnershipTypeInherited indicates that the User has inherited Membership or Ownership for the Access List. + MembershipOrOwnershipTypeInherited +) + +// AccessListAndMembersGetter is a minimal interface for fetching AccessLists by name, and AccessListMembers for an Access List. +type AccessListAndMembersGetter interface { + ListAccessListMembers(ctx context.Context, accessListName string, pageSize int, pageToken string) (members []*accesslist.AccessListMember, nextToken string, err error) + GetAccessList(ctx context.Context, accessListName string) (*accesslist.AccessList, error) +} + +// GetMembersFor returns a flattened list of Members for an Access List, including inherited Members. +// +// Returned Members are not validated for expiration or other requirements – use IsAccessListMember +// to validate a Member's membership status. +func GetMembersFor(ctx context.Context, accessListName string, g AccessListAndMembersGetter) ([]*accesslist.AccessListMember, error) { + return getMembersFor(ctx, accessListName, g, make(map[string]struct{})) +} + +func getMembersFor(ctx context.Context, accessListName string, g AccessListAndMembersGetter, visited map[string]struct{}) ([]*accesslist.AccessListMember, error) { + if _, ok := visited[accessListName]; ok { + return nil, nil + } + visited[accessListName] = struct{}{} + + members, err := fetchMembers(ctx, accessListName, g) + if err != nil { + return nil, trace.Wrap(err) + } + + var allMembers []*accesslist.AccessListMember + for _, member := range members { + if member.Spec.MembershipKind != accesslist.MembershipKindList { + allMembers = append(allMembers, member) + continue + } + childMembers, err := getMembersFor(ctx, member.Spec.Name, g, visited) + if err != nil { + return nil, trace.Wrap(err) + } + allMembers = append(allMembers, childMembers...) + } + + return allMembers, nil +} + +// fetchMembers is a simple helper to fetch all top-level AccessListMembers for an AccessList. +func fetchMembers(ctx context.Context, accessListName string, g AccessListAndMembersGetter) ([]*accesslist.AccessListMember, error) { + var allMembers []*accesslist.AccessListMember + pageToken := "" + for { + page, nextToken, err := g.ListAccessListMembers(ctx, accessListName, 0, pageToken) + if err != nil { + // If the AccessList doesn't exist yet, should return an empty list of members + if trace.IsNotFound(err) { + break + } + return nil, trace.Wrap(err) + } + allMembers = append(allMembers, page...) + if nextToken == "" { + break + } + pageToken = nextToken + } + return allMembers, nil +} + +// ValidateAccessListWithMembers validates a new or existing AccessList with a list of AccessListMembers. +func ValidateAccessListWithMembers(ctx context.Context, accessList *accesslist.AccessList, members []*accesslist.AccessListMember, g AccessListAndMembersGetter) error { + for _, owner := range accessList.Spec.Owners { + if owner.MembershipKind != accesslist.MembershipKindList { + continue + } + ownerList, err := g.GetAccessList(ctx, owner.Name) + if err != nil { + return trace.Wrap(err) + } + if err := validateAddition(ctx, accessList, ownerList, RelationshipKindOwner, g); err != nil { + return trace.Wrap(err) + } + } + for _, member := range members { + if member.Spec.MembershipKind != accesslist.MembershipKindList { + continue + } + memberList, err := g.GetAccessList(ctx, member.Spec.Name) + if err != nil { + return trace.Wrap(err) + } + if err := validateAddition(ctx, accessList, memberList, RelationshipKindMember, g); err != nil { + return trace.Wrap(err) + } + } + return nil +} + +// collectOwners is a helper to recursively collect all Owners for an Access List, including inherited Owners. +func collectOwners(ctx context.Context, accessList *accesslist.AccessList, g AccessListAndMembersGetter, owners map[string]*accesslist.Owner, visited map[string]struct{}) error { + //owners := make([]*accesslist.Owner, 0) + if _, ok := visited[accessList.GetName()]; ok { + return nil + } + visited[accessList.GetName()] = struct{}{} + + for _, owner := range accessList.Spec.Owners { + if owner.MembershipKind != accesslist.MembershipKindList { + // Collect direct owner users + owners[owner.Name] = &owner + continue + } + + // For owner lists, we need to collect their members as owners + ownerMembers, err := collectMembersAsOwners(ctx, owner.Name, g, visited) + if err != nil { + return trace.Wrap(err) + } + //owners = append(owners, ownerMembers...) + for _, ownerMember := range ownerMembers { + owners[ownerMember.Name] = ownerMember + } + } + + return nil +} + +// collectMembersAsOwners is a helper to collect all nested members of an AccessList and return them cast as Owners. +func collectMembersAsOwners(ctx context.Context, accessListName string, g AccessListAndMembersGetter, visited map[string]struct{}) ([]*accesslist.Owner, error) { + owners := make([]*accesslist.Owner, 0) + if _, ok := visited[accessListName]; ok { + return owners, nil + } + visited[accessListName] = struct{}{} + + listMembers, err := GetMembersFor(ctx, accessListName, g) + if err != nil { + return nil, trace.Wrap(err) + } + + for _, member := range listMembers { + owners = append(owners, &accesslist.Owner{ + Name: member.Spec.Name, + Description: member.Metadata.Description, + IneligibleStatus: "", + MembershipKind: accesslist.MembershipKindUser, + }) + } + + return owners, nil +} + +// GetOwnersFor returns a flattened list of Owners for an Access List, including inherited Owners. +// +// Returned Owners are not validated for expiration or other requirements – use IsAccessListOwner +// to validate an Owner's ownership status. +func GetOwnersFor(ctx context.Context, accessList *accesslist.AccessList, g AccessListAndMembersGetter) ([]*accesslist.Owner, error) { + ownersMap := make(map[string]*accesslist.Owner) + if err := collectOwners(ctx, accessList, g, ownersMap, make(map[string]struct{})); err != nil { + return nil, trace.Wrap(err) + } + owners := make([]*accesslist.Owner, 0, len(ownersMap)) + for _, owner := range ownersMap { + owners = append(owners, owner) + } + return owners, nil +} + +// ValidateAccessListMember validates a new or existing AccessListMember for an Access List. +func ValidateAccessListMember( + ctx context.Context, + parentList *accesslist.AccessList, + member *accesslist.AccessListMember, + g AccessListAndMembersGetter, +) error { + if member.Spec.MembershipKind != accesslist.MembershipKindList { + return nil + } + return validateAccessListMemberOrOwner(ctx, parentList, member.Spec.Name, RelationshipKindMember, g) +} + +// ValidateAccessListOwner validates a new or existing AccessListOwner for an Access List. +func ValidateAccessListOwner( + ctx context.Context, + parentList *accesslist.AccessList, + owner *accesslist.Owner, + g AccessListAndMembersGetter, +) error { + if owner.MembershipKind != accesslist.MembershipKindList { + return nil + } + return validateAccessListMemberOrOwner(ctx, parentList, owner.Name, RelationshipKindOwner, g) +} + +func validateAccessListMemberOrOwner( + ctx context.Context, + parentList *accesslist.AccessList, + memberOrOwnerName string, + kind RelationshipKind, + g AccessListAndMembersGetter, +) error { + // Ensure member or owner list exists + memberOrOwnerList, err := g.GetAccessList(ctx, memberOrOwnerName) + if err != nil { + return trace.Wrap(err) + } + + // Validate addition + if err := validateAddition(ctx, parentList, memberOrOwnerList, kind, g); err != nil { + return trace.Wrap(err) + } + return nil +} + +func validateAddition( + ctx context.Context, + parentList *accesslist.AccessList, + childList *accesslist.AccessList, + kind RelationshipKind, + g AccessListAndMembersGetter, +) error { + kindStr := "a Member" + if kind == RelationshipKindOwner { + kindStr = "an Owner" + } + + // Cycle detection + reachable, err := isReachable(ctx, childList, parentList, make(map[string]struct{}), g) + if err != nil { + return trace.Wrap(err) + } + if reachable { + return trace.BadParameter( + "Access List '%s' can't be added as %s of '%s' because '%s' is already included as a Member or Owner in '%s'", + childList.Spec.Title, kindStr, parentList.Spec.Title, parentList.Spec.Title, childList.Spec.Title) + } + + // Max depth check + exceeds, err := exceedsMaxDepth(ctx, parentList, childList, kind, g) + if err != nil { + return trace.Wrap(err) + } + if exceeds { + return trace.BadParameter( + "Access List '%s' can't be added as %s of '%s' because it would exceed the maximum nesting depth of %d", + childList.Spec.Title, kindStr, parentList.Spec.Title, accesslist.MaxAllowedDepth) + } + + return nil +} + +func isReachable( + ctx context.Context, + currentList *accesslist.AccessList, + targetList *accesslist.AccessList, + visited map[string]struct{}, + g AccessListAndMembersGetter, +) (bool, error) { + if currentList.GetName() == targetList.GetName() { + return true, nil + } + if _, ok := visited[currentList.GetName()]; ok { + return false, nil + } + visited[currentList.GetName()] = struct{}{} + + // Traverse member lists + listMembers, err := fetchMembers(ctx, currentList.GetName(), g) + if err != nil { + return false, trace.Wrap(err) + } + for _, member := range listMembers { + if member.Spec.MembershipKind == accesslist.MembershipKindList { + childList, err := g.GetAccessList(ctx, member.Spec.Name) + if err != nil { + return false, trace.Wrap(err) + } + reachable, err := isReachable(ctx, childList, targetList, visited, g) + if err != nil { + return false, trace.Wrap(err) + } + if reachable { + return true, nil + } + } + } + + // Traverse owner lists + for _, owner := range currentList.Spec.Owners { + if owner.MembershipKind == accesslist.MembershipKindList { + ownerList, err := g.GetAccessList(ctx, owner.Name) + if err != nil { + return false, trace.Wrap(err) + } + reachable, err := isReachable(ctx, ownerList, targetList, visited, g) + if err != nil { + return false, trace.Wrap(err) + } + if reachable { + return true, nil + } + } + } + + return false, nil +} + +func exceedsMaxDepth( + ctx context.Context, + parentList *accesslist.AccessList, + childList *accesslist.AccessList, + kind RelationshipKind, + g AccessListAndMembersGetter, +) (bool, error) { + switch kind { + case RelationshipKindOwner: + // For Owners, only consider the depth downwards from the child node + depthDownwards, err := maxDepthDownwards(ctx, childList.GetName(), make(map[string]struct{}), g) + if err != nil { + return false, trace.Wrap(err) + } + return depthDownwards > accesslist.MaxAllowedDepth, nil + default: + // For Members, consider the depth upwards from the parent node, downwards from the child node, and the edge between them + depthUpwards, err := maxDepthUpwards(ctx, parentList, make(map[string]struct{}), g) + if err != nil { + return false, trace.Wrap(err) + } + depthDownwards, err := maxDepthDownwards(ctx, childList.GetName(), make(map[string]struct{}), g) + if err != nil { + return false, trace.Wrap(err) + } + totalDepth := depthUpwards + depthDownwards + 1 // +1 for the edge between parent and child + return totalDepth > accesslist.MaxAllowedDepth, nil + } +} + +func maxDepthDownwards( + ctx context.Context, + currentListName string, + seen map[string]struct{}, + g AccessListAndMembersGetter, +) (int, error) { + if _, ok := seen[currentListName]; ok { + return 0, nil + } + seen[currentListName] = struct{}{} + + maxDepth := 0 + + listMembers, err := fetchMembers(ctx, currentListName, g) + if err != nil { + return 0, trace.Wrap(err) + } + for _, member := range listMembers { + if member.Spec.MembershipKind == accesslist.MembershipKindList { + childListName := member.Spec.Name + depth, err := maxDepthDownwards(ctx, childListName, seen, g) + if err != nil { + return 0, trace.Wrap(err) + } + depth += 1 // Edge to the child + if depth > maxDepth { + maxDepth = depth + } + } + } + + delete(seen, currentListName) + + return maxDepth, nil +} + +func maxDepthUpwards( + ctx context.Context, + currentList *accesslist.AccessList, + seen map[string]struct{}, + g AccessListAndMembersGetter, +) (int, error) { + if _, ok := seen[currentList.GetName()]; ok { + return 0, nil + } + seen[currentList.GetName()] = struct{}{} + + maxDepth := 0 + + // Traverse MemberOf relationships + for _, parentListName := range currentList.Status.MemberOf { + parentList, err := g.GetAccessList(ctx, parentListName) + if err != nil { + return 0, trace.Wrap(err) // Treat missing lists as depth 0 + } + depth, err := maxDepthUpwards(ctx, parentList, seen, g) + if err != nil { + return 0, trace.Wrap(err) + } + depth += 1 // Edge to the parent + if depth > maxDepth { + maxDepth = depth + } + } + + delete(seen, currentList.GetName()) + + return maxDepth, nil +} + +func IsAccessListOwner( + ctx context.Context, + user types.User, + accessList *accesslist.AccessList, + g AccessListAndMembersGetter, + lockGetter services.LockGetter, + clock clockwork.Clock, +) (MembershipOrOwnershipType, error) { + if lockGetter != nil { + locks, err := lockGetter.GetLocks(ctx, true, types.LockTarget{ + User: user.GetName(), + }) + if err != nil { + return MembershipOrOwnershipTypeNone, trace.Wrap(err) + } + if len(locks) > 0 { + return MembershipOrOwnershipTypeNone, trace.AccessDenied("User '%s' is currently locked", user.GetName()) + } + } + + var ownershipErr error + + for _, owner := range accessList.Spec.Owners { + // Is user an explicit owner? + if owner.MembershipKind != accesslist.MembershipKindList && owner.Name == user.GetName() { + if !UserMeetsRequirements(user, accessList.Spec.OwnershipRequires) { + // Avoid non-deterministic behavior in these checks. Rather than returning immediately, continue + // through all owners to make sure there isn't a valid match later on. + ownershipErr = trace.AccessDenied("User '%s' does not meet the ownership requirements for Access List '%s'", user.GetName(), accessList.Spec.Title) + continue + } + return MembershipOrOwnershipTypeExplicit, nil + } + // Is user an inherited owner through any potential owner AccessLists? + if owner.MembershipKind == accesslist.MembershipKindList { + ownerAccessList, err := g.GetAccessList(ctx, owner.Name) + if err != nil { + ownershipErr = trace.Wrap(err) + continue + } + // Since we already verified that the user is not locked, don't provide lockGetter here + membershipType, err := IsAccessListMember(ctx, user, ownerAccessList, g, nil, clock) + if err != nil { + ownershipErr = trace.Wrap(err) + continue + } + if membershipType != MembershipOrOwnershipTypeNone { + if !UserMeetsRequirements(user, accessList.Spec.OwnershipRequires) { + ownershipErr = trace.AccessDenied("User '%s' does not meet the ownership requirements for Access List '%s'", user.GetName(), accessList.Spec.Title) + continue + } + return MembershipOrOwnershipTypeInherited, nil + } + } + } + + return MembershipOrOwnershipTypeNone, trace.Wrap(ownershipErr) +} + +func IsAccessListMember( + ctx context.Context, + user types.User, + accessList *accesslist.AccessList, + g AccessListAndMembersGetter, + lockGetter services.LockGetter, + clock clockwork.Clock, +) (MembershipOrOwnershipType, error) { + if lockGetter != nil { + locks, err := lockGetter.GetLocks(ctx, true, types.LockTarget{ + User: user.GetName(), + }) + if err != nil { + return MembershipOrOwnershipTypeNone, trace.Wrap(err) + } + if len(locks) > 0 { + return MembershipOrOwnershipTypeNone, trace.AccessDenied("User '%s' is currently locked", user.GetName()) + } + } + + members, err := fetchMembers(ctx, accessList.GetName(), g) + if err != nil { + return MembershipOrOwnershipTypeNone, trace.Wrap(err) + } + + var membershipErr error + + for _, member := range members { + // Is user an explicit member? + if member.Spec.MembershipKind != accesslist.MembershipKindList && member.GetName() == user.GetName() { + if !UserMeetsRequirements(user, accessList.Spec.MembershipRequires) { + // Avoid non-deterministic behavior in these checks. Rather than returning immediately, continue + // through all members to make sure there isn't a valid match later on. + membershipErr = trace.AccessDenied("User '%s' does not meet the membership requirements for Access List '%s'", user.GetName(), accessList.Spec.Title) + continue + } + if !member.Spec.Expires.IsZero() && !clock.Now().Before(member.Spec.Expires) { + membershipErr = trace.AccessDenied("User '%s's membership in Access List '%s' has expired", user.GetName(), accessList.Spec.Title) + continue + } + return MembershipOrOwnershipTypeExplicit, nil + } + // Is user an inherited member through any potential member AccessLists? + if member.Spec.MembershipKind == accesslist.MembershipKindList { + memberAccessList, err := g.GetAccessList(ctx, member.GetName()) + if err != nil { + membershipErr = trace.Wrap(err) + continue + } + // Since we already verified that the user is not locked, don't provide lockGetter here + membershipType, err := IsAccessListMember(ctx, user, memberAccessList, g, nil, clock) + if err != nil { + membershipErr = trace.Wrap(err) + continue + } + if membershipType != MembershipOrOwnershipTypeNone { + if !UserMeetsRequirements(user, accessList.Spec.MembershipRequires) { + membershipErr = trace.AccessDenied("User '%s' does not meet the membership requirements for Access List '%s'", user.GetName(), accessList.Spec.Title) + continue + } + if !member.Spec.Expires.IsZero() && !clock.Now().Before(member.Spec.Expires) { + membershipErr = trace.AccessDenied("User '%s's membership in Access List '%s' has expired", user.GetName(), accessList.Spec.Title) + continue + } + return MembershipOrOwnershipTypeInherited, nil + } + } + } + + return MembershipOrOwnershipTypeNone, trace.Wrap(membershipErr) +} + +// UserMeetsRequirements is a helper which will return whether the User meets the AccessList Ownership/MembershipRequires. +func UserMeetsRequirements(identity types.User, requires accesslist.Requires) bool { + // Assemble the user's roles for easy look up. + userRolesMap := map[string]struct{}{} + for _, role := range identity.GetRoles() { + userRolesMap[role] = struct{}{} + } + + // Check that the user meets the role requirements. + for _, role := range requires.Roles { + if _, ok := userRolesMap[role]; !ok { + return false + } + } + + // Assemble traits for easy lookup. + userTraitsMap := map[string]map[string]struct{}{} + for k, values := range identity.GetTraits() { + if _, ok := userTraitsMap[k]; !ok { + userTraitsMap[k] = map[string]struct{}{} + } + + for _, v := range values { + userTraitsMap[k][v] = struct{}{} + } + } + + // Check that user meets trait requirements. + for k, values := range requires.Traits { + if _, ok := userTraitsMap[k]; !ok { + return false + } + + for _, v := range values { + if _, ok := userTraitsMap[k][v]; !ok { + return false + } + } + } + + // The user meets all requirements. + return true +} + +func getAncestorsFor(ctx context.Context, accessList *accesslist.AccessList, kind RelationshipKind, g AccessListAndMembersGetter) ([]*accesslist.AccessList, error) { + ancestorsMap := make(map[string]*accesslist.AccessList) + if err := collectAncestors(ctx, accessList, kind, g, make(map[string]struct{}), ancestorsMap); err != nil { + return nil, trace.Wrap(err) + } + ancestors := make([]*accesslist.AccessList, 0, len(ancestorsMap)) + for _, al := range ancestorsMap { + ancestors = append(ancestors, al) + } + return ancestors, nil +} + +func collectAncestors(ctx context.Context, accessList *accesslist.AccessList, kind RelationshipKind, g AccessListAndMembersGetter, visited map[string]struct{}, ancestors map[string]*accesslist.AccessList) error { + if _, ok := visited[accessList.GetName()]; ok { + return nil + } + visited[accessList.GetName()] = struct{}{} + + switch kind { + case RelationshipKindOwner: + // Add parents where this list is an owner to ancestors + for _, ownerParent := range accessList.Status.OwnerOf { + ownerParentAcl, err := g.GetAccessList(ctx, ownerParent) + if err != nil { + return trace.Wrap(err) + } + ancestors[ownerParent] = ownerParentAcl + } + // Recursively traverse parents where this list is a member + for _, memberParent := range accessList.Status.MemberOf { + memberParentAcl, err := g.GetAccessList(ctx, memberParent) + if err != nil { + return trace.Wrap(err) + } + if err := collectAncestors(ctx, memberParentAcl, kind, g, visited, ancestors); err != nil { + return trace.Wrap(err) + } + } + default: + // Only collect parents where this list is a member + for _, memberParent := range accessList.Status.MemberOf { + memberParentAcl, err := g.GetAccessList(ctx, memberParent) + if err != nil { + return trace.Wrap(err) + } + ancestors[memberParent] = memberParentAcl + if err := collectAncestors(ctx, memberParentAcl, kind, g, visited, ancestors); err != nil { + return trace.Wrap(err) + } + } + } + + return nil +} + +// GetInheritedGrants returns the combined Grants for an Access List's members, inherited from any ancestor lists. +func GetInheritedGrants(ctx context.Context, accessList *accesslist.AccessList, g AccessListAndMembersGetter) (*accesslist.Grants, error) { + grants := accesslist.Grants{ + Traits: trait.Traits{}, + } + + collectedRoles := make(map[string]struct{}) + collectedTraits := make(map[string]map[string]struct{}) + + addGrants := func(grantRoles []string, grantTraits trait.Traits) { + for _, role := range grantRoles { + if _, exists := collectedRoles[role]; !exists { + grants.Roles = append(grants.Roles, role) + collectedRoles[role] = struct{}{} + } + } + for traitKey, traitValues := range grantTraits { + if _, exists := collectedTraits[traitKey]; !exists { + collectedTraits[traitKey] = make(map[string]struct{}) + } + for _, traitValue := range traitValues { + if _, exists := collectedTraits[traitKey][traitValue]; !exists { + grants.Traits[traitKey] = append(grants.Traits[traitKey], traitValue) + collectedTraits[traitKey][traitValue] = struct{}{} + } + } + } + } + + // Get ancestors via member relationship + ancestorLists, err := getAncestorsFor(ctx, accessList, RelationshipKindMember, g) + if err != nil { + return nil, trace.Wrap(err) + } + for _, ancestor := range ancestorLists { + memberGrants := ancestor.GetGrants() + addGrants(memberGrants.Roles, memberGrants.Traits) + } + + // Get ancestors via owner relationship + ancestorOwnerLists, err := getAncestorsFor(ctx, accessList, RelationshipKindOwner, g) + if err != nil { + return nil, trace.Wrap(err) + } + for _, ancestorOwner := range ancestorOwnerLists { + ownerGrants := ancestorOwner.GetOwnerGrants() + addGrants(ownerGrants.Roles, ownerGrants.Traits) + } + + slices.Sort(grants.Roles) + grants.Roles = slices.Compact(grants.Roles) + + for k, v := range grants.Traits { + slices.Sort(v) + grants.Traits[k] = slices.Compact(v) + } + + return &grants, nil +} diff --git a/lib/accesslists/hierarchy_test.go b/lib/accesslists/hierarchy_test.go new file mode 100644 index 0000000000000..43d295fb34b6d --- /dev/null +++ b/lib/accesslists/hierarchy_test.go @@ -0,0 +1,666 @@ +/* + * Teleport + * Copyright (C) 2024 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 accesslists + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/gravitational/trace" + "github.com/jonboulle/clockwork" + "github.com/stretchr/testify/require" + + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/api/types/accesslist" + "github.com/gravitational/teleport/api/types/header" +) + +// Mock implementation of AccessListAndMembersGetter. +type mockAccessListAndMembersGetter struct { + accessLists map[string]*accesslist.AccessList + members map[string][]*accesslist.AccessListMember +} + +func (m *mockAccessListAndMembersGetter) GetAccessList(ctx context.Context, accessListName string) (*accesslist.AccessList, error) { + accessList, exists := m.accessLists[accessListName] + if !exists { + return nil, trace.NotFound("access list %v not found", accessListName) + } + return accessList, nil +} + +func (m *mockAccessListAndMembersGetter) ListAccessListMembers(ctx context.Context, accessListName string, pageSize int, pageToken string) ([]*accesslist.AccessListMember, string, error) { + members, exists := m.members[accessListName] + if !exists { + return nil, "", nil + } + return members, "", nil +} + +type mockLocksGetter struct { + targets map[string][]types.Lock +} + +func (m *mockLocksGetter) GetLock(ctx context.Context, name string) (types.Lock, error) { + panic("not implemented") +} + +func (m *mockLocksGetter) GetLocks(ctx context.Context, inForceOnly bool, targets ...types.LockTarget) ([]types.Lock, error) { + var locks []types.Lock + for _, target := range targets { + locks = append(locks, m.targets[target.User]...) + } + return locks, nil +} + +const ( + ownerUser = "ownerUser" + ownerUser2 = "ownerUser2" + member1 = "member1" + member2 = "member2" +) + +func TestAccessListHierarchyDepthCheck(t *testing.T) { + clock := clockwork.NewFakeClock() + ctx := context.Background() + + numAcls := accesslist.MaxAllowedDepth + 2 // Extra 2 to test exceeding the max depth + + acls := make([]*accesslist.AccessList, numAcls) + for i := 0; i < numAcls; i++ { + acls[i] = newAccessList(t, fmt.Sprintf("acl%d", i+1), clock) + } + + accessListAndMembersGetter := &mockAccessListAndMembersGetter{ + members: make(map[string][]*accesslist.AccessListMember), + accessLists: make(map[string]*accesslist.AccessList), + } + + // Create members up to MaxAllowedDepth + for i := 0; i < accesslist.MaxAllowedDepth; i++ { + member := newAccessListMember(t, acls[i].GetName(), acls[i+1].GetName(), accesslist.MembershipKindList, clock) + acls[i+1].Status.MemberOf = append(acls[i+1].Status.MemberOf, acls[i].GetName()) + accessListAndMembersGetter.members[acls[i].GetName()] = []*accesslist.AccessListMember{member} + accessListAndMembersGetter.accessLists[acls[i].GetName()] = acls[i] + } + // Set remaining Access Lists' members to empty slices + for i := accesslist.MaxAllowedDepth; i < numAcls; i++ { + accessListAndMembersGetter.members[acls[i].GetName()] = []*accesslist.AccessListMember{} + accessListAndMembersGetter.accessLists[acls[i].GetName()] = acls[i] + } + + // Should be valid with existing member < MaxAllowedDepth + err := ValidateAccessListMember(ctx, acls[accesslist.MaxAllowedDepth-1], accessListAndMembersGetter.members[acls[accesslist.MaxAllowedDepth-1].GetName()][0], accessListAndMembersGetter) + require.NoError(t, err) + + // Now, attempt to add a member that increases the depth beyond MaxAllowedDepth + extraMember := newAccessListMember( + t, + acls[accesslist.MaxAllowedDepth].GetName(), + acls[accesslist.MaxAllowedDepth+1].GetName(), + accesslist.MembershipKindList, + clock, + ) + + // Validate adding this member should fail due to exceeding max depth + err = ValidateAccessListMember(ctx, acls[accesslist.MaxAllowedDepth], extraMember, accessListAndMembersGetter) + require.Error(t, err) + require.ErrorIs(t, err, trace.BadParameter("Access List '%s' can't be added as a Member of '%s' because it would exceed the maximum nesting depth of %d", acls[accesslist.MaxAllowedDepth+1].Spec.Title, acls[accesslist.MaxAllowedDepth].Spec.Title, accesslist.MaxAllowedDepth)) +} + +func TestAccessListValidateWithMembers(t *testing.T) { + clock := clockwork.NewFakeClock() + ctx := context.Background() + + // We're creating a hierarchy with a depth of 10, and then trying to add it as a Member of a 'root' Access List. This should fail. + rootAcl := newAccessList(t, "root", clock) + nestedAcls := make([]*accesslist.AccessList, 0, accesslist.MaxAllowedDepth) + for i := 0; i < accesslist.MaxAllowedDepth+1; i++ { + acl := newAccessList(t, fmt.Sprintf("acl-%d", i), clock) + nestedAcls = append(nestedAcls, acl) + } + rootAclMember := newAccessListMember(t, rootAcl.GetName(), nestedAcls[0].GetName(), accesslist.MembershipKindList, clock) + members := make([]*accesslist.AccessListMember, 0, accesslist.MaxAllowedDepth-1) + for i := 0; i < accesslist.MaxAllowedDepth; i++ { + member := newAccessListMember(t, nestedAcls[i].GetName(), nestedAcls[i+1].GetName(), accesslist.MembershipKindList, clock) + nestedAcls[i+1].Status.MemberOf = append(nestedAcls[i+1].Status.MemberOf, nestedAcls[i].GetName()) + members = append(members, member) + } + + accessListAndMembersGetter := &mockAccessListAndMembersGetter{ + members: map[string][]*accesslist.AccessListMember{ + rootAcl.GetName(): {}, + }, + accessLists: map[string]*accesslist.AccessList{ + rootAcl.GetName(): rootAcl, + }, + } + for i := 0; i < accesslist.MaxAllowedDepth+1; i++ { + if i < accesslist.MaxAllowedDepth { + accessListAndMembersGetter.members[nestedAcls[i].GetName()] = []*accesslist.AccessListMember{members[i]} + } + accessListAndMembersGetter.accessLists[nestedAcls[i].GetName()] = nestedAcls[i] + } + + // Should validate successfully, as acl-0 -> acl-10 is a valid hierarchy of depth 10. + err := ValidateAccessListWithMembers(ctx, rootAcl, []*accesslist.AccessListMember{}, accessListAndMembersGetter) + require.NoError(t, err) + err = ValidateAccessListWithMembers(ctx, nestedAcls[0], []*accesslist.AccessListMember{accessListAndMembersGetter.members[nestedAcls[0].GetName()][0]}, accessListAndMembersGetter) + require.NoError(t, err) + + // Calling `ValidateAccessListWithMembers`, with `rootAclm1`, should fail, as it would exceed the maximum nesting depth. + err = ValidateAccessListWithMembers(ctx, rootAcl, []*accesslist.AccessListMember{rootAclMember}, accessListAndMembersGetter) + require.Error(t, err) + require.ErrorIs(t, err, trace.BadParameter("Access List '%s' can't be added as a Member of '%s' because it would exceed the maximum nesting depth of %d", nestedAcls[0].Spec.Title, rootAcl.Spec.Title, accesslist.MaxAllowedDepth)) + + const Length = accesslist.MaxAllowedDepth/2 + 1 + + // Next, we're creating two separate hierarchies, each with a depth of `MaxAllowedDepth/2`. When testing the validation, we'll try to connect the two hierarchies, which should fail. + nestedAcls1 := make([]*accesslist.AccessList, 0, Length) + for i := 0; i <= Length; i++ { + acl := newAccessList(t, fmt.Sprintf("acl1-%d", i), clock) + nestedAcls1 = append(nestedAcls1, acl) + } + + // Create the second hierarchy. + nestedAcls2 := make([]*accesslist.AccessList, 0, Length) + for i := 0; i <= Length; i++ { + acl := newAccessList(t, fmt.Sprintf("acl2-%d", i), clock) + nestedAcls2 = append(nestedAcls2, acl) + } + + accessListAndMembersGetter = &mockAccessListAndMembersGetter{ + members: map[string][]*accesslist.AccessListMember{}, + accessLists: map[string]*accesslist.AccessList{}, + } + + // Create the members for the first hierarchy. + for i := 0; i < Length; i++ { + member := newAccessListMember(t, nestedAcls1[i].GetName(), nestedAcls1[i+1].GetName(), accesslist.MembershipKindList, clock) + nestedAcls1[i+1].Status.MemberOf = append(nestedAcls1[i+1].Status.MemberOf, nestedAcls1[i].GetName()) + accessListAndMembersGetter.members[nestedAcls1[i].GetName()] = []*accesslist.AccessListMember{member} + accessListAndMembersGetter.accessLists[nestedAcls1[i].GetName()] = nestedAcls1[i] + } + + // Create the members for the second hierarchy. + for i := 0; i < Length; i++ { + member := newAccessListMember(t, nestedAcls2[i].GetName(), nestedAcls2[i+1].GetName(), accesslist.MembershipKindList, clock) + nestedAcls2[i+1].Status.MemberOf = append(nestedAcls2[i+1].Status.MemberOf, nestedAcls2[i].GetName()) + accessListAndMembersGetter.members[nestedAcls2[i].GetName()] = []*accesslist.AccessListMember{member} + accessListAndMembersGetter.accessLists[nestedAcls2[i].GetName()] = nestedAcls2[i] + } + + // For the first hierarchy + nestedAcls1Last := nestedAcls1[len(nestedAcls1)-1] + accessListAndMembersGetter.accessLists[nestedAcls1Last.GetName()] = nestedAcls1Last + + // For the second hierarchy + nestedAcls2Last := nestedAcls2[len(nestedAcls2)-1] + accessListAndMembersGetter.accessLists[nestedAcls2Last.GetName()] = nestedAcls2Last + + // Should validate successfully when adding another list, as both hierarchies are valid. + err = ValidateAccessListWithMembers(ctx, nestedAcls1Last, []*accesslist.AccessListMember{newAccessListMember(t, nestedAcls1Last.GetName(), nestedAcls2Last.GetName(), accesslist.MembershipKindList, clock)}, accessListAndMembersGetter) + require.NoError(t, err) + err = ValidateAccessListWithMembers(ctx, nestedAcls2Last, []*accesslist.AccessListMember{newAccessListMember(t, nestedAcls2Last.GetName(), nestedAcls1Last.GetName(), accesslist.MembershipKindList, clock)}, accessListAndMembersGetter) + require.NoError(t, err) + + // Now, we'll try to connect the two hierarchies, which should fail. + err = ValidateAccessListWithMembers(ctx, nestedAcls1Last, []*accesslist.AccessListMember{newAccessListMember(t, nestedAcls1Last.GetName(), nestedAcls2[0].GetName(), accesslist.MembershipKindList, clock)}, accessListAndMembersGetter) + require.Error(t, err) + require.ErrorIs(t, err, trace.BadParameter("Access List '%s' can't be added as a Member of '%s' because it would exceed the maximum nesting depth of %d", nestedAcls2[0].Spec.Title, nestedAcls1[len(nestedAcls1)-1].Spec.Title, accesslist.MaxAllowedDepth)) +} + +func TestAccessListHierarchyCircularRefsCheck(t *testing.T) { + clock := clockwork.NewFakeClock() + ctx := context.Background() + + acl1 := newAccessList(t, "1", clock) + acl2 := newAccessList(t, "2", clock) + acl3 := newAccessList(t, "3", clock) + + // acl1 -> acl2 -> acl3 + acl1m1 := newAccessListMember(t, acl1.GetName(), acl2.GetName(), accesslist.MembershipKindList, clock) + acl2.Status.MemberOf = append(acl2.Status.MemberOf, acl1.GetName()) + acl2m1 := newAccessListMember(t, acl2.GetName(), acl3.GetName(), accesslist.MembershipKindList, clock) + acl3.Status.MemberOf = append(acl3.Status.MemberOf, acl2.GetName()) + + // acl3 -> acl1 + acl3m1 := newAccessListMember(t, acl3.GetName(), acl1.GetName(), accesslist.MembershipKindList, clock) + + accessListAndMembersGetter := &mockAccessListAndMembersGetter{ + members: map[string][]*accesslist.AccessListMember{ + acl1.GetName(): {acl1m1}, + acl2.GetName(): {acl2m1}, + acl3.GetName(): {}, + }, + accessLists: map[string]*accesslist.AccessList{ + acl1.GetName(): acl1, + acl2.GetName(): acl2, + acl3.GetName(): acl3, + }, + } + + // Circular references should not be allowed. + err := ValidateAccessListMember(ctx, acl3, acl3m1, accessListAndMembersGetter) + //err = hierarchy.ValidateAccessListMember(acl3.GetName(), acl3m1) + require.Error(t, err) + require.ErrorIs(t, err, trace.BadParameter("Access List '%s' can't be added as a Member of '%s' because '%s' is already included as a Member or Owner in '%s'", acl1.Spec.Title, acl3.Spec.Title, acl3.Spec.Title, acl1.Spec.Title)) + + // By removing acl3 as a member of acl2, the relationship should be valid. + accessListAndMembersGetter.members[acl2.GetName()] = []*accesslist.AccessListMember{} + accessListAndMembersGetter.accessLists[acl3.GetName()].Status.MemberOf = []string{} + err = ValidateAccessListMember(ctx, acl3, acl3m1, accessListAndMembersGetter) + require.NoError(t, err) + + // Circular references with Ownership should also be disallowed. + acl4 := newAccessList(t, "4", clock) + acl5 := newAccessList(t, "5", clock) + + // acl4 includes acl5 as a Member + acl4m1 := newAccessListMember(t, acl4.GetName(), acl5.GetName(), accesslist.MembershipKindList, clock) + acl5.Status.MemberOf = append(acl5.Status.MemberOf, acl4.GetName()) + + // acl5 includes acl4 as an Owner. + acl5.Spec.Owners = append(acl5.Spec.Owners, accesslist.Owner{ + Name: acl4.GetName(), + Description: "asdf", + MembershipKind: accesslist.MembershipKindList, + }) + acl4.Status.OwnerOf = append(acl4.Status.OwnerOf, acl5.GetName()) + + accessListAndMembersGetter = &mockAccessListAndMembersGetter{ + members: map[string][]*accesslist.AccessListMember{ + acl4.GetName(): {acl4m1}, + acl5.GetName(): {}, + }, + accessLists: map[string]*accesslist.AccessList{ + acl4.GetName(): acl4, + acl5.GetName(): acl5, + }, + } + + err = ValidateAccessListWithMembers(ctx, acl5, []*accesslist.AccessListMember{acl4m1}, accessListAndMembersGetter) + require.Error(t, err) + require.ErrorIs(t, err, trace.BadParameter("Access List '%s' can't be added as an Owner of '%s' because '%s' is already included as a Member or Owner in '%s'", acl4.Spec.Title, acl5.Spec.Title, acl5.Spec.Title, acl4.Spec.Title)) +} + +func TestAccessListHierarchyIsOwner(t *testing.T) { + clock := clockwork.NewFakeClock() + ctx := context.Background() + + acl1 := newAccessList(t, "1", clock) + acl2 := newAccessList(t, "2", clock) + acl3 := newAccessList(t, "3", clock) + acl4 := newAccessList(t, "4", clock) + + // acl1 -> acl2 -> acl3 as members + acl1m1 := newAccessListMember(t, acl1.GetName(), acl2.GetName(), accesslist.MembershipKindList, clock) + acl2.Status.MemberOf = append(acl2.Status.MemberOf, acl1.GetName()) + acl1m2 := newAccessListMember(t, acl1.GetName(), member1, accesslist.MembershipKindUser, clock) + acl2m1 := newAccessListMember(t, acl2.GetName(), acl3.GetName(), accesslist.MembershipKindList, clock) + acl3.Status.MemberOf = append(acl3.Status.MemberOf, acl2.GetName()) + acl4m1 := newAccessListMember(t, acl4.GetName(), member2, accesslist.MembershipKindUser, clock) + + // acl4 -> acl1 as owner + acl4.Spec.Owners = append(acl4.Spec.Owners, accesslist.Owner{ + Name: acl1.GetName(), + Description: "asdf", + MembershipKind: accesslist.MembershipKindList, + }) + acl1.Status.OwnerOf = append(acl1.Status.OwnerOf, acl4.GetName()) + + accessListAndMembersGetter := &mockAccessListAndMembersGetter{ + members: map[string][]*accesslist.AccessListMember{ + acl1.GetName(): {acl1m1, acl1m2}, + acl2.GetName(): {acl2m1}, + acl3.GetName(): {}, + acl4.GetName(): {acl4m1}, + }, + accessLists: map[string]*accesslist.AccessList{ + acl1.GetName(): acl1, + acl2.GetName(): acl2, + acl3.GetName(): acl3, + acl4.GetName(): acl4, + }, + } + + // User which does not meet acl1's Membership requirements. + stubUserNoRequires, err := types.NewUser(member1) + require.NoError(t, err) + + ownershipType, err := IsAccessListOwner(ctx, stubUserNoRequires, acl4, accessListAndMembersGetter, nil, clock) + require.Error(t, err) + require.ErrorIs(t, err, trace.AccessDenied("User '%s' does not meet the membership requirements for Access List '%s'", member1, acl1.Spec.Title)) + // Should not have inherited ownership due to missing OwnershipRequires. + require.Equal(t, MembershipOrOwnershipTypeNone, ownershipType) + + // User which only meets acl1's Membership requirements. + stubUserMeetsMemberRequires, err := types.NewUser(member1) + require.NoError(t, err) + stubUserMeetsMemberRequires.SetTraits(map[string][]string{ + "mtrait1": {"mvalue1", "mvalue2"}, + "mtrait2": {"mvalue3", "mvalue4"}, + }) + stubUserMeetsMemberRequires.SetRoles([]string{"mrole1", "mrole2"}) + + ownershipType, err = IsAccessListOwner(ctx, stubUserMeetsMemberRequires, acl4, accessListAndMembersGetter, nil, clock) + require.Error(t, err) + require.ErrorIs(t, err, trace.AccessDenied("User '%s' does not meet the ownership requirements for Access List '%s'", member1, acl4.Spec.Title)) + require.Equal(t, MembershipOrOwnershipTypeNone, ownershipType) + + // User which meets acl1's Membership and acl1's Ownership requirements. + stubUserMeetsAllRequires, err := types.NewUser(member1) + require.NoError(t, err) + stubUserMeetsAllRequires.SetTraits(map[string][]string{ + "mtrait1": {"mvalue1", "mvalue2"}, + "mtrait2": {"mvalue3", "mvalue4"}, + "otrait1": {"ovalue1", "ovalue2"}, + "otrait2": {"ovalue3", "ovalue4"}, + }) + stubUserMeetsAllRequires.SetRoles([]string{"mrole1", "mrole2", "orole1", "orole2"}) + + ownershipType, err = IsAccessListOwner(ctx, stubUserMeetsAllRequires, acl4, accessListAndMembersGetter, nil, clock) + require.NoError(t, err) + // Should have inherited ownership from acl1's inclusion in acl4's Owners. + require.Equal(t, MembershipOrOwnershipTypeInherited, ownershipType) + + stubUserMeetsAllRequires.SetName(member2) + ownershipType, err = IsAccessListOwner(ctx, stubUserMeetsAllRequires, acl4, accessListAndMembersGetter, nil, clock) + require.NoError(t, err) + // Should not have ownership. + require.Equal(t, MembershipOrOwnershipTypeNone, ownershipType) +} + +func TestAccessListIsMember(t *testing.T) { + clock := clockwork.NewFakeClock() + ctx := context.Background() + + acl1 := newAccessList(t, "1", clock) + acl1m1 := newAccessListMember(t, acl1.GetName(), member1, accesslist.MembershipKindUser, clock) + + locksGetter := &mockLocksGetter{ + targets: map[string][]types.Lock{}, + } + accessListAndMembersGetter := &mockAccessListAndMembersGetter{ + members: map[string][]*accesslist.AccessListMember{ + acl1.GetName(): {acl1m1}, + }, + accessLists: map[string]*accesslist.AccessList{ + acl1.GetName(): acl1, + }, + } + + stubMember1, err := types.NewUser(member1) + require.NoError(t, err) + stubMember1.SetTraits(map[string][]string{ + "mtrait1": {"mvalue1", "mvalue2"}, + "mtrait2": {"mvalue3", "mvalue4"}, + }) + stubMember1.SetRoles([]string{"mrole1", "mrole2"}) + + membershipType, err := IsAccessListMember(ctx, stubMember1, acl1, accessListAndMembersGetter, locksGetter, clock) + require.NoError(t, err) + require.Equal(t, MembershipOrOwnershipTypeExplicit, membershipType) + + // When user is Locked, should not be considered a Member. + lock, err := types.NewLock("user-lock", types.LockSpecV2{ + Target: types.LockTarget{ + User: member1, + }, + }) + require.NoError(t, err) + locksGetter.targets[member1] = []types.Lock{lock} + + membershipType, err = IsAccessListMember(ctx, stubMember1, acl1, accessListAndMembersGetter, locksGetter, clock) + require.ErrorIs(t, err, trace.AccessDenied("User '%s' is currently locked", member1)) + require.Equal(t, MembershipOrOwnershipTypeNone, membershipType) +} + +func TestGetOwners(t *testing.T) { + ctx := context.Background() + clock := clockwork.NewFakeClock() + + // Create Access Lists + acl1 := newAccessList(t, "1", clock) + acl2 := newAccessList(t, "2", clock) + acl3 := newAccessList(t, "3", clock) + + // Set up owners + // acl1 is owned by user "ownerA" and access list acl2 + acl1.Spec.Owners = []accesslist.Owner{ + { + Name: "ownerA", + MembershipKind: accesslist.MembershipKindUser, + }, + { + Name: acl2.GetName(), + MembershipKind: accesslist.MembershipKindList, + }, + } + acl2.Status.OwnerOf = append(acl2.Status.OwnerOf, acl1.GetName()) + + // acl2 is owned by user "ownerB" and access list aclC + acl2.Spec.Owners = []accesslist.Owner{ + { + Name: "ownerB", + MembershipKind: accesslist.MembershipKindUser, + }, + { + Name: acl3.GetName(), + MembershipKind: accesslist.MembershipKindList, + }, + } + acl3.Status.OwnerOf = append(acl3.Status.OwnerOf, acl2.GetName()) + + // acl3 is owned by user "ownerC" + acl3.Spec.Owners = []accesslist.Owner{ + { + Name: "ownerC", + MembershipKind: accesslist.MembershipKindUser, + }, + } + + // Set up members for owner lists + // aclB has member "memberB" + acl2m1 := newAccessListMember(t, acl2.GetName(), "memberB", accesslist.MembershipKindUser, clock) + // aclC has member "memberC" + acl3m1 := newAccessListMember(t, acl3.GetName(), "memberC", accesslist.MembershipKindUser, clock) + + accessListAndMembersGetter := &mockAccessListAndMembersGetter{ + members: map[string][]*accesslist.AccessListMember{ + acl2.GetName(): {acl2m1}, + acl3.GetName(): {acl3m1}, + }, + } + + // Test GetOwners for acl1 + owners, err := GetOwnersFor(ctx, acl1, accessListAndMembersGetter) + require.NoError(t, err) + + // Expected owners: + // - Direct owner: "ownerA" + // - Inherited owners via acl2 (since acl2 is an owner of acl1): + // - Members of acl2: "memberB" + // Note: Owners of acl2 ("ownerB") and members/owners of acl3 are not inherited by acl1 + + expectedOwners := map[string]bool{ + "ownerA": true, // Direct owner of acl1 + "memberB": true, // Member of acl2 (owner list of acl1) + } + + actualOwners := make(map[string]bool) + for _, owner := range owners { + actualOwners[owner.Name] = true + } + + require.Equal(t, expectedOwners, actualOwners, "Owners do not match expected owners") + + // Test GetOwners for acl2 + owners, err = GetOwnersFor(ctx, acl2, accessListAndMembersGetter) + require.NoError(t, err) + + // Expected owners: + // - Direct owner: "ownerB" + // - Inherited owners via acl3 (since acl3 is an owner of acl2): + // - Members of acl3: "memberC" + + expectedOwners = map[string]bool{ + "ownerB": true, // Direct owner of acl2 + "memberC": true, // Member of acl3 (owner list of acl2) + } + + actualOwners = make(map[string]bool) + for _, owner := range owners { + actualOwners[owner.Name] = true + } + + require.Equal(t, expectedOwners, actualOwners, "Owners do not match expected owners") +} + +func TestGetInheritedGrants(t *testing.T) { + ctx := context.Background() + clock := clockwork.NewFakeClock() + + aclroot := newAccessList(t, "root", clock) + acl1 := newAccessList(t, "1", clock) + acl2 := newAccessList(t, "2", clock) + + // aclroot has a trait for owners - "root-owner-trait", and a role for owners - "root-owner-role" + aclroot.Spec.OwnerGrants = accesslist.Grants{ + Traits: map[string][]string{ + "root-owner-trait": {"root-owner-value"}, + }, + Roles: []string{"root-owner-role"}, + } + + // acl1 has a trait for members - "1-member-trait", and a role for members - "1-member-role" + acl1.Spec.Grants = accesslist.Grants{ + Traits: map[string][]string{ + "1-member-trait": {"1-member-value"}, + }, + Roles: []string{"1-member-role"}, + } + + // acl2 has no traits or roles + acl2.Spec.Grants = accesslist.Grants{} + + aclroot.SetOwners([]accesslist.Owner{ + { + Name: acl1.GetName(), + MembershipKind: accesslist.MembershipKindList, + }, + }) + acl1.Status.OwnerOf = append(acl1.Status.OwnerOf, aclroot.GetName()) + + accessListAndMembersGetter := &mockAccessListAndMembersGetter{ + accessLists: map[string]*accesslist.AccessList{ + aclroot.GetName(): aclroot, + acl1.GetName(): acl1, + acl2.GetName(): acl2, + }, + } + + // acl1 is an Owner of aclroot, and acl2 is a Member of acl1. + acl2.Status.MemberOf = append(acl2.Status.MemberOf, acl1.GetName()) + + // so, members of acl2 should inherit aclroot's owner grants, and acl1's member grants. + expectedGrants := &accesslist.Grants{ + Traits: map[string][]string{ + "1-member-trait": {"1-member-value"}, + "root-owner-trait": {"root-owner-value"}, + }, + Roles: []string{"1-member-role", "root-owner-role"}, + } + + grants, err := GetInheritedGrants(ctx, acl2, accessListAndMembersGetter) + require.NoError(t, err) + require.Equal(t, expectedGrants, grants) +} + +func newAccessList(t *testing.T, name string, clock clockwork.Clock) *accesslist.AccessList { + t.Helper() + + accessList, err := accesslist.NewAccessList( + header.Metadata{ + Name: name, + }, + accesslist.Spec{ + Title: name, + Description: "test access list", + Owners: []accesslist.Owner{ + {Name: ownerUser, Description: "owner user", MembershipKind: accesslist.MembershipKindUser}, + {Name: ownerUser2, Description: "owner user 2", MembershipKind: accesslist.MembershipKindUser}, + }, + Audit: accesslist.Audit{ + NextAuditDate: clock.Now().Add(time.Hour * 24 * 365), + Notifications: accesslist.Notifications{ + Start: 336 * time.Hour, // Two weeks. + }, + }, + MembershipRequires: accesslist.Requires{ + Roles: []string{"mrole1", "mrole2"}, + Traits: map[string][]string{ + "mtrait1": {"mvalue1", "mvalue2"}, + "mtrait2": {"mvalue3", "mvalue4"}, + }, + }, + OwnershipRequires: accesslist.Requires{ + Roles: []string{"orole1", "orole2"}, + Traits: map[string][]string{ + "otrait1": {"ovalue1", "ovalue2"}, + "otrait2": {"ovalue3", "ovalue4"}, + }, + }, + Grants: accesslist.Grants{ + Roles: []string{"grole1", "grole2"}, + Traits: map[string][]string{ + "gtrait1": {"gvalue1", "gvalue2"}, + "gtrait2": {"gvalue3", "gvalue4"}, + }, + }, + }, + ) + require.NoError(t, err) + + return accessList +} + +func newAccessListMember(t *testing.T, accessListName, memberName string, memberKind string, clock clockwork.Clock) *accesslist.AccessListMember { + t.Helper() + + member, err := accesslist.NewAccessListMember( + header.Metadata{ + Name: memberName, + }, + accesslist.AccessListMemberSpec{ + AccessList: accessListName, + Name: memberName, + Joined: clock.Now().UTC(), + Expires: clock.Now().UTC().Add(24 * time.Hour), + Reason: "because", + AddedBy: "maxim.dietz@goteleport.com", + MembershipKind: memberKind, + }, + ) + require.NoError(t, err) + + return member +}