feat: Allow including Access Lists as nested members and owners (#38738)

- Recursively check for accesslist membership

- Allow adding/removing/listing included access lists in acl commands

- Add a recursive test

- Use dynamic access lists structure from RFD

- Resolve proto changes

- Exclude 'list' members from Access List memberCount

- Calc Access List member count with members of type 'list' excluded,
  return seperately to front end

- Update examples/integrations

- Update crd docs

- Update tf docs

- Perform calculation of inherited roles/traits to AccessList service in
  order to utilize cache and minimize number of requests.

- Grant Okta integration RO for Access Lists

- Update AccessListMember-* events

- Include count for inherited grants

- Include MembershipKind of affected member(s)

- Emit inherited grants / members' MembershipKind for AccessListMember-*
  events

- Update notified owners for Access Requests

- Ensure dynamic owners are notified for Access Requests

- Ensure dynamic owners are notified via Slack integration

- Optionally pass an AbortSignal to `fetchAccessLists` in Web UI

- Replace usages of `services.IsAccessListOwner/IsAccessListMember` with
  equivelant funcs from `Hierarchy`

- Remove final references to AccessListMembershipChecker

- Don't allow ACL deletion when member/owner in other lists

- Guard Access List deletion behind membership/ownership checks for List

- Expose Hierarchy func to recursively get all members

- Tidy UserLoginStateGenerator logic involving ACL Membership/Ownership

Co-authored-by: Maxim Dietz <[email protected]>
This commit is contained in:
Alex McGrath
2024-11-01 22:07:36 +00:00
committed by GitHub
co-authored by Maxim Dietz
parent 2ad2d990f4
commit fa97b8f90d
38 changed files with 5783 additions and 4086 deletions
+39 -3
View File
@@ -23,6 +23,7 @@ import (
accesslistv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/accesslist/v1"
"github.com/gravitational/teleport/api/types/accesslist"
conv "github.com/gravitational/teleport/api/types/accesslist/convert/v1"
traitv1 "github.com/gravitational/teleport/api/types/trait/convert/v1"
)
// Client is an access list client that conforms to the following lib/services interfaces:
@@ -113,6 +114,21 @@ func (c *Client) GetAccessListsToReview(ctx context.Context) ([]*accesslist.Acce
return accessLists, nil
}
// GetInheritedGrants returns grants inherited by access list accessListID from parent access lists.
func (c *Client) GetInheritedGrants(ctx context.Context, accessListID string) (*accesslist.Grants, error) {
resp, err := c.grpcClient.GetInheritedGrants(ctx, &accesslistv1.GetInheritedGrantsRequest{
AccessListId: accessListID,
})
if err != nil {
return nil, trace.Wrap(err)
}
return &accesslist.Grants{
Roles: resp.Grants.Roles,
Traits: traitv1.FromProto(resp.Grants.Traits),
}, nil
}
// UpsertAccessList creates or updates an access list resource.
func (c *Client) UpsertAccessList(ctx context.Context, accessList *accesslist.AccessList) (*accesslist.AccessList, error) {
resp, err := c.grpcClient.UpsertAccessList(ctx, &accesslistv1.UpsertAccessListRequest{
@@ -151,15 +167,15 @@ func (c *Client) DeleteAllAccessLists(ctx context.Context) error {
}
// CountAccessListMembers will count all access list members.
func (c *Client) CountAccessListMembers(ctx context.Context, accessListName string) (uint32, error) {
func (c *Client) CountAccessListMembers(ctx context.Context, accessListName string) (users uint32, lists uint32, err error) {
resp, err := c.grpcClient.CountAccessListMembers(ctx, &accesslistv1.CountAccessListMembersRequest{
AccessListName: accessListName,
})
if err != nil {
return 0, trace.Wrap(err)
return 0, 0, trace.Wrap(err)
}
return resp.Count, nil
return resp.Count, resp.ListCount, nil
}
// ListAccessListMembers returns a paginated list of all access list members for an access list.
@@ -221,6 +237,26 @@ func (c *Client) GetAccessListMember(ctx context.Context, accessList string, mem
return member, trace.Wrap(err)
}
// GetAccessListOwners returns a list of all owners in an Access List, including those inherited from nested Access Lists.
//
// Returned Owners are not validated for ownership requirements – use `IsAccessListOwner` for validation.
func (c *Client) GetAccessListOwners(ctx context.Context, accessListName string) ([]*accesslist.Owner, error) {
resp, err := c.grpcClient.GetAccessListOwners(ctx, &accesslistv1.GetAccessListOwnersRequest{
AccessList: accessListName,
})
if err != nil {
return nil, trace.Wrap(err)
}
owners := make([]*accesslist.Owner, 0, len(resp.Owners))
for _, owner := range resp.Owners {
ownerProto := conv.FromOwnerProto(owner)
owners = append(owners, &ownerProto)
}
return owners, nil
}
// UpsertAccessListMember creates or updates an access list member resource.
func (c *Client) UpsertAccessListMember(ctx context.Context, member *accesslist.AccessListMember) (*accesslist.AccessListMember, error) {
resp, err := c.grpcClient.UpsertAccessListMember(ctx, &accesslistv1.UpsertAccessListMemberRequest{
File diff suppressed because it is too large Load Diff
@@ -46,6 +46,7 @@ const (
AccessListService_ListAccessListMembers_FullMethodName = "/teleport.accesslist.v1.AccessListService/ListAccessListMembers"
AccessListService_ListAllAccessListMembers_FullMethodName = "/teleport.accesslist.v1.AccessListService/ListAllAccessListMembers"
AccessListService_GetAccessListMember_FullMethodName = "/teleport.accesslist.v1.AccessListService/GetAccessListMember"
AccessListService_GetAccessListOwners_FullMethodName = "/teleport.accesslist.v1.AccessListService/GetAccessListOwners"
AccessListService_UpsertAccessListMember_FullMethodName = "/teleport.accesslist.v1.AccessListService/UpsertAccessListMember"
AccessListService_UpdateAccessListMember_FullMethodName = "/teleport.accesslist.v1.AccessListService/UpdateAccessListMember"
AccessListService_DeleteAccessListMember_FullMethodName = "/teleport.accesslist.v1.AccessListService/DeleteAccessListMember"
@@ -58,6 +59,7 @@ const (
AccessListService_DeleteAccessListReview_FullMethodName = "/teleport.accesslist.v1.AccessListService/DeleteAccessListReview"
AccessListService_AccessRequestPromote_FullMethodName = "/teleport.accesslist.v1.AccessListService/AccessRequestPromote"
AccessListService_GetSuggestedAccessLists_FullMethodName = "/teleport.accesslist.v1.AccessListService/GetSuggestedAccessLists"
AccessListService_GetInheritedGrants_FullMethodName = "/teleport.accesslist.v1.AccessListService/GetInheritedGrants"
)
// AccessListServiceClient is the client API for AccessListService service.
@@ -93,6 +95,9 @@ type AccessListServiceClient interface {
ListAllAccessListMembers(ctx context.Context, in *ListAllAccessListMembersRequest, opts ...grpc.CallOption) (*ListAllAccessListMembersResponse, error)
// GetAccessListMember returns the specified access list member resource.
GetAccessListMember(ctx context.Context, in *GetAccessListMemberRequest, opts ...grpc.CallOption) (*Member, error)
// GetAccessListOwners returns a list of all owners in an Access List,
// including those inherited from nested Access Lists.
GetAccessListOwners(ctx context.Context, in *GetAccessListOwnersRequest, opts ...grpc.CallOption) (*GetAccessListOwnersResponse, error)
// UpsertAccessListMember creates or updates an access list member resource.
UpsertAccessListMember(ctx context.Context, in *UpsertAccessListMemberRequest, opts ...grpc.CallOption) (*Member, error)
// UpdateAccessListMember conditionally updates an access list member resource.
@@ -125,6 +130,8 @@ type AccessListServiceClient interface {
// GetSuggestedAccessLists returns suggested access lists for an access
// request.
GetSuggestedAccessLists(ctx context.Context, in *GetSuggestedAccessListsRequest, opts ...grpc.CallOption) (*GetSuggestedAccessListsResponse, error)
// GetInheritedGrants returns the inherited grants for an access list.
GetInheritedGrants(ctx context.Context, in *GetInheritedGrantsRequest, opts ...grpc.CallOption) (*GetInheritedGrantsResponse, error)
}
type accessListServiceClient struct {
@@ -255,6 +262,16 @@ func (c *accessListServiceClient) GetAccessListMember(ctx context.Context, in *G
return out, nil
}
func (c *accessListServiceClient) GetAccessListOwners(ctx context.Context, in *GetAccessListOwnersRequest, opts ...grpc.CallOption) (*GetAccessListOwnersResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetAccessListOwnersResponse)
err := c.cc.Invoke(ctx, AccessListService_GetAccessListOwners_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *accessListServiceClient) UpsertAccessListMember(ctx context.Context, in *UpsertAccessListMemberRequest, opts ...grpc.CallOption) (*Member, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(Member)
@@ -375,6 +392,16 @@ func (c *accessListServiceClient) GetSuggestedAccessLists(ctx context.Context, i
return out, nil
}
func (c *accessListServiceClient) GetInheritedGrants(ctx context.Context, in *GetInheritedGrantsRequest, opts ...grpc.CallOption) (*GetInheritedGrantsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetInheritedGrantsResponse)
err := c.cc.Invoke(ctx, AccessListService_GetInheritedGrants_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// AccessListServiceServer is the server API for AccessListService service.
// All implementations must embed UnimplementedAccessListServiceServer
// for forward compatibility.
@@ -408,6 +435,9 @@ type AccessListServiceServer interface {
ListAllAccessListMembers(context.Context, *ListAllAccessListMembersRequest) (*ListAllAccessListMembersResponse, error)
// GetAccessListMember returns the specified access list member resource.
GetAccessListMember(context.Context, *GetAccessListMemberRequest) (*Member, error)
// GetAccessListOwners returns a list of all owners in an Access List,
// including those inherited from nested Access Lists.
GetAccessListOwners(context.Context, *GetAccessListOwnersRequest) (*GetAccessListOwnersResponse, error)
// UpsertAccessListMember creates or updates an access list member resource.
UpsertAccessListMember(context.Context, *UpsertAccessListMemberRequest) (*Member, error)
// UpdateAccessListMember conditionally updates an access list member resource.
@@ -440,6 +470,8 @@ type AccessListServiceServer interface {
// GetSuggestedAccessLists returns suggested access lists for an access
// request.
GetSuggestedAccessLists(context.Context, *GetSuggestedAccessListsRequest) (*GetSuggestedAccessListsResponse, error)
// GetInheritedGrants returns the inherited grants for an access list.
GetInheritedGrants(context.Context, *GetInheritedGrantsRequest) (*GetInheritedGrantsResponse, error)
mustEmbedUnimplementedAccessListServiceServer()
}
@@ -486,6 +518,9 @@ func (UnimplementedAccessListServiceServer) ListAllAccessListMembers(context.Con
func (UnimplementedAccessListServiceServer) GetAccessListMember(context.Context, *GetAccessListMemberRequest) (*Member, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetAccessListMember not implemented")
}
func (UnimplementedAccessListServiceServer) GetAccessListOwners(context.Context, *GetAccessListOwnersRequest) (*GetAccessListOwnersResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetAccessListOwners not implemented")
}
func (UnimplementedAccessListServiceServer) UpsertAccessListMember(context.Context, *UpsertAccessListMemberRequest) (*Member, error) {
return nil, status.Errorf(codes.Unimplemented, "method UpsertAccessListMember not implemented")
}
@@ -522,6 +557,9 @@ func (UnimplementedAccessListServiceServer) AccessRequestPromote(context.Context
func (UnimplementedAccessListServiceServer) GetSuggestedAccessLists(context.Context, *GetSuggestedAccessListsRequest) (*GetSuggestedAccessListsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetSuggestedAccessLists not implemented")
}
func (UnimplementedAccessListServiceServer) GetInheritedGrants(context.Context, *GetInheritedGrantsRequest) (*GetInheritedGrantsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetInheritedGrants not implemented")
}
func (UnimplementedAccessListServiceServer) mustEmbedUnimplementedAccessListServiceServer() {}
func (UnimplementedAccessListServiceServer) testEmbeddedByValue() {}
@@ -759,6 +797,24 @@ func _AccessListService_GetAccessListMember_Handler(srv interface{}, ctx context
return interceptor(ctx, in, info, handler)
}
func _AccessListService_GetAccessListOwners_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetAccessListOwnersRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AccessListServiceServer).GetAccessListOwners(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AccessListService_GetAccessListOwners_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AccessListServiceServer).GetAccessListOwners(ctx, req.(*GetAccessListOwnersRequest))
}
return interceptor(ctx, in, info, handler)
}
func _AccessListService_UpsertAccessListMember_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(UpsertAccessListMemberRequest)
if err := dec(in); err != nil {
@@ -975,6 +1031,24 @@ func _AccessListService_GetSuggestedAccessLists_Handler(srv interface{}, ctx con
return interceptor(ctx, in, info, handler)
}
func _AccessListService_GetInheritedGrants_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetInheritedGrantsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AccessListServiceServer).GetInheritedGrants(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AccessListService_GetInheritedGrants_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AccessListServiceServer).GetInheritedGrants(ctx, req.(*GetInheritedGrantsRequest))
}
return interceptor(ctx, in, info, handler)
}
// AccessListService_ServiceDesc is the grpc.ServiceDesc for AccessListService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
@@ -1030,6 +1104,10 @@ var AccessListService_ServiceDesc = grpc.ServiceDesc{
MethodName: "GetAccessListMember",
Handler: _AccessListService_GetAccessListMember_Handler,
},
{
MethodName: "GetAccessListOwners",
Handler: _AccessListService_GetAccessListOwners_Handler,
},
{
MethodName: "UpsertAccessListMember",
Handler: _AccessListService_UpsertAccessListMember_Handler,
@@ -1078,6 +1156,10 @@ var AccessListService_ServiceDesc = grpc.ServiceDesc{
MethodName: "GetSuggestedAccessLists",
Handler: _AccessListService_GetSuggestedAccessLists_Handler,
},
{
MethodName: "GetInheritedGrants",
Handler: _AccessListService_GetInheritedGrants_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "teleport/accesslist/v1/accesslist_service.proto",
File diff suppressed because it is too large Load Diff
@@ -53,6 +53,9 @@ service AccessListService {
rpc ListAllAccessListMembers(ListAllAccessListMembersRequest) returns (ListAllAccessListMembersResponse);
// GetAccessListMember returns the specified access list member resource.
rpc GetAccessListMember(GetAccessListMemberRequest) returns (Member);
// GetAccessListOwners returns a list of all owners in an Access List,
// including those inherited from nested Access Lists.
rpc GetAccessListOwners(GetAccessListOwnersRequest) returns (GetAccessListOwnersResponse);
// UpsertAccessListMember creates or updates an access list member resource.
rpc UpsertAccessListMember(UpsertAccessListMemberRequest) returns (Member);
// UpdateAccessListMember conditionally updates an access list member resource.
@@ -89,6 +92,9 @@ service AccessListService {
// GetSuggestedAccessLists returns suggested access lists for an access
// request.
rpc GetSuggestedAccessLists(GetSuggestedAccessListsRequest) returns (GetSuggestedAccessListsResponse);
// GetInheritedGrants returns the inherited grants for an access list.
rpc GetInheritedGrants(GetInheritedGrantsRequest) returns (GetInheritedGrantsResponse);
}
// GetAccessListsRequest is the request for getting all access lists.
@@ -117,6 +123,18 @@ message ListAccessListsResponse {
string next_token = 2;
}
// GetInheritedGrantsRequest is the request for getting inherited grants.
message GetInheritedGrantsRequest {
// access_list_id is the ID of the access list to retrieve.
string access_list_id = 1;
}
// GetInheritedGrantsResponse is the response for getting inherited grants.
message GetInheritedGrantsResponse {
// grants is the list of inherited member grants.
AccessListGrants grants = 1;
}
// GetAccessListRequest is the request for retrieving an access list.
message GetAccessListRequest {
// name is the name of the access list to retrieve.
@@ -166,6 +184,8 @@ message CountAccessListMembersRequest {
message CountAccessListMembersResponse {
// count is the number of access list members in the access list.
uint32 count = 1;
// list_count is the number of access list members of type list in the access list.
uint32 list_count = 2;
}
// ListAccessListMembersRequest is the request for getting paginated access list
@@ -237,6 +257,21 @@ message GetAccessListMemberRequest {
string member_name = 2;
}
// GetAccessListOwnersRequest is the request for getting a list of all owners
// in an Access List, including those inherited from nested Access Lists.
message GetAccessListOwnersRequest {
// access_list is the name of the access list.
string access_list = 1;
}
// GetAccessListOwnersResponse is the response for getting a list of all
// owners in an Access List, including those inherited from nested Access Lists.
message GetAccessListOwnersResponse {
// owners is the list of all owners in the Access List, including those
// inherited from nested Access Lists.
repeated accesslist.v1.AccessListOwner owners = 1;
}
// UpsertAccessListMemberRequest is the request for upserting an access list
// member.
message UpsertAccessListMemberRequest {
@@ -20,6 +20,7 @@ import "gogoproto/gogo.proto";
import "google/protobuf/struct.proto";
import "google/protobuf/timestamp.proto";
import "google/protobuf/wrappers.proto";
import "teleport/accesslist/v1/accesslist.proto";
import "teleport/legacy/types/types.proto";
import "teleport/legacy/types/wrappers/wrappers.proto";
@@ -280,6 +281,10 @@ message AccessListMember {
// MemberName is the name of the member.
string MemberName = 4 [(gogoproto.jsontag) = "member_name,omitempty"];
// MembershipKind describes the kind of membership, either
// `MEMBERSHIP_KIND_USER` or `MEMBERSHIP_KIND_LIST`.
teleport.accesslist.v1.MembershipKind membership_kind = 5 [(gogoproto.jsontag) = "membership_kind,omitempty"];
}
// AccessListReviewMembershipRequirementsChanged contains information for when membership requirements change as part of a review.
@@ -16,6 +16,8 @@ syntax = "proto3";
package teleport.usageevents.v1;
import "teleport/accesslist/v1/accesslist.proto";
option go_package = "github.com/gravitational/teleport/api/gen/proto/go/usageevents/v1;usageeventsv1";
// UIBannerClickEvent is a usage event sent by the UI when the upgrade
@@ -482,6 +484,14 @@ message AccessListMetadata {
string id = 1;
}
// AccessListMemberMetadata contains common metadata for Access List Member related events.
message AccessListMemberMetadata {
// name is the name of the member.
string name = 1;
// membership_kind is the type of membership in the parent access list.
teleport.accesslist.v1.MembershipKind membership_kind = 2;
}
// AccessListCreate is an event that is emitted when an access list is created.
message AccessListCreate {
AccessListMetadata metadata = 1;
@@ -500,16 +510,19 @@ message AccessListDelete {
// AccessListMemberCreate is an event that is emitted when a member is added to an access list.
message AccessListMemberCreate {
AccessListMetadata metadata = 1;
AccessListMemberMetadata member_metadata = 2;
}
// AccessListMemberCreate is an event that is emitted when a member is updated in an access list.
message AccessListMemberUpdate {
AccessListMetadata metadata = 1;
AccessListMemberMetadata member_metadata = 2;
}
// AccessListMemberDelete is an event that is emitted when a member is removed from an access list.
message AccessListMemberDelete {
AccessListMetadata metadata = 1;
AccessListMemberMetadata member_metadata = 2;
}
// AccessListGrantsToUser is an event that is emitted when access list permissions are granted to a user
@@ -518,8 +531,14 @@ message AccessListGrantsToUser {
// count_roles_granted is the number of roles granted to a user.
int32 count_roles_granted = 1;
// count_inherited_roles_granted is the number of roles granted to a user inherited from nested access lists.
int32 count_inherited_roles_granted = 3;
// count_traits_granted is the number of traits granted to a user.
int32 count_traits_granted = 2;
// count_inherited_traits_granted is the number of traits granted to a user inherited from nested access lists.
int32 count_inherited_traits_granted = 4;
}
// AccessListReviewCreate is an event that is emitted when an access list review is created.
@@ -47,6 +47,7 @@ func FromMemberProto(msg *accesslistv1.Member, opts ...MemberOption) (*accesslis
// Set it to empty as default.
// Must provide as options to set it with the provided value.
IneligibleStatus: "",
MembershipKind: msg.Spec.MembershipKind.String(),
})
if err != nil {
return nil, trace.Wrap(err)
@@ -79,6 +80,11 @@ func ToMemberProto(member *accesslist.AccessListMember) *accesslistv1.Member {
ineligibleStatus = accesslistv1.IneligibleStatus(enumVal)
}
var membershipKind accesslistv1.MembershipKind
if enumVal, ok := accesslistv1.MembershipKind_value[member.Spec.MembershipKind]; ok {
membershipKind = accesslistv1.MembershipKind(enumVal)
}
return &accesslistv1.Member{
Header: headerv1.ToResourceHeaderProto(member.ResourceHeader),
Spec: &accesslistv1.MemberSpec{
@@ -89,6 +95,7 @@ func ToMemberProto(member *accesslist.AccessListMember) *accesslistv1.Member {
Reason: member.Spec.Reason,
AddedBy: member.Spec.AddedBy,
IneligibleStatus: ineligibleStatus,
MembershipKind: membershipKind,
},
}
}
+1088 -1053
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+74 -4
View File
@@ -1710,6 +1710,12 @@ export interface AccessListMemberCreateEvent {
* @generated from protobuf field: prehog.v1alpha.AccessListMetadata metadata = 2;
*/
metadata?: AccessListMetadata;
/**
* member_kind is the type of membership of the created member in the parent access list.
*
* @generated from protobuf field: string member_kind = 3;
*/
memberKind: string;
}
/**
* AccessListMemberUpdate is an event that is emitted when a member is updated in an access list.
@@ -1727,6 +1733,12 @@ export interface AccessListMemberUpdateEvent {
* @generated from protobuf field: prehog.v1alpha.AccessListMetadata metadata = 2;
*/
metadata?: AccessListMetadata;
/**
* membership_kind is the type of membership of the updated member in the parent access list.
*
* @generated from protobuf field: string member_kind = 3;
*/
memberKind: string;
}
/**
* AccessListMemberDelete is an event that is emitted when a member is removed from an access list.
@@ -1744,6 +1756,12 @@ export interface AccessListMemberDeleteEvent {
* @generated from protobuf field: prehog.v1alpha.AccessListMetadata metadata = 2;
*/
metadata?: AccessListMetadata;
/**
* member_kind is the type of membership of the deleted user in the parent access list.
*
* @generated from protobuf field: string member_kind = 3;
*/
memberKind: string;
}
/**
* AccessListGrantsToUser is an event that is emitted when access list permissions are granted to a user
@@ -1770,6 +1788,18 @@ export interface AccessListGrantsToUserEvent {
* @generated from protobuf field: int32 count_traits_granted = 3;
*/
countTraitsGranted: number;
/**
* count_inherited_roles_granted is the number of roles granted to a user inherited from nested access lists.
*
* @generated from protobuf field: int32 count_inherited_roles_granted = 4;
*/
countInheritedRolesGranted: number;
/**
* count_inherited_traits_granted is the number of traits granted to a user inherited from nested access lists.
*
* @generated from protobuf field: int32 count_inherited_traits_granted = 5;
*/
countInheritedTraitsGranted: number;
}
/**
* AccessListReviewCreateEvent is an event that is emitted when an access list review is created.
@@ -7781,12 +7811,14 @@ class AccessListMemberCreateEvent$Type extends MessageType<AccessListMemberCreat
constructor() {
super("prehog.v1alpha.AccessListMemberCreateEvent", [
{ no: 1, name: "user_name", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
{ no: 2, name: "metadata", kind: "message", T: () => AccessListMetadata }
{ no: 2, name: "metadata", kind: "message", T: () => AccessListMetadata },
{ no: 3, name: "member_kind", kind: "scalar", T: 9 /*ScalarType.STRING*/ }
]);
}
create(value?: PartialMessage<AccessListMemberCreateEvent>): AccessListMemberCreateEvent {
const message = globalThis.Object.create((this.messagePrototype!));
message.userName = "";
message.memberKind = "";
if (value !== undefined)
reflectionMergePartial<AccessListMemberCreateEvent>(this, message, value);
return message;
@@ -7802,6 +7834,9 @@ class AccessListMemberCreateEvent$Type extends MessageType<AccessListMemberCreat
case /* prehog.v1alpha.AccessListMetadata metadata */ 2:
message.metadata = AccessListMetadata.internalBinaryRead(reader, reader.uint32(), options, message.metadata);
break;
case /* string member_kind */ 3:
message.memberKind = reader.string();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
@@ -7820,6 +7855,9 @@ class AccessListMemberCreateEvent$Type extends MessageType<AccessListMemberCreat
/* prehog.v1alpha.AccessListMetadata metadata = 2; */
if (message.metadata)
AccessListMetadata.internalBinaryWrite(message.metadata, writer.tag(2, WireType.LengthDelimited).fork(), options).join();
/* string member_kind = 3; */
if (message.memberKind !== "")
writer.tag(3, WireType.LengthDelimited).string(message.memberKind);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
@@ -7835,12 +7873,14 @@ class AccessListMemberUpdateEvent$Type extends MessageType<AccessListMemberUpdat
constructor() {
super("prehog.v1alpha.AccessListMemberUpdateEvent", [
{ no: 1, name: "user_name", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
{ no: 2, name: "metadata", kind: "message", T: () => AccessListMetadata }
{ no: 2, name: "metadata", kind: "message", T: () => AccessListMetadata },
{ no: 3, name: "member_kind", kind: "scalar", T: 9 /*ScalarType.STRING*/ }
]);
}
create(value?: PartialMessage<AccessListMemberUpdateEvent>): AccessListMemberUpdateEvent {
const message = globalThis.Object.create((this.messagePrototype!));
message.userName = "";
message.memberKind = "";
if (value !== undefined)
reflectionMergePartial<AccessListMemberUpdateEvent>(this, message, value);
return message;
@@ -7856,6 +7896,9 @@ class AccessListMemberUpdateEvent$Type extends MessageType<AccessListMemberUpdat
case /* prehog.v1alpha.AccessListMetadata metadata */ 2:
message.metadata = AccessListMetadata.internalBinaryRead(reader, reader.uint32(), options, message.metadata);
break;
case /* string member_kind */ 3:
message.memberKind = reader.string();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
@@ -7874,6 +7917,9 @@ class AccessListMemberUpdateEvent$Type extends MessageType<AccessListMemberUpdat
/* prehog.v1alpha.AccessListMetadata metadata = 2; */
if (message.metadata)
AccessListMetadata.internalBinaryWrite(message.metadata, writer.tag(2, WireType.LengthDelimited).fork(), options).join();
/* string member_kind = 3; */
if (message.memberKind !== "")
writer.tag(3, WireType.LengthDelimited).string(message.memberKind);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
@@ -7889,12 +7935,14 @@ class AccessListMemberDeleteEvent$Type extends MessageType<AccessListMemberDelet
constructor() {
super("prehog.v1alpha.AccessListMemberDeleteEvent", [
{ no: 1, name: "user_name", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
{ no: 2, name: "metadata", kind: "message", T: () => AccessListMetadata }
{ no: 2, name: "metadata", kind: "message", T: () => AccessListMetadata },
{ no: 3, name: "member_kind", kind: "scalar", T: 9 /*ScalarType.STRING*/ }
]);
}
create(value?: PartialMessage<AccessListMemberDeleteEvent>): AccessListMemberDeleteEvent {
const message = globalThis.Object.create((this.messagePrototype!));
message.userName = "";
message.memberKind = "";
if (value !== undefined)
reflectionMergePartial<AccessListMemberDeleteEvent>(this, message, value);
return message;
@@ -7910,6 +7958,9 @@ class AccessListMemberDeleteEvent$Type extends MessageType<AccessListMemberDelet
case /* prehog.v1alpha.AccessListMetadata metadata */ 2:
message.metadata = AccessListMetadata.internalBinaryRead(reader, reader.uint32(), options, message.metadata);
break;
case /* string member_kind */ 3:
message.memberKind = reader.string();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
@@ -7928,6 +7979,9 @@ class AccessListMemberDeleteEvent$Type extends MessageType<AccessListMemberDelet
/* prehog.v1alpha.AccessListMetadata metadata = 2; */
if (message.metadata)
AccessListMetadata.internalBinaryWrite(message.metadata, writer.tag(2, WireType.LengthDelimited).fork(), options).join();
/* string member_kind = 3; */
if (message.memberKind !== "")
writer.tag(3, WireType.LengthDelimited).string(message.memberKind);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
@@ -7944,7 +7998,9 @@ class AccessListGrantsToUserEvent$Type extends MessageType<AccessListGrantsToUse
super("prehog.v1alpha.AccessListGrantsToUserEvent", [
{ no: 1, name: "user_name", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
{ no: 2, name: "count_roles_granted", kind: "scalar", T: 5 /*ScalarType.INT32*/ },
{ no: 3, name: "count_traits_granted", kind: "scalar", T: 5 /*ScalarType.INT32*/ }
{ no: 3, name: "count_traits_granted", kind: "scalar", T: 5 /*ScalarType.INT32*/ },
{ no: 4, name: "count_inherited_roles_granted", kind: "scalar", T: 5 /*ScalarType.INT32*/ },
{ no: 5, name: "count_inherited_traits_granted", kind: "scalar", T: 5 /*ScalarType.INT32*/ }
]);
}
create(value?: PartialMessage<AccessListGrantsToUserEvent>): AccessListGrantsToUserEvent {
@@ -7952,6 +8008,8 @@ class AccessListGrantsToUserEvent$Type extends MessageType<AccessListGrantsToUse
message.userName = "";
message.countRolesGranted = 0;
message.countTraitsGranted = 0;
message.countInheritedRolesGranted = 0;
message.countInheritedTraitsGranted = 0;
if (value !== undefined)
reflectionMergePartial<AccessListGrantsToUserEvent>(this, message, value);
return message;
@@ -7970,6 +8028,12 @@ class AccessListGrantsToUserEvent$Type extends MessageType<AccessListGrantsToUse
case /* int32 count_traits_granted */ 3:
message.countTraitsGranted = reader.int32();
break;
case /* int32 count_inherited_roles_granted */ 4:
message.countInheritedRolesGranted = reader.int32();
break;
case /* int32 count_inherited_traits_granted */ 5:
message.countInheritedTraitsGranted = reader.int32();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
@@ -7991,6 +8055,12 @@ class AccessListGrantsToUserEvent$Type extends MessageType<AccessListGrantsToUse
/* int32 count_traits_granted = 3; */
if (message.countTraitsGranted !== 0)
writer.tag(3, WireType.Varint).int32(message.countTraitsGranted);
/* int32 count_inherited_roles_granted = 4; */
if (message.countInheritedRolesGranted !== 0)
writer.tag(4, WireType.Varint).int32(message.countInheritedRolesGranted);
/* int32 count_inherited_traits_granted = 5; */
if (message.countInheritedTraitsGranted !== 0)
writer.tag(5, WireType.Varint).int32(message.countInheritedTraitsGranted);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
@@ -21,6 +21,8 @@
import type { RpcTransport } from "@protobuf-ts/runtime-rpc";
import type { ServiceInfo } from "@protobuf-ts/runtime-rpc";
import { AccessListService } from "./accesslist_service_pb";
import type { GetInheritedGrantsResponse } from "./accesslist_service_pb";
import type { GetInheritedGrantsRequest } from "./accesslist_service_pb";
import type { GetSuggestedAccessListsResponse } from "./accesslist_service_pb";
import type { GetSuggestedAccessListsRequest } from "./accesslist_service_pb";
import type { AccessRequestPromoteResponse } from "./accesslist_service_pb";
@@ -39,6 +41,8 @@ import type { DeleteAllAccessListMembersForAccessListRequest } from "./accesslis
import type { DeleteAccessListMemberRequest } from "./accesslist_service_pb";
import type { UpdateAccessListMemberRequest } from "./accesslist_service_pb";
import type { UpsertAccessListMemberRequest } from "./accesslist_service_pb";
import type { GetAccessListOwnersResponse } from "./accesslist_service_pb";
import type { GetAccessListOwnersRequest } from "./accesslist_service_pb";
import type { Member } from "./accesslist_pb";
import type { GetAccessListMemberRequest } from "./accesslist_service_pb";
import type { ListAllAccessListMembersResponse } from "./accesslist_service_pb";
@@ -144,6 +148,13 @@ export interface IAccessListServiceClient {
* @generated from protobuf rpc: GetAccessListMember(teleport.accesslist.v1.GetAccessListMemberRequest) returns (teleport.accesslist.v1.Member);
*/
getAccessListMember(input: GetAccessListMemberRequest, options?: RpcOptions): UnaryCall<GetAccessListMemberRequest, Member>;
/**
* GetAccessListOwners returns a list of all owners in an Access List,
* including those inherited from nested Access Lists.
*
* @generated from protobuf rpc: GetAccessListOwners(teleport.accesslist.v1.GetAccessListOwnersRequest) returns (teleport.accesslist.v1.GetAccessListOwnersResponse);
*/
getAccessListOwners(input: GetAccessListOwnersRequest, options?: RpcOptions): UnaryCall<GetAccessListOwnersRequest, GetAccessListOwnersResponse>;
/**
* UpsertAccessListMember creates or updates an access list member resource.
*
@@ -224,6 +235,12 @@ export interface IAccessListServiceClient {
* @generated from protobuf rpc: GetSuggestedAccessLists(teleport.accesslist.v1.GetSuggestedAccessListsRequest) returns (teleport.accesslist.v1.GetSuggestedAccessListsResponse);
*/
getSuggestedAccessLists(input: GetSuggestedAccessListsRequest, options?: RpcOptions): UnaryCall<GetSuggestedAccessListsRequest, GetSuggestedAccessListsResponse>;
/**
* GetInheritedGrants returns the inherited grants for an access list.
*
* @generated from protobuf rpc: GetInheritedGrants(teleport.accesslist.v1.GetInheritedGrantsRequest) returns (teleport.accesslist.v1.GetInheritedGrantsResponse);
*/
getInheritedGrants(input: GetInheritedGrantsRequest, options?: RpcOptions): UnaryCall<GetInheritedGrantsRequest, GetInheritedGrantsResponse>;
}
/**
* AccessListService provides CRUD methods for Access List resources.
@@ -347,13 +364,23 @@ export class AccessListServiceClient implements IAccessListServiceClient, Servic
const method = this.methods[11], opt = this._transport.mergeOptions(options);
return stackIntercept<GetAccessListMemberRequest, Member>("unary", this._transport, method, opt, input);
}
/**
* GetAccessListOwners returns a list of all owners in an Access List,
* including those inherited from nested Access Lists.
*
* @generated from protobuf rpc: GetAccessListOwners(teleport.accesslist.v1.GetAccessListOwnersRequest) returns (teleport.accesslist.v1.GetAccessListOwnersResponse);
*/
getAccessListOwners(input: GetAccessListOwnersRequest, options?: RpcOptions): UnaryCall<GetAccessListOwnersRequest, GetAccessListOwnersResponse> {
const method = this.methods[12], opt = this._transport.mergeOptions(options);
return stackIntercept<GetAccessListOwnersRequest, GetAccessListOwnersResponse>("unary", this._transport, method, opt, input);
}
/**
* UpsertAccessListMember creates or updates an access list member resource.
*
* @generated from protobuf rpc: UpsertAccessListMember(teleport.accesslist.v1.UpsertAccessListMemberRequest) returns (teleport.accesslist.v1.Member);
*/
upsertAccessListMember(input: UpsertAccessListMemberRequest, options?: RpcOptions): UnaryCall<UpsertAccessListMemberRequest, Member> {
const method = this.methods[12], opt = this._transport.mergeOptions(options);
const method = this.methods[13], opt = this._transport.mergeOptions(options);
return stackIntercept<UpsertAccessListMemberRequest, Member>("unary", this._transport, method, opt, input);
}
/**
@@ -362,7 +389,7 @@ export class AccessListServiceClient implements IAccessListServiceClient, Servic
* @generated from protobuf rpc: UpdateAccessListMember(teleport.accesslist.v1.UpdateAccessListMemberRequest) returns (teleport.accesslist.v1.Member);
*/
updateAccessListMember(input: UpdateAccessListMemberRequest, options?: RpcOptions): UnaryCall<UpdateAccessListMemberRequest, Member> {
const method = this.methods[13], opt = this._transport.mergeOptions(options);
const method = this.methods[14], opt = this._transport.mergeOptions(options);
return stackIntercept<UpdateAccessListMemberRequest, Member>("unary", this._transport, method, opt, input);
}
/**
@@ -372,7 +399,7 @@ export class AccessListServiceClient implements IAccessListServiceClient, Servic
* @generated from protobuf rpc: DeleteAccessListMember(teleport.accesslist.v1.DeleteAccessListMemberRequest) returns (google.protobuf.Empty);
*/
deleteAccessListMember(input: DeleteAccessListMemberRequest, options?: RpcOptions): UnaryCall<DeleteAccessListMemberRequest, Empty> {
const method = this.methods[14], opt = this._transport.mergeOptions(options);
const method = this.methods[15], opt = this._transport.mergeOptions(options);
return stackIntercept<DeleteAccessListMemberRequest, Empty>("unary", this._transport, method, opt, input);
}
/**
@@ -382,7 +409,7 @@ export class AccessListServiceClient implements IAccessListServiceClient, Servic
* @generated from protobuf rpc: DeleteAllAccessListMembersForAccessList(teleport.accesslist.v1.DeleteAllAccessListMembersForAccessListRequest) returns (google.protobuf.Empty);
*/
deleteAllAccessListMembersForAccessList(input: DeleteAllAccessListMembersForAccessListRequest, options?: RpcOptions): UnaryCall<DeleteAllAccessListMembersForAccessListRequest, Empty> {
const method = this.methods[15], opt = this._transport.mergeOptions(options);
const method = this.methods[16], opt = this._transport.mergeOptions(options);
return stackIntercept<DeleteAllAccessListMembersForAccessListRequest, Empty>("unary", this._transport, method, opt, input);
}
/**
@@ -392,7 +419,7 @@ export class AccessListServiceClient implements IAccessListServiceClient, Servic
* @generated from protobuf rpc: DeleteAllAccessListMembers(teleport.accesslist.v1.DeleteAllAccessListMembersRequest) returns (google.protobuf.Empty);
*/
deleteAllAccessListMembers(input: DeleteAllAccessListMembersRequest, options?: RpcOptions): UnaryCall<DeleteAllAccessListMembersRequest, Empty> {
const method = this.methods[16], opt = this._transport.mergeOptions(options);
const method = this.methods[17], opt = this._transport.mergeOptions(options);
return stackIntercept<DeleteAllAccessListMembersRequest, Empty>("unary", this._transport, method, opt, input);
}
/**
@@ -401,7 +428,7 @@ export class AccessListServiceClient implements IAccessListServiceClient, Servic
* @generated from protobuf rpc: UpsertAccessListWithMembers(teleport.accesslist.v1.UpsertAccessListWithMembersRequest) returns (teleport.accesslist.v1.UpsertAccessListWithMembersResponse);
*/
upsertAccessListWithMembers(input: UpsertAccessListWithMembersRequest, options?: RpcOptions): UnaryCall<UpsertAccessListWithMembersRequest, UpsertAccessListWithMembersResponse> {
const method = this.methods[17], opt = this._transport.mergeOptions(options);
const method = this.methods[18], opt = this._transport.mergeOptions(options);
return stackIntercept<UpsertAccessListWithMembersRequest, UpsertAccessListWithMembersResponse>("unary", this._transport, method, opt, input);
}
/**
@@ -411,7 +438,7 @@ export class AccessListServiceClient implements IAccessListServiceClient, Servic
* @generated from protobuf rpc: ListAccessListReviews(teleport.accesslist.v1.ListAccessListReviewsRequest) returns (teleport.accesslist.v1.ListAccessListReviewsResponse);
*/
listAccessListReviews(input: ListAccessListReviewsRequest, options?: RpcOptions): UnaryCall<ListAccessListReviewsRequest, ListAccessListReviewsResponse> {
const method = this.methods[18], opt = this._transport.mergeOptions(options);
const method = this.methods[19], opt = this._transport.mergeOptions(options);
return stackIntercept<ListAccessListReviewsRequest, ListAccessListReviewsResponse>("unary", this._transport, method, opt, input);
}
/**
@@ -421,7 +448,7 @@ export class AccessListServiceClient implements IAccessListServiceClient, Servic
* @generated from protobuf rpc: ListAllAccessListReviews(teleport.accesslist.v1.ListAllAccessListReviewsRequest) returns (teleport.accesslist.v1.ListAllAccessListReviewsResponse);
*/
listAllAccessListReviews(input: ListAllAccessListReviewsRequest, options?: RpcOptions): UnaryCall<ListAllAccessListReviewsRequest, ListAllAccessListReviewsResponse> {
const method = this.methods[19], opt = this._transport.mergeOptions(options);
const method = this.methods[20], opt = this._transport.mergeOptions(options);
return stackIntercept<ListAllAccessListReviewsRequest, ListAllAccessListReviewsResponse>("unary", this._transport, method, opt, input);
}
/**
@@ -432,7 +459,7 @@ export class AccessListServiceClient implements IAccessListServiceClient, Servic
* @generated from protobuf rpc: CreateAccessListReview(teleport.accesslist.v1.CreateAccessListReviewRequest) returns (teleport.accesslist.v1.CreateAccessListReviewResponse);
*/
createAccessListReview(input: CreateAccessListReviewRequest, options?: RpcOptions): UnaryCall<CreateAccessListReviewRequest, CreateAccessListReviewResponse> {
const method = this.methods[20], opt = this._transport.mergeOptions(options);
const method = this.methods[21], opt = this._transport.mergeOptions(options);
return stackIntercept<CreateAccessListReviewRequest, CreateAccessListReviewResponse>("unary", this._transport, method, opt, input);
}
/**
@@ -441,7 +468,7 @@ export class AccessListServiceClient implements IAccessListServiceClient, Servic
* @generated from protobuf rpc: DeleteAccessListReview(teleport.accesslist.v1.DeleteAccessListReviewRequest) returns (google.protobuf.Empty);
*/
deleteAccessListReview(input: DeleteAccessListReviewRequest, options?: RpcOptions): UnaryCall<DeleteAccessListReviewRequest, Empty> {
const method = this.methods[21], opt = this._transport.mergeOptions(options);
const method = this.methods[22], opt = this._transport.mergeOptions(options);
return stackIntercept<DeleteAccessListReviewRequest, Empty>("unary", this._transport, method, opt, input);
}
/**
@@ -450,7 +477,7 @@ export class AccessListServiceClient implements IAccessListServiceClient, Servic
* @generated from protobuf rpc: AccessRequestPromote(teleport.accesslist.v1.AccessRequestPromoteRequest) returns (teleport.accesslist.v1.AccessRequestPromoteResponse);
*/
accessRequestPromote(input: AccessRequestPromoteRequest, options?: RpcOptions): UnaryCall<AccessRequestPromoteRequest, AccessRequestPromoteResponse> {
const method = this.methods[22], opt = this._transport.mergeOptions(options);
const method = this.methods[23], opt = this._transport.mergeOptions(options);
return stackIntercept<AccessRequestPromoteRequest, AccessRequestPromoteResponse>("unary", this._transport, method, opt, input);
}
/**
@@ -460,7 +487,16 @@ export class AccessListServiceClient implements IAccessListServiceClient, Servic
* @generated from protobuf rpc: GetSuggestedAccessLists(teleport.accesslist.v1.GetSuggestedAccessListsRequest) returns (teleport.accesslist.v1.GetSuggestedAccessListsResponse);
*/
getSuggestedAccessLists(input: GetSuggestedAccessListsRequest, options?: RpcOptions): UnaryCall<GetSuggestedAccessListsRequest, GetSuggestedAccessListsResponse> {
const method = this.methods[23], opt = this._transport.mergeOptions(options);
const method = this.methods[24], opt = this._transport.mergeOptions(options);
return stackIntercept<GetSuggestedAccessListsRequest, GetSuggestedAccessListsResponse>("unary", this._transport, method, opt, input);
}
/**
* GetInheritedGrants returns the inherited grants for an access list.
*
* @generated from protobuf rpc: GetInheritedGrants(teleport.accesslist.v1.GetInheritedGrantsRequest) returns (teleport.accesslist.v1.GetInheritedGrantsResponse);
*/
getInheritedGrants(input: GetInheritedGrantsRequest, options?: RpcOptions): UnaryCall<GetInheritedGrantsRequest, GetInheritedGrantsResponse> {
const method = this.methods[25], opt = this._transport.mergeOptions(options);
return stackIntercept<GetInheritedGrantsRequest, GetInheritedGrantsResponse>("unary", this._transport, method, opt, input);
}
}
@@ -18,6 +18,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { GetInheritedGrantsResponse } from "./accesslist_service_pb";
import { GetInheritedGrantsRequest } from "./accesslist_service_pb";
import { GetSuggestedAccessListsResponse } from "./accesslist_service_pb";
import { GetSuggestedAccessListsRequest } from "./accesslist_service_pb";
import { AccessRequestPromoteResponse } from "./accesslist_service_pb";
@@ -36,6 +38,8 @@ import { DeleteAllAccessListMembersForAccessListRequest } from "./accesslist_ser
import { DeleteAccessListMemberRequest } from "./accesslist_service_pb";
import { UpdateAccessListMemberRequest } from "./accesslist_service_pb";
import { UpsertAccessListMemberRequest } from "./accesslist_service_pb";
import { GetAccessListOwnersResponse } from "./accesslist_service_pb";
import { GetAccessListOwnersRequest } from "./accesslist_service_pb";
import { Member } from "./accesslist_pb";
import { GetAccessListMemberRequest } from "./accesslist_service_pb";
import { ListAllAccessListMembersResponse } from "./accesslist_service_pb";
@@ -139,6 +143,13 @@ export interface IAccessListService extends grpc.UntypedServiceImplementation {
* @generated from protobuf rpc: GetAccessListMember(teleport.accesslist.v1.GetAccessListMemberRequest) returns (teleport.accesslist.v1.Member);
*/
getAccessListMember: grpc.handleUnaryCall<GetAccessListMemberRequest, Member>;
/**
* GetAccessListOwners returns a list of all owners in an Access List,
* including those inherited from nested Access Lists.
*
* @generated from protobuf rpc: GetAccessListOwners(teleport.accesslist.v1.GetAccessListOwnersRequest) returns (teleport.accesslist.v1.GetAccessListOwnersResponse);
*/
getAccessListOwners: grpc.handleUnaryCall<GetAccessListOwnersRequest, GetAccessListOwnersResponse>;
/**
* UpsertAccessListMember creates or updates an access list member resource.
*
@@ -219,6 +230,12 @@ export interface IAccessListService extends grpc.UntypedServiceImplementation {
* @generated from protobuf rpc: GetSuggestedAccessLists(teleport.accesslist.v1.GetSuggestedAccessListsRequest) returns (teleport.accesslist.v1.GetSuggestedAccessListsResponse);
*/
getSuggestedAccessLists: grpc.handleUnaryCall<GetSuggestedAccessListsRequest, GetSuggestedAccessListsResponse>;
/**
* GetInheritedGrants returns the inherited grants for an access list.
*
* @generated from protobuf rpc: GetInheritedGrants(teleport.accesslist.v1.GetInheritedGrantsRequest) returns (teleport.accesslist.v1.GetInheritedGrantsResponse);
*/
getInheritedGrants: grpc.handleUnaryCall<GetInheritedGrantsRequest, GetInheritedGrantsResponse>;
}
/**
* @grpc/grpc-js definition for the protobuf service teleport.accesslist.v1.AccessListService.
@@ -352,6 +369,16 @@ export const accessListServiceDefinition: grpc.ServiceDefinition<IAccessListServ
responseSerialize: value => Buffer.from(Member.toBinary(value)),
requestSerialize: value => Buffer.from(GetAccessListMemberRequest.toBinary(value))
},
getAccessListOwners: {
path: "/teleport.accesslist.v1.AccessListService/GetAccessListOwners",
originalName: "GetAccessListOwners",
requestStream: false,
responseStream: false,
responseDeserialize: bytes => GetAccessListOwnersResponse.fromBinary(bytes),
requestDeserialize: bytes => GetAccessListOwnersRequest.fromBinary(bytes),
responseSerialize: value => Buffer.from(GetAccessListOwnersResponse.toBinary(value)),
requestSerialize: value => Buffer.from(GetAccessListOwnersRequest.toBinary(value))
},
upsertAccessListMember: {
path: "/teleport.accesslist.v1.AccessListService/UpsertAccessListMember",
originalName: "UpsertAccessListMember",
@@ -471,5 +498,15 @@ export const accessListServiceDefinition: grpc.ServiceDefinition<IAccessListServ
requestDeserialize: bytes => GetSuggestedAccessListsRequest.fromBinary(bytes),
responseSerialize: value => Buffer.from(GetSuggestedAccessListsResponse.toBinary(value)),
requestSerialize: value => Buffer.from(GetSuggestedAccessListsRequest.toBinary(value))
},
getInheritedGrants: {
path: "/teleport.accesslist.v1.AccessListService/GetInheritedGrants",
originalName: "GetInheritedGrants",
requestStream: false,
responseStream: false,
responseDeserialize: bytes => GetInheritedGrantsResponse.fromBinary(bytes),
requestDeserialize: bytes => GetInheritedGrantsRequest.fromBinary(bytes),
responseSerialize: value => Buffer.from(GetInheritedGrantsResponse.toBinary(value)),
requestSerialize: value => Buffer.from(GetInheritedGrantsRequest.toBinary(value))
}
};
+262 -2
View File
@@ -32,7 +32,9 @@ import { MessageType } from "@protobuf-ts/runtime";
import { AccessRequestV3 } from "../../legacy/types/types_pb";
import { Timestamp } from "../../../google/protobuf/timestamp_pb";
import { Review } from "./accesslist_pb";
import { AccessListOwner } from "./accesslist_pb";
import { Member } from "./accesslist_pb";
import { AccessListGrants } from "./accesslist_pb";
import { AccessList } from "./accesslist_pb";
/**
* GetAccessListsRequest is the request for getting all access lists.
@@ -92,6 +94,32 @@ export interface ListAccessListsResponse {
*/
nextToken: string;
}
/**
* GetInheritedGrantsRequest is the request for getting inherited grants.
*
* @generated from protobuf message teleport.accesslist.v1.GetInheritedGrantsRequest
*/
export interface GetInheritedGrantsRequest {
/**
* access_list_id is the ID of the access list to retrieve.
*
* @generated from protobuf field: string access_list_id = 1;
*/
accessListId: string;
}
/**
* GetInheritedGrantsResponse is the response for getting inherited grants.
*
* @generated from protobuf message teleport.accesslist.v1.GetInheritedGrantsResponse
*/
export interface GetInheritedGrantsResponse {
/**
* grants is the list of inherited member grants.
*
* @generated from protobuf field: teleport.accesslist.v1.AccessListGrants grants = 1;
*/
grants?: AccessListGrants;
}
/**
* GetAccessListRequest is the request for retrieving an access list.
*
@@ -198,6 +226,12 @@ export interface CountAccessListMembersResponse {
* @generated from protobuf field: uint32 count = 1;
*/
count: number;
/**
* list_count is the number of access list members of type list in the access list.
*
* @generated from protobuf field: uint32 list_count = 2;
*/
listCount: number;
}
/**
* ListAccessListMembersRequest is the request for getting paginated access list
@@ -345,6 +379,35 @@ export interface GetAccessListMemberRequest {
*/
memberName: string;
}
/**
* GetAccessListOwnersRequest is the request for getting a list of all owners
* in an Access List, including those inherited from nested Access Lists.
*
* @generated from protobuf message teleport.accesslist.v1.GetAccessListOwnersRequest
*/
export interface GetAccessListOwnersRequest {
/**
* access_list is the name of the access list.
*
* @generated from protobuf field: string access_list = 1;
*/
accessList: string;
}
/**
* GetAccessListOwnersResponse is the response for getting a list of all
* owners in an Access List, including those inherited from nested Access Lists.
*
* @generated from protobuf message teleport.accesslist.v1.GetAccessListOwnersResponse
*/
export interface GetAccessListOwnersResponse {
/**
* owners is the list of all owners in the Access List, including those
* inherited from nested Access Lists.
*
* @generated from protobuf field: repeated teleport.accesslist.v1.AccessListOwner owners = 1;
*/
owners: AccessListOwner[];
}
/**
* UpsertAccessListMemberRequest is the request for upserting an access list
* member.
@@ -806,6 +869,99 @@ class ListAccessListsResponse$Type extends MessageType<ListAccessListsResponse>
*/
export const ListAccessListsResponse = new ListAccessListsResponse$Type();
// @generated message type with reflection information, may provide speed optimized methods
class GetInheritedGrantsRequest$Type extends MessageType<GetInheritedGrantsRequest> {
constructor() {
super("teleport.accesslist.v1.GetInheritedGrantsRequest", [
{ no: 1, name: "access_list_id", kind: "scalar", T: 9 /*ScalarType.STRING*/ }
]);
}
create(value?: PartialMessage<GetInheritedGrantsRequest>): GetInheritedGrantsRequest {
const message = globalThis.Object.create((this.messagePrototype!));
message.accessListId = "";
if (value !== undefined)
reflectionMergePartial<GetInheritedGrantsRequest>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: GetInheritedGrantsRequest): GetInheritedGrantsRequest {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* string access_list_id */ 1:
message.accessListId = reader.string();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: GetInheritedGrantsRequest, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* string access_list_id = 1; */
if (message.accessListId !== "")
writer.tag(1, WireType.LengthDelimited).string(message.accessListId);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
}
/**
* @generated MessageType for protobuf message teleport.accesslist.v1.GetInheritedGrantsRequest
*/
export const GetInheritedGrantsRequest = new GetInheritedGrantsRequest$Type();
// @generated message type with reflection information, may provide speed optimized methods
class GetInheritedGrantsResponse$Type extends MessageType<GetInheritedGrantsResponse> {
constructor() {
super("teleport.accesslist.v1.GetInheritedGrantsResponse", [
{ no: 1, name: "grants", kind: "message", T: () => AccessListGrants }
]);
}
create(value?: PartialMessage<GetInheritedGrantsResponse>): GetInheritedGrantsResponse {
const message = globalThis.Object.create((this.messagePrototype!));
if (value !== undefined)
reflectionMergePartial<GetInheritedGrantsResponse>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: GetInheritedGrantsResponse): GetInheritedGrantsResponse {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* teleport.accesslist.v1.AccessListGrants grants */ 1:
message.grants = AccessListGrants.internalBinaryRead(reader, reader.uint32(), options, message.grants);
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: GetInheritedGrantsResponse, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* teleport.accesslist.v1.AccessListGrants grants = 1; */
if (message.grants)
AccessListGrants.internalBinaryWrite(message.grants, writer.tag(1, WireType.LengthDelimited).fork(), options).join();
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
}
/**
* @generated MessageType for protobuf message teleport.accesslist.v1.GetInheritedGrantsResponse
*/
export const GetInheritedGrantsResponse = new GetInheritedGrantsResponse$Type();
// @generated message type with reflection information, may provide speed optimized methods
class GetAccessListRequest$Type extends MessageType<GetAccessListRequest> {
constructor() {
super("teleport.accesslist.v1.GetAccessListRequest", [
@@ -1139,12 +1295,14 @@ export const CountAccessListMembersRequest = new CountAccessListMembersRequest$T
class CountAccessListMembersResponse$Type extends MessageType<CountAccessListMembersResponse> {
constructor() {
super("teleport.accesslist.v1.CountAccessListMembersResponse", [
{ no: 1, name: "count", kind: "scalar", T: 13 /*ScalarType.UINT32*/ }
{ no: 1, name: "count", kind: "scalar", T: 13 /*ScalarType.UINT32*/ },
{ no: 2, name: "list_count", kind: "scalar", T: 13 /*ScalarType.UINT32*/ }
]);
}
create(value?: PartialMessage<CountAccessListMembersResponse>): CountAccessListMembersResponse {
const message = globalThis.Object.create((this.messagePrototype!));
message.count = 0;
message.listCount = 0;
if (value !== undefined)
reflectionMergePartial<CountAccessListMembersResponse>(this, message, value);
return message;
@@ -1157,6 +1315,9 @@ class CountAccessListMembersResponse$Type extends MessageType<CountAccessListMem
case /* uint32 count */ 1:
message.count = reader.uint32();
break;
case /* uint32 list_count */ 2:
message.listCount = reader.uint32();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
@@ -1172,6 +1333,9 @@ class CountAccessListMembersResponse$Type extends MessageType<CountAccessListMem
/* uint32 count = 1; */
if (message.count !== 0)
writer.tag(1, WireType.Varint).uint32(message.count);
/* uint32 list_count = 2; */
if (message.listCount !== 0)
writer.tag(2, WireType.Varint).uint32(message.listCount);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
@@ -1574,6 +1738,100 @@ class GetAccessListMemberRequest$Type extends MessageType<GetAccessListMemberReq
*/
export const GetAccessListMemberRequest = new GetAccessListMemberRequest$Type();
// @generated message type with reflection information, may provide speed optimized methods
class GetAccessListOwnersRequest$Type extends MessageType<GetAccessListOwnersRequest> {
constructor() {
super("teleport.accesslist.v1.GetAccessListOwnersRequest", [
{ no: 1, name: "access_list", kind: "scalar", T: 9 /*ScalarType.STRING*/ }
]);
}
create(value?: PartialMessage<GetAccessListOwnersRequest>): GetAccessListOwnersRequest {
const message = globalThis.Object.create((this.messagePrototype!));
message.accessList = "";
if (value !== undefined)
reflectionMergePartial<GetAccessListOwnersRequest>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: GetAccessListOwnersRequest): GetAccessListOwnersRequest {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* string access_list */ 1:
message.accessList = reader.string();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: GetAccessListOwnersRequest, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* string access_list = 1; */
if (message.accessList !== "")
writer.tag(1, WireType.LengthDelimited).string(message.accessList);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
}
/**
* @generated MessageType for protobuf message teleport.accesslist.v1.GetAccessListOwnersRequest
*/
export const GetAccessListOwnersRequest = new GetAccessListOwnersRequest$Type();
// @generated message type with reflection information, may provide speed optimized methods
class GetAccessListOwnersResponse$Type extends MessageType<GetAccessListOwnersResponse> {
constructor() {
super("teleport.accesslist.v1.GetAccessListOwnersResponse", [
{ no: 1, name: "owners", kind: "message", repeat: 1 /*RepeatType.PACKED*/, T: () => AccessListOwner }
]);
}
create(value?: PartialMessage<GetAccessListOwnersResponse>): GetAccessListOwnersResponse {
const message = globalThis.Object.create((this.messagePrototype!));
message.owners = [];
if (value !== undefined)
reflectionMergePartial<GetAccessListOwnersResponse>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: GetAccessListOwnersResponse): GetAccessListOwnersResponse {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* repeated teleport.accesslist.v1.AccessListOwner owners */ 1:
message.owners.push(AccessListOwner.internalBinaryRead(reader, reader.uint32(), options));
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: GetAccessListOwnersResponse, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* repeated teleport.accesslist.v1.AccessListOwner owners = 1; */
for (let i = 0; i < message.owners.length; i++)
AccessListOwner.internalBinaryWrite(message.owners[i], writer.tag(1, WireType.LengthDelimited).fork(), options).join();
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
}
/**
* @generated MessageType for protobuf message teleport.accesslist.v1.GetAccessListOwnersResponse
*/
export const GetAccessListOwnersResponse = new GetAccessListOwnersResponse$Type();
// @generated message type with reflection information, may provide speed optimized methods
class UpsertAccessListMemberRequest$Type extends MessageType<UpsertAccessListMemberRequest> {
constructor() {
super("teleport.accesslist.v1.UpsertAccessListMemberRequest", [
@@ -2394,6 +2652,7 @@ export const AccessListService = new ServiceType("teleport.accesslist.v1.AccessL
{ name: "ListAccessListMembers", options: {}, I: ListAccessListMembersRequest, O: ListAccessListMembersResponse },
{ name: "ListAllAccessListMembers", options: {}, I: ListAllAccessListMembersRequest, O: ListAllAccessListMembersResponse },
{ name: "GetAccessListMember", options: {}, I: GetAccessListMemberRequest, O: Member },
{ name: "GetAccessListOwners", options: {}, I: GetAccessListOwnersRequest, O: GetAccessListOwnersResponse },
{ name: "UpsertAccessListMember", options: {}, I: UpsertAccessListMemberRequest, O: Member },
{ name: "UpdateAccessListMember", options: {}, I: UpdateAccessListMemberRequest, O: Member },
{ name: "DeleteAccessListMember", options: {}, I: DeleteAccessListMemberRequest, O: Empty },
@@ -2405,5 +2664,6 @@ export const AccessListService = new ServiceType("teleport.accesslist.v1.AccessL
{ name: "CreateAccessListReview", options: {}, I: CreateAccessListReviewRequest, O: CreateAccessListReviewResponse },
{ name: "DeleteAccessListReview", options: {}, I: DeleteAccessListReviewRequest, O: Empty },
{ name: "AccessRequestPromote", options: {}, I: AccessRequestPromoteRequest, O: AccessRequestPromoteResponse },
{ name: "GetSuggestedAccessLists", options: {}, I: GetSuggestedAccessListsRequest, O: GetSuggestedAccessListsResponse }
{ name: "GetSuggestedAccessLists", options: {}, I: GetSuggestedAccessListsRequest, O: GetSuggestedAccessListsResponse },
{ name: "GetInheritedGrants", options: {}, I: GetInheritedGrantsRequest, O: GetInheritedGrantsResponse }
]);
+17 -2
View File
@@ -249,10 +249,25 @@ func (a *App) getRecipientsRequiringReminders(ctx context.Context, accessList *a
func (a *App) fetchRecipients(ctx context.Context, accessList *accesslist.AccessList, now, notificationStart time.Time) map[string]common.Recipient {
log := logger.Get(ctx)
allRecipients := make(map[string]common.Recipient, len(accessList.Spec.Owners))
var allOwners []*accesslist.Owner
allOwners, err := a.apiClient.GetAccessListOwners(ctx, accessList.GetName())
if err != nil {
// TODO(kiosion): Remove in v18; protecting against server not having `GetAccessListOwners` func.
if trace.IsNotImplemented(err) {
log.WithError(err).Warnf("Error getting nested owners for access list '%v', continuing with only explicit owners", accessList.GetName())
for _, owner := range accessList.Spec.Owners {
allOwners = append(allOwners, &owner)
}
} else {
log.WithError(err).Errorf("Error getting owners for access list '%v'", accessList.GetName())
}
}
allRecipients := make(map[string]common.Recipient, len(allOwners))
// Get the owners from the bot as recipients.
for _, owner := range accessList.Spec.Owners {
for _, owner := range allOwners {
recipient, err := a.bot.FetchRecipient(ctx, owner.Name)
if err != nil {
log.Debugf("error getting recipient %s", owner.Name)
+4
View File
@@ -64,6 +64,10 @@ func (w *wrappedClient) ListAccessLists(ctx context.Context, pageSize int, pageT
return w.Client.AccessListClient().ListAccessLists(ctx, pageSize, pageToken)
}
func (w *wrappedClient) GetAccessListOwners(ctx context.Context, accessListName string) ([]*accesslist.Owner, error) {
return w.Client.AccessListClient().GetAccessListOwners(ctx, accessListName)
}
// ListAccessMonitoringRulesWithFilter lists current access monitoring rules.
func (w *wrappedClient) ListAccessMonitoringRulesWithFilter(ctx context.Context, pageSize int, pageToken string, subjects []string, notificationName string) ([]*accessmonitoringrulesv1.AccessMonitoringRule, string, error) {
return w.Client.AccessMonitoringRulesClient().ListAccessMonitoringRulesWithFilter(ctx, pageSize, pageToken, subjects, notificationName)
@@ -42,4 +42,5 @@ type Client interface {
ListResources(ctx context.Context, req proto.ListResourcesRequest) (*types.ListResourcesResponse, error)
ListAccessLists(context.Context, int, string) ([]*accesslist.AccessList, string, error)
ListAccessMonitoringRulesWithFilter(ctx context.Context, pageSize int, pageToken string, subjects []string, notificationName string) ([]*accessmonitoringrulesv1.AccessMonitoringRule, string, error)
GetAccessListOwners(ctx context.Context, accessListName string) ([]*accesslist.Owner, error)
}
+116 -2
View File
@@ -1564,11 +1564,17 @@ func TestUpdateAccessRequestWithAdditionalReviewers(t *testing.T) {
return req
}
mustAccessList := func(name string, owners ...string) *accesslist.AccessList {
type testAccessListOwner struct {
name string
kind string
}
mustAccessListWithMembershipKind := func(name string, owners ...testAccessListOwner) *accesslist.AccessList {
ownersSpec := make([]accesslist.Owner, len(owners))
for i, owner := range owners {
ownersSpec[i] = accesslist.Owner{
Name: owner,
Name: owner.name,
MembershipKind: owner.kind,
}
}
accessList, err := accesslist.NewAccessList(header.Metadata{
@@ -1587,10 +1593,22 @@ func TestUpdateAccessRequestWithAdditionalReviewers(t *testing.T) {
return accessList
}
mustAccessList := func(name string, owners ...string) *accesslist.AccessList {
ownersStruct := make([]testAccessListOwner, 0, len(owners))
for _, owner := range owners {
ownersStruct = append(ownersStruct, testAccessListOwner{owner, accesslist.MembershipKindUser})
}
return mustAccessListWithMembershipKind(name, ownersStruct...)
}
tests := []struct {
name string
req types.AccessRequest
accessLists []*accesslist.AccessList
accessListMembers []struct {
Header header.Metadata
Spec accesslist.AccessListMemberSpec
}
promotions *types.AccessRequestAllowedPromotions
expectedReviewers []string
}{
@@ -1615,6 +1633,94 @@ func TestUpdateAccessRequestWithAdditionalReviewers(t *testing.T) {
},
expectedReviewers: []string{"rev1", "rev2", "owner1", "owner2", "owner3"},
},
{
name: "with ownership through nested list",
req: mustRequest("rev1"),
accessLists: []*accesslist.AccessList{
mustAccessList("nested1", "owner1"),
mustAccessListWithMembershipKind(
"nested",
testAccessListOwner{"owner1", accesslist.MembershipKindUser},
testAccessListOwner{"nested1", accesslist.MembershipKindList},
),
mustAccessListWithMembershipKind(
"root",
testAccessListOwner{"owner1", accesslist.MembershipKindUser},
testAccessListOwner{"nested", accesslist.MembershipKindList},
),
},
accessListMembers: []struct {
Header header.Metadata
Spec accesslist.AccessListMemberSpec
}{
{
Header: header.Metadata{
Name: "nested",
},
Spec: accesslist.AccessListMemberSpec{
AccessList: "root",
Name: "nested",
Joined: clock.Now().UTC(),
Expires: clock.Now().UTC().Add(24 * time.Hour),
Reason: "because",
AddedBy: "owner1",
MembershipKind: accesslist.MembershipKindList,
},
},
{
Header: header.Metadata{
Name: "nested1",
},
Spec: accesslist.AccessListMemberSpec{
AccessList: "nested",
Name: "nested1",
Joined: clock.Now().UTC(),
Expires: clock.Now().UTC().Add(24 * time.Hour),
Reason: "because",
AddedBy: "owner1",
MembershipKind: accesslist.MembershipKindList,
},
},
{
Header: header.Metadata{
Name: "owner2",
},
Spec: accesslist.AccessListMemberSpec{
AccessList: "nested",
Name: "owner2",
Joined: clock.Now().UTC(),
Expires: clock.Now().UTC().Add(24 * time.Hour),
Reason: "because",
AddedBy: "owner1",
MembershipKind: accesslist.MembershipKindUser,
},
},
{
Header: header.Metadata{
Name: "owner3",
},
Spec: accesslist.AccessListMemberSpec{
AccessList: "nested1",
Name: "owner3",
Joined: clock.Now().UTC(),
Expires: clock.Now().UTC().Add(24 * time.Hour),
Reason: "because",
AddedBy: "owner1",
MembershipKind: accesslist.MembershipKindUser,
},
},
},
promotions: &types.AccessRequestAllowedPromotions{
Promotions: []*types.AccessRequestAllowedPromotion{
{AccessListName: "root"},
{AccessListName: "nested"},
},
},
// owner1 is owner of 'root', should be included
// owner2 is member of 'nested', which is owner of 'root', should be included via inheritance
// owner3 is member of 'nested1', which is member of 'nested', which is owner of 'root', should be included via two levels of inheritance
expectedReviewers: []string{"rev1", "owner1", "owner2", "owner3"},
},
{
name: "no promotions",
req: mustRequest("rev1", "rev2"),
@@ -1644,6 +1750,14 @@ func TestUpdateAccessRequestWithAdditionalReviewers(t *testing.T) {
_, err = accessLists.UpsertAccessList(ctx, accessList)
require.NoError(t, err)
}
if test.accessListMembers != nil {
for _, memberData := range test.accessListMembers {
member, err := accesslist.NewAccessListMember(memberData.Header, memberData.Spec)
require.NoError(t, err)
_, err = accessLists.UpsertAccessListMember(ctx, member)
require.NoError(t, err)
}
}
req := test.req.Copy()
updateAccessRequestWithAdditionalReviewers(ctx, req, accessLists, test.promotions)
+3 -3
View File
@@ -5305,13 +5305,13 @@ func updateAccessRequestWithAdditionalReviewers(ctx context.Context, req types.A
// Iterate through the promotions, adding the owners of the corresponding access lists as reviewers.
for _, promotion := range promotions.Promotions {
accessList, err := accessLists.GetAccessList(ctx, promotion.AccessListName)
allOwners, err := accessLists.GetAccessListOwners(ctx, promotion.AccessListName)
if err != nil {
log.WithError(err).Warn("Failed to get access list, skipping additional reviewers")
log.WithError(err).Warnf("Failed to get nested access list owners for %v, skipping additional reviewers", promotion.AccessListName)
break
}
for _, owner := range accessList.GetOwners() {
for _, owner := range allOwners {
additionalReviewers[owner.Name] = struct{}{}
}
}
+1 -1
View File
@@ -1164,7 +1164,7 @@ type Cache interface {
GetAccessList(context.Context, string) (*accesslist.AccessList, error)
// CountAccessListMembers will count all access list members.
CountAccessListMembers(ctx context.Context, accessListName string) (uint32, error)
CountAccessListMembers(ctx context.Context, accessListName string) (users uint32, lists uint32, err error)
// ListAccessListMembers returns a paginated list of all access list members.
// May return a DynamicAccessListError if the requested access list has an
// implicit member list and the underlying implementation does not have
+69 -38
View File
@@ -32,9 +32,9 @@ import (
"github.com/gravitational/teleport/api/types/header"
"github.com/gravitational/teleport/api/types/userloginstate"
"github.com/gravitational/teleport/api/utils"
"github.com/gravitational/teleport/lib/accesslists"
"github.com/gravitational/teleport/lib/modules"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/tlsca"
)
// AccessListsAndLockGetter is an interface for retrieving access lists and locks.
@@ -97,12 +97,11 @@ func (g *GeneratorConfig) CheckAndSetDefaults() error {
// Generator will generate a user login state from a user.
type Generator struct {
log *logrus.Entry
accessLists AccessListsAndLockGetter
access services.Access
usageEvents UsageEventsClient
memberChecker *services.AccessListMembershipChecker
clock clockwork.Clock
log *logrus.Entry
accessLists AccessListsAndLockGetter
access services.Access
usageEvents UsageEventsClient
clock clockwork.Clock
}
// NewGenerator creates a new user login state generator.
@@ -112,12 +111,11 @@ func NewGenerator(config GeneratorConfig) (*Generator, error) {
}
return &Generator{
log: config.Log,
accessLists: config.AccessLists,
access: config.Access,
usageEvents: config.UsageEvents,
memberChecker: services.NewAccessListMembershipChecker(config.Clock, config.AccessLists, config.Access),
clock: config.Clock,
log: config.Log,
accessLists: config.AccessLists,
access: config.Access,
usageEvents: config.UsageEvents,
clock: config.Clock,
}, nil
}
@@ -151,7 +149,8 @@ func (g *Generator) Generate(ctx context.Context, user types.User) (*userloginst
}
// Generate the user login state.
if err := g.addAccessListsToState(ctx, user, uls); err != nil {
inheritedRoles, inheritedTraits, err := g.addAccessListsToState(ctx, user, uls)
if err != nil {
return nil, trace.Wrap(err)
}
@@ -162,7 +161,7 @@ func (g *Generator) Generate(ctx context.Context, user types.User) (*userloginst
if g.usageEvents != nil {
// Emit the usage event metadata.
if err := g.emitUsageEvent(ctx, user, uls); err != nil {
if err := g.emitUsageEvent(ctx, user, uls, inheritedRoles, inheritedTraits); err != nil {
g.log.Debug("Error emitting usage event during user login state generation, skipping")
}
}
@@ -170,35 +169,54 @@ func (g *Generator) Generate(ctx context.Context, user types.User) (*userloginst
return uls, nil
}
// addAccessListsToState will added the user's applicable access lists to the user login state.
func (g *Generator) addAccessListsToState(ctx context.Context, user types.User, state *userloginstate.UserLoginState) error {
// addAccessListsToState will add the user's applicable access lists to the user login state,
// returning any inherited roles and traits.
func (g *Generator) addAccessListsToState(ctx context.Context, user types.User, state *userloginstate.UserLoginState) ([]string, map[string][]string, error) {
accessLists, err := g.accessLists.GetAccessLists(ctx)
if err != nil {
return trace.Wrap(err)
return nil, nil, trace.Wrap(err)
}
// Create an identity for testing membership to access lists.
identity := tlsca.Identity{
Username: user.GetName(),
Groups: user.GetRoles(),
Traits: user.GetTraits(),
UserType: user.GetUserType(),
}
var allInheritedRoles []string
allInheritedTraits := make(map[string][]string)
for _, accessList := range accessLists {
if err := services.IsAccessListOwner(identity, accessList); err == nil {
g.grantRolesAndTraits(identity, accessList.Spec.OwnerGrants, state)
// Grants are inherited if the user is a member of the access list, explicitly or via inheritance.
membershipKind, err := accesslists.IsAccessListMember(ctx, user, accessList, g.accessLists, g.accessLists, g.clock)
if err != nil {
g.log.WithError(err).Warn("checking access list membership")
}
if err := g.memberChecker.IsAccessListMember(ctx, identity, accessList); err == nil {
g.grantRolesAndTraits(identity, accessList.Spec.Grants, state)
if err == nil && membershipKind != accesslists.MembershipOrOwnershipTypeNone {
g.grantRolesAndTraits(accessList.Spec.Grants, state)
if membershipKind == accesslists.MembershipOrOwnershipTypeInherited {
allInheritedRoles = append(allInheritedRoles, accessList.Spec.Grants.Roles...)
for k, values := range accessList.Spec.Grants.Traits {
allInheritedTraits[k] = append(allInheritedTraits[k], values...)
}
}
}
// OwnerGrants are inherited if the user is an owner of the access list, explicitly or via inheritance.
ownershipType, err := accesslists.IsAccessListOwner(ctx, user, accessList, g.accessLists, g.accessLists, g.clock)
if err != nil {
g.log.WithError(err).Warn("checking access list ownership")
}
if err == nil && ownershipType != accesslists.MembershipOrOwnershipTypeNone {
g.grantRolesAndTraits(accessList.Spec.OwnerGrants, state)
if ownershipType == accesslists.MembershipOrOwnershipTypeInherited {
allInheritedRoles = append(allInheritedRoles, accessList.Spec.OwnerGrants.Roles...)
for k, values := range accessList.Spec.OwnerGrants.Traits {
allInheritedTraits[k] = append(allInheritedTraits[k], values...)
}
}
}
}
return nil
return allInheritedRoles, allInheritedTraits, nil
}
func (g *Generator) grantRolesAndTraits(identity tlsca.Identity, grants accesslist.Grants, state *userloginstate.UserLoginState) {
// grantRolesAndTraits will append the roles and traits from the provided Grants to the UserLoginState,
// returning inherited roles and traits if membershipOrOwnershipType is inherited.
func (g *Generator) grantRolesAndTraits(grants accesslist.Grants, state *userloginstate.UserLoginState) {
state.Spec.Roles = append(state.Spec.Roles, grants.Roles...)
if state.Spec.Traits == nil && len(grants.Traits) > 0 {
@@ -214,7 +232,6 @@ func (g *Generator) grantRolesAndTraits(identity tlsca.Identity, grants accessli
func (g *Generator) postProcess(ctx context.Context, state *userloginstate.UserLoginState) error {
// Deduplicate roles and traits
state.Spec.Roles = utils.Deduplicate(state.Spec.Roles)
for k, v := range state.Spec.Traits {
state.Spec.Traits[k] = utils.Deduplicate(v)
}
@@ -225,6 +242,7 @@ func (g *Generator) postProcess(ctx context.Context, state *userloginstate.UserL
}
// Make sure all the roles exist. If they don't, error out.
// Since InheritedRoles are always a subset of Roles, we don't need to check them.
var existingRoles []string
for _, role := range state.Spec.Roles {
_, err := g.access.GetRole(ctx, role)
@@ -240,7 +258,7 @@ func (g *Generator) postProcess(ctx context.Context, state *userloginstate.UserL
}
// emitUsageEvent will emit the usage event for user state generation.
func (g *Generator) emitUsageEvent(ctx context.Context, user types.User, state *userloginstate.UserLoginState) error {
func (g *Generator) emitUsageEvent(ctx context.Context, user types.User, state *userloginstate.UserLoginState, inheritedRoles []string, inheritedTraits map[string][]string) error {
staticRoleCount := len(user.GetRoles())
staticTraitCount := 0
for _, values := range user.GetTraits() {
@@ -253,17 +271,30 @@ func (g *Generator) emitUsageEvent(ctx context.Context, user types.User, state *
stateTraitCount += len(values)
}
inheritedRoles = utils.Deduplicate(inheritedRoles)
for k, v := range inheritedTraits {
inheritedTraits[k] = utils.Deduplicate(v)
}
countInheritedRolesGranted := len(inheritedRoles)
countInheritedTraitsGranted := 0
for _, values := range inheritedTraits {
countInheritedTraitsGranted += len(values)
}
countRolesGranted := stateRoleCount - staticRoleCount
countTraitsGranted := stateTraitCount - staticTraitCount
// No roles or traits were granted, so skip emitting the event.
if countRolesGranted == 0 && countTraitsGranted == 0 {
// No roles or traits were granted or inherited, so skip emitting the event.
if countRolesGranted+countTraitsGranted+countInheritedRolesGranted+countInheritedTraitsGranted == 0 {
return nil
}
grantsToUser := &usageeventsv1.AccessListGrantsToUser{
CountRolesGranted: int32(countRolesGranted),
CountTraitsGranted: int32(countTraitsGranted),
CountRolesGranted: int32(countRolesGranted),
CountTraitsGranted: int32(countTraitsGranted),
CountInheritedRolesGranted: int32(countInheritedRolesGranted),
CountInheritedTraitsGranted: int32(countInheritedTraitsGranted),
}
if err := g.usageEvents.SubmitUsageEvent(ctx, &proto.SubmitUsageEventRequest{
+259 -42
View File
@@ -57,6 +57,7 @@ func TestAccessLists(t *testing.T) {
})
user, err := types.NewUser("user")
require.NoError(t, err)
user.SetStaticLabels(map[string]string{
"label1": "value1",
"label2": "value2",
@@ -65,24 +66,25 @@ func TestAccessLists(t *testing.T) {
user.SetTraits(map[string][]string{
"otrait1": {"value1", "value2"},
})
require.NoError(t, err)
userNoRolesOrTraits, err := types.NewUser("user")
require.NoError(t, err)
clock := clockwork.NewFakeClock()
tests := []struct {
name string
user types.User
cloud bool
accessLists []*accesslist.AccessList
members []*accesslist.AccessListMember
locks []types.Lock
roles []string
wantErr require.ErrorAssertionFunc
expected *userloginstate.UserLoginState
expectedRoleCount int
expectedTraitCount int
name string
user types.User
cloud bool
accessLists []*accesslist.AccessList
members []*accesslist.AccessListMember
locks []types.Lock
roles []string
wantErr require.ErrorAssertionFunc
expected *userloginstate.UserLoginState
expectedRoleCount int
expectedTraitCount int
expectedInheritedRoleCount int
expectedInheritedTraitCount int
}{
{
name: "access lists are empty",
@@ -98,9 +100,12 @@ func TestAccessLists(t *testing.T) {
[]string{"orole1"},
trait.Traits{"otrait1": {"value1", "value2"}},
[]string{"orole1"},
trait.Traits{"otrait1": {"value1", "value2"}}),
expectedRoleCount: 0,
expectedTraitCount: 0,
trait.Traits{"otrait1": {"value1", "value2"}},
),
expectedRoleCount: 0,
expectedTraitCount: 0,
expectedInheritedRoleCount: 0,
expectedInheritedTraitCount: 0,
},
{
name: "access lists add roles and traits",
@@ -126,9 +131,12 @@ func TestAccessLists(t *testing.T) {
[]string{"orole1"},
trait.Traits{"otrait1": {"value1", "value2"}},
[]string{"orole1", "role1", "role2"},
trait.Traits{"otrait1": {"value1", "value2"}, "trait1": {"value1", "value2"}, "trait2": {"value3"}}),
expectedRoleCount: 2,
expectedTraitCount: 3,
trait.Traits{"otrait1": {"value1", "value2"}, "trait1": {"value1", "value2"}, "trait2": {"value3"}},
),
expectedRoleCount: 2,
expectedTraitCount: 3,
expectedInheritedRoleCount: 0,
expectedInheritedTraitCount: 0,
},
{
name: "lock prevents adding roles and traits",
@@ -157,9 +165,12 @@ func TestAccessLists(t *testing.T) {
[]string{"orole1"},
trait.Traits{"otrait1": {"value1", "value2"}},
[]string{"orole1"},
trait.Traits{"otrait1": []string{"value1", "value2"}}),
expectedRoleCount: 0,
expectedTraitCount: 0,
trait.Traits{"otrait1": []string{"value1", "value2"}},
),
expectedRoleCount: 0,
expectedTraitCount: 0,
expectedInheritedRoleCount: 0,
expectedInheritedTraitCount: 0,
},
{
name: "access lists add member roles and traits (cloud disabled)",
@@ -185,9 +196,12 @@ func TestAccessLists(t *testing.T) {
[]string{"orole1"},
trait.Traits{"otrait1": {"value1", "value2"}},
[]string{"orole1", "role1", "role2"},
trait.Traits{"otrait1": {"value1", "value2"}, "trait1": {"value1", "value2"}, "trait2": {"value3"}}),
expectedRoleCount: 0,
expectedTraitCount: 0,
trait.Traits{"otrait1": {"value1", "value2"}, "trait1": {"value1", "value2"}, "trait2": {"value3"}},
),
expectedRoleCount: 0,
expectedTraitCount: 0,
expectedInheritedRoleCount: 0,
expectedInheritedTraitCount: 0,
},
{
name: "access lists add owner roles and traits",
@@ -212,9 +226,12 @@ func TestAccessLists(t *testing.T) {
[]string{"orole1"},
trait.Traits{"otrait1": {"value1", "value2"}},
[]string{"orole1", "owner-role1", "owner-role2"},
trait.Traits{"otrait1": {"value1", "value2"}, "owner-trait1": {"owner-value1"}}),
expectedRoleCount: 2,
expectedTraitCount: 1,
trait.Traits{"otrait1": {"value1", "value2"}, "owner-trait1": {"owner-value1"}},
),
expectedRoleCount: 2,
expectedTraitCount: 1,
expectedInheritedRoleCount: 0,
expectedInheritedTraitCount: 0,
},
{
name: "access lists add owner and member roles and traits",
@@ -239,9 +256,12 @@ func TestAccessLists(t *testing.T) {
[]string{"orole1"},
trait.Traits{"otrait1": {"value1", "value2"}},
[]string{"orole1", "owner-role1", "owner-role2", "role1"},
trait.Traits{"otrait1": {"value1", "value2"}, "trait1": {"owner-value1", "value1"}}),
expectedRoleCount: 3,
expectedTraitCount: 2,
trait.Traits{"otrait1": {"value1", "value2"}, "trait1": {"owner-value1", "value1"}},
),
expectedRoleCount: 3,
expectedTraitCount: 2,
expectedInheritedRoleCount: 0,
expectedInheritedTraitCount: 0,
},
{
name: "access lists add member roles and traits, roles missing from backend",
@@ -287,9 +307,13 @@ func TestAccessLists(t *testing.T) {
trait.Traits{"otrait1": {"value1", "value2"}},
[]string{"orole1", "role1"},
trait.Traits{
"otrait1": {"value1", "value2"}, "trait1": {"value1"}}),
expectedRoleCount: 1,
expectedTraitCount: 1,
"otrait1": {"value1", "value2"}, "trait1": {"value1"},
},
),
expectedRoleCount: 1,
expectedTraitCount: 1,
expectedInheritedRoleCount: 0,
expectedInheritedTraitCount: 0,
},
{
name: "access lists add roles with duplicates",
@@ -310,9 +334,12 @@ func TestAccessLists(t *testing.T) {
[]string{"orole1"},
trait.Traits{"otrait1": {"value1", "value2"}},
[]string{"orole1", "role1", "role2", "role3"},
trait.Traits{"otrait1": {"value1", "value2"}}),
expectedRoleCount: 3,
expectedTraitCount: 0,
trait.Traits{"otrait1": {"value1", "value2"}},
),
expectedRoleCount: 3,
expectedTraitCount: 0,
expectedInheritedRoleCount: 0,
expectedInheritedTraitCount: 0,
},
{
name: "access lists add traits with duplicates",
@@ -343,9 +370,12 @@ func TestAccessLists(t *testing.T) {
[]string{"orole1"},
trait.Traits{"otrait1": {"value1", "value2"}},
[]string{"orole1"},
trait.Traits{"otrait1": {"value1", "value2"}, "trait1": {"value1", "value2"}, "trait2": {"value3", "value4", "value1"}, "trait3": {"value5", "value6"}}),
expectedRoleCount: 0,
expectedTraitCount: 7,
trait.Traits{"otrait1": {"value1", "value2"}, "trait1": {"value1", "value2"}, "trait2": {"value3", "value4", "value1"}, "trait3": {"value5", "value6"}},
),
expectedRoleCount: 0,
expectedTraitCount: 7,
expectedInheritedRoleCount: 0,
expectedInheritedTraitCount: 0,
},
{
name: "access lists add traits with no roles or traits in original",
@@ -375,9 +405,148 @@ func TestAccessLists(t *testing.T) {
trait.Traits{
"trait1": {"value1", "value2"},
"trait2": {"value3", "value4"},
"trait3": {"value5", "value6"}}),
expectedRoleCount: 1,
expectedTraitCount: 6,
"trait3": {"value5", "value6"},
},
),
expectedRoleCount: 1,
expectedTraitCount: 6,
expectedInheritedRoleCount: 0,
expectedInheritedTraitCount: 0,
},
{
name: "access lists member of nested list",
cloud: true,
user: userNoRolesOrTraits,
// user is member of acl 3, acl 1 includes acl 2, which includes 3
// so user will be granted role1 and 2, and trait1
accessLists: []*accesslist.AccessList{
newAccessList(t, clock, "1", grants([]string{"role1"},
trait.Traits{
"trait1": {"value"},
}),
emptyGrants),
newAccessList(t, clock, "2", grants([]string{"role1"}, trait.Traits{}),
emptyGrants),
newAccessList(t, clock, "3", grants([]string{"role2"}, trait.Traits{}), emptyGrants),
},
members: append(
newAccessListMembers(t, clock, "3", "user"),
newAccessListMemberWithKind(t, clock, "2", accesslist.MembershipKindList, "3"),
newAccessListMemberWithKind(t, clock, "1", accesslist.MembershipKindList, "2")),
roles: []string{"role1", "role2"},
wantErr: require.NoError,
expected: newUserLoginState(t, "user",
nil,
nil,
nil,
[]string{"role1", "role2"},
trait.Traits{"trait1": {"value"}},
),
expectedRoleCount: 2,
expectedTraitCount: 1,
expectedInheritedRoleCount: 1,
expectedInheritedTraitCount: 1,
},
{
name: "access lists member of nested list",
cloud: true,
user: userNoRolesOrTraits,
// user is member of acl 3, acl 1 includes acl 2, which includes 3
// so user will be granted role1 and 2, and trait1
accessLists: []*accesslist.AccessList{
newAccessList(t, clock, "1", grants([]string{"role1"},
trait.Traits{
"trait1": {"value"},
}),
emptyGrants),
newAccessList(t, clock, "2", grants([]string{"role1"}, trait.Traits{}),
emptyGrants),
newAccessList(t, clock, "3", grants([]string{"role2"}, trait.Traits{}), emptyGrants),
},
members: append(
newAccessListMembers(t, clock, "3", "user"),
newAccessListMemberWithKind(t, clock, "2", accesslist.MembershipKindList, "3"),
newAccessListMemberWithKind(t, clock, "1", accesslist.MembershipKindList, "2")),
roles: []string{"role1", "role2"},
wantErr: require.NoError,
expected: newUserLoginState(t, "user",
nil,
nil,
nil,
[]string{"role1", "role2"},
trait.Traits{"trait1": {"value"}},
),
expectedRoleCount: 2,
expectedTraitCount: 1,
expectedInheritedRoleCount: 1,
expectedInheritedTraitCount: 1,
},
{
name: "access lists member of nested list, in diamond formation",
cloud: true,
user: userNoRolesOrTraits,
// user is member of acl 1, acl 2 and 3 include acl 1, acl 4 includes acls 2 and 3
// so user will be granted {trait: [1,2,3,4]}
accessLists: []*accesslist.AccessList{
newAccessList(t, clock, "1", grants([]string{}, trait.Traits{"trait": {"1"}}), emptyGrants),
newAccessList(t, clock, "2", grants([]string{}, trait.Traits{"trait": {"2"}}), emptyGrants),
newAccessList(t, clock, "3", grants([]string{}, trait.Traits{"trait": {"3"}}), emptyGrants),
newAccessList(t, clock, "4", grants([]string{}, trait.Traits{"trait": {"4"}}), emptyGrants),
},
members: append(
newAccessListMembers(t, clock, "1", "user"),
newAccessListMemberWithKind(t, clock, "2", accesslist.MembershipKindList, "1"),
newAccessListMemberWithKind(t, clock, "3", accesslist.MembershipKindList, "1"),
newAccessListMemberWithKind(t, clock, "4", accesslist.MembershipKindList, "3"),
newAccessListMemberWithKind(t, clock, "4", accesslist.MembershipKindList, "2"),
),
roles: nil,
wantErr: require.NoError,
expected: newUserLoginState(t, "user",
nil,
nil,
nil,
nil,
trait.Traits{"trait": {"1", "2", "3", "4"}},
),
expectedRoleCount: 0,
expectedTraitCount: 4,
expectedInheritedRoleCount: 0,
// trait 1 is directly granted to user via acl 1; it is not inherited
expectedInheritedTraitCount: 3,
},
{
name: "members in nested access lists inherit parent's owner grants",
cloud: true,
user: userNoRolesOrTraits,
// user is member of acl 1, acl 3, includes members of acl 2, which includes members of acl 1 as owners
accessLists: []*accesslist.AccessList{
newAccessList(t, clock, "1", emptyGrants, grants([]string{"oroleA"}, trait.Traits{"okey": {"oval1"}})),
newAccessList(t, clock, "2", emptyGrants, grants([]string{"oroleB"}, trait.Traits{"okey": {"oval2"}})),
newAccessListWithOwners(t, clock, "3", emptyGrants, grants([]string{"oroleC"}, trait.Traits{"okey": {"oval3"}}), []accesslist.Owner{{
Name: "2",
Description: "hello",
MembershipKind: accesslist.MembershipKindList},
}),
},
members: append(
newAccessListMembers(t, clock, "1", "user"),
newAccessListMemberWithKind(t, clock, "2", accesslist.MembershipKindList, "1"),
newAccessListMemberWithKind(t, clock, "3", accesslist.MembershipKindList, "2"),
),
roles: []string{"oroleA", "oroleB", "oroleC"},
wantErr: require.NoError,
expected: newUserLoginState(t, "user",
nil,
nil,
nil,
[]string{"oroleC"},
trait.Traits{"okey": {"oval3"}},
),
expectedRoleCount: 1,
expectedTraitCount: 1,
expectedInheritedRoleCount: 1,
expectedInheritedTraitCount: 1,
},
}
@@ -438,6 +607,8 @@ func TestAccessLists(t *testing.T) {
require.Equal(t, test.expectedRoleCount, int(event.AccessListGrantsToUser.CountRolesGranted))
require.Equal(t, test.expectedTraitCount, int(event.AccessListGrantsToUser.CountTraitsGranted))
require.Equal(t, test.expectedInheritedRoleCount, int(event.AccessListGrantsToUser.CountInheritedRolesGranted))
require.Equal(t, test.expectedInheritedTraitCount, int(event.AccessListGrantsToUser.CountInheritedTraitsGranted))
}
})
}
@@ -521,6 +692,33 @@ func newAccessList(t *testing.T, clock clockwork.Clock, name string, grants acce
return accessList
}
func newAccessListWithOwners(t *testing.T, clock clockwork.Clock, name string, grants accesslist.Grants, ownerGrants accesslist.Grants, owners []accesslist.Owner) *accesslist.AccessList {
t.Helper()
accessList, err := accesslist.NewAccessList(header.Metadata{
Name: name,
}, accesslist.Spec{
Title: "title",
Audit: accesslist.Audit{
NextAuditDate: clock.Now().Add(time.Hour * 48),
},
Owners: owners,
OwnershipRequires: accesslist.Requires{
Roles: []string{},
Traits: map[string][]string{},
},
MembershipRequires: accesslist.Requires{
Roles: []string{},
Traits: map[string][]string{},
},
Grants: grants,
OwnerGrants: ownerGrants,
})
require.NoError(t, err)
return accessList
}
func newAccessListMembers(t *testing.T, clock clockwork.Clock, accessList string, members ...string) []*accesslist.AccessListMember {
alMembers := make([]*accesslist.AccessListMember, len(members))
for i, member := range members {
@@ -541,6 +739,25 @@ func newAccessListMembers(t *testing.T, clock clockwork.Clock, accessList string
return alMembers
}
func newAccessListMemberWithKind(t *testing.T, clock clockwork.Clock, accessList string, kind string, member string) *accesslist.AccessListMember {
var err error
res, err := accesslist.NewAccessListMember(header.Metadata{
Name: member,
}, accesslist.AccessListMemberSpec{
AccessList: accessList,
Name: member,
Joined: clock.Now(),
Expires: clock.Now().Add(24 * time.Hour),
Reason: "added",
AddedBy: ownerUser,
MembershipKind: kind,
})
require.NoError(t, err)
return res
}
func newUserLoginState(t *testing.T, name string, labels map[string]string, originalRoles []string, originalTraits map[string][]string,
roles []string, traits map[string][]string) *userloginstate.UserLoginState {
t.Helper()
+2 -1
View File
@@ -1232,7 +1232,8 @@ func definitionForBuiltinRole(clusterName string, recConfig readonly.SessionReco
types.NewRule(types.KindRole, services.RO()),
types.NewRule(types.KindLock, services.RW()),
types.NewRule(types.KindSAML, services.ReadNoSecrets()),
// Okta can manage access lists and roles it creates.
types.NewRule(types.KindAccessList, services.RO()),
// Okta can read/write access lists and roles it creates.
{
Resources: []string{types.KindRole},
Verbs: services.RW(),
+2 -2
View File
@@ -3327,13 +3327,13 @@ func (c *Cache) GetAccessList(ctx context.Context, name string) (*accesslist.Acc
}
// CountAccessListMembers will count all access list members.
func (c *Cache) CountAccessListMembers(ctx context.Context, accessListName string) (uint32, error) {
func (c *Cache) CountAccessListMembers(ctx context.Context, accessListName string) (uint32, uint32, error) {
ctx, span := c.Tracer.Start(ctx, "cache/CountAccessListMembers")
defer span.End()
rg, err := readCollectionCache(c, c.collections.accessListMembers)
if err != nil {
return 0, trace.Wrap(err)
return 0, 0, trace.Wrap(err)
}
defer rg.Release()
return rg.reader.CountAccessListMembers(ctx, accessListName)
+4 -3
View File
@@ -2586,16 +2586,17 @@ func TestAccessListMembers(t *testing.T) {
require.NoError(t, err)
}
count, err := p.accessLists.CountAccessListMembers(ctx, al.GetName())
count, listCount, err := p.accessLists.CountAccessListMembers(ctx, al.GetName())
require.NoError(t, err)
require.Equal(t, uint32(40), count)
require.Equal(t, uint32(0), listCount)
// Eventually, this should be reflected in the cache.
require.Eventually(t, func() bool {
// Make sure the cache has a single resource in it.
count, err := p.cache.CountAccessListMembers(ctx, al.GetName())
count, listCount, err := p.cache.CountAccessListMembers(ctx, al.GetName())
assert.NoError(t, err)
return count == uint32(40)
return count == uint32(40) && listCount == uint32(0)
}, time.Second*2, time.Millisecond*250)
}
+5 -5
View File
@@ -3175,7 +3175,7 @@ func (accessListExecutor) getAll(ctx context.Context, cache *Cache, loadSecrets
}
func (accessListExecutor) upsert(ctx context.Context, cache *Cache, resource *accesslist.AccessList) error {
_, err := cache.accessListCache.UpsertAccessList(ctx, resource)
_, err := cache.accessListCache.UnconditionalUpsertAccessList(ctx, resource)
return trace.Wrap(err)
}
@@ -3184,7 +3184,7 @@ func (accessListExecutor) deleteAll(ctx context.Context, cache *Cache) error {
}
func (accessListExecutor) delete(ctx context.Context, cache *Cache, resource types.Resource) error {
return cache.accessListCache.DeleteAccessList(ctx, resource.GetName())
return cache.accessListCache.UnconditionalDeleteAccessList(ctx, resource.GetName())
}
func (accessListExecutor) isSingleton() bool { return false }
@@ -3225,7 +3225,7 @@ func (accessListMemberExecutor) getAll(ctx context.Context, cache *Cache, loadSe
}
func (accessListMemberExecutor) upsert(ctx context.Context, cache *Cache, resource *accesslist.AccessListMember) error {
_, err := cache.accessListCache.UpsertAccessListMember(ctx, resource)
_, err := cache.accessListCache.UnconditionalUpsertAccessListMember(ctx, resource)
return trace.Wrap(err)
}
@@ -3234,7 +3234,7 @@ func (accessListMemberExecutor) deleteAll(ctx context.Context, cache *Cache) err
}
func (accessListMemberExecutor) delete(ctx context.Context, cache *Cache, resource types.Resource) error {
return cache.accessListCache.DeleteAccessListMember(ctx,
return cache.accessListCache.UnconditionalDeleteAccessListMember(ctx,
resource.GetMetadata().Description, // Cache passes access ID via description field.
resource.GetName())
}
@@ -3249,7 +3249,7 @@ func (accessListMemberExecutor) getReader(cache *Cache, cacheOK bool) accessList
}
type accessListMembersGetter interface {
CountAccessListMembers(ctx context.Context, accessListName string) (uint32, error)
CountAccessListMembers(ctx context.Context, accessListName string) (uint32, uint32, error)
ListAccessListMembers(ctx context.Context, accessListName string, pageSize int, nextToken string) ([]*accesslist.AccessListMember, string, error)
GetAccessListMember(ctx context.Context, accessList string, memberName string) (*accesslist.AccessListMember, error)
ListAllAccessListMembers(ctx context.Context, pageSize int, pageToken string) ([]*accesslist.AccessListMember, string, error)
+10 -3
View File
@@ -236,6 +236,9 @@ type AccessResourcesGetter interface {
ListAccessLists(context.Context, int, string) ([]*accesslist.AccessList, string, error)
ListResources(ctx context.Context, req proto.ListResourcesRequest) (*types.ListResourcesResponse, error)
GetAccessList(context.Context, string) (*accesslist.AccessList, error)
GetAccessLists(ctx context.Context) ([]*accesslist.AccessList, error)
ListAccessListMembers(ctx context.Context, accessList string, pageSize int, pageToken string) (members []*accesslist.AccessListMember, nextToken string, err error)
GetAccessListMember(ctx context.Context, accessList string, memberName string) (*accesslist.AccessListMember, error)
@@ -257,8 +260,12 @@ type AccessListSuggestionClient interface {
type RoleGetter interface {
GetRole(ctx context.Context, name string) (types.Role, error)
}
type AccessListGetter interface {
type AccessListAndMembersGetter interface {
GetAccessList(ctx context.Context, name string) (*accesslist.AccessList, error)
GetAccessLists(ctx context.Context) ([]*accesslist.AccessList, error)
GetAccessListMember(ctx context.Context, accessList string, memberName string) (*accesslist.AccessListMember, error)
ListAccessListMembers(ctx context.Context, accessListName string, pageSize int, pageToken string) (members []*accesslist.AccessListMember, nextToken string, err error)
}
// Modules defines interface that external libraries can implement customizing
@@ -283,7 +290,7 @@ type Modules interface {
// GenerateAccessRequestPromotions generates a list of valid promotions for given access request.
GenerateAccessRequestPromotions(context.Context, AccessResourcesGetter, types.AccessRequest) (*types.AccessRequestAllowedPromotions, error)
// GetSuggestedAccessLists generates a list of valid promotions for given access request.
GetSuggestedAccessLists(ctx context.Context, identity *tlsca.Identity, clt AccessListSuggestionClient, accessListGetter AccessListGetter, requestID string) ([]*accesslist.AccessList, error)
GetSuggestedAccessLists(ctx context.Context, identity *tlsca.Identity, clt AccessListSuggestionClient, accessListGetter AccessListAndMembersGetter, requestID string) ([]*accesslist.AccessList, error)
// EnableRecoveryCodes enables the usage of recovery codes for resetting forgotten passwords
EnableRecoveryCodes()
// EnablePlugins enables the hosted plugins runtime
@@ -427,7 +434,7 @@ func (p *defaultModules) GenerateAccessRequestPromotions(_ context.Context, _ Ac
}
func (p *defaultModules) GetSuggestedAccessLists(ctx context.Context, identity *tlsca.Identity, clt AccessListSuggestionClient,
accessListGetter AccessListGetter, requestID string,
accessListGetter AccessListAndMembersGetter, requestID string,
) ([]*accesslist.AccessList, error) {
return nil, trace.NotImplemented("GetSuggestedAccessLists not implemented")
}
+9 -138
View File
@@ -20,17 +20,13 @@ package services
import (
"context"
"slices"
"time"
"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"
accesslistclient "github.com/gravitational/teleport/api/client/accesslist"
accesslistv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/accesslist/v1"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/types/accesslist"
"github.com/gravitational/teleport/lib/tlsca"
"github.com/gravitational/teleport/lib/utils"
)
@@ -48,6 +44,8 @@ type AccessListsGetter interface {
GetAccessList(context.Context, string) (*accesslist.AccessList, error)
// GetAccessListsToReview returns access lists that the user needs to review.
GetAccessListsToReview(context.Context) ([]*accesslist.AccessList, error)
// GetInheritedGrants returns grants inherited by access list accessListID from parent access lists.
GetInheritedGrants(context.Context, string) (*accesslist.Grants, error)
}
// AccessListsSuggestionsGetter defines an interface for reading access lists suggestions.
@@ -140,6 +138,10 @@ type AccessListMemberGetter interface {
// implicit member list and the underlying implementation does not have
// enough information to compute the dynamic member record.
GetAccessListMember(ctx context.Context, accessList string, memberName string) (*accesslist.AccessListMember, error)
// GetAccessList returns the specified access list resource.
GetAccessList(context.Context, string) (*accesslist.AccessList, error)
// GetAccessLists returns a list of all access lists.
GetAccessLists(context.Context) ([]*accesslist.AccessList, error)
}
// AccessListMembersGetter defines an interface for reading access list members.
@@ -147,7 +149,7 @@ type AccessListMembersGetter interface {
AccessListMemberGetter
// CountAccessListMembers will count all access list members.
CountAccessListMembers(ctx context.Context, accessListName string) (uint32, error)
CountAccessListMembers(ctx context.Context, accessListName string) (membersCount uint32, listCount uint32, err error)
// ListAccessListMembers returns a paginated list of all access list members.
// May return a DynamicAccessListError if the requested access list has an
// implicit member list and the underlying implementation does not have
@@ -156,6 +158,8 @@ type AccessListMembersGetter interface {
// ListAllAccessListMembers returns a paginated list of all access list members for all access lists.
ListAllAccessListMembers(ctx context.Context, pageSize int, pageToken string) (members []*accesslist.AccessListMember, nextToken string, err error)
GetAccessListMember(ctx context.Context, accessList string, memberName string) (*accesslist.AccessListMember, error)
// GetAccessListOwners returns a list of all owners in an Access List, including those inherited from nested Access Lists.
GetAccessListOwners(ctx context.Context, accessList string) ([]*accesslist.Owner, error)
}
// AccessListMembers defines an interface for managing AccessListMembers.
@@ -218,139 +222,6 @@ func UnmarshalAccessListMember(data []byte, opts ...MarshalOption) (*accesslist.
return &member, nil
}
// IsAccessListOwner will return true if the user is an owner for the current list.
func IsAccessListOwner(identity tlsca.Identity, accessList *accesslist.AccessList) error {
// An opaque access denied error.
accessDenied := trace.AccessDenied("access denied")
// Is the supplied identity in the owners list?
ownerIdx := slices.IndexFunc(accessList.GetOwners(), func(owner accesslist.Owner) bool {
return owner.Name == identity.Username
})
if ownerIdx == -1 {
return accessDenied
}
// Does the supplied Identity meet the ownership requirements?
if !UserMeetsRequirements(identity, accessList.Spec.OwnershipRequires) {
return accessDenied
}
// We've gotten through all the checks, so the user is an owner.
return nil
}
// AccessListMembershipChecker will check if users are members of an access list and
// makes sure the user is not locked and meets membership requirements.
type AccessListMembershipChecker struct {
members AccessListMemberGetter
locks LockGetter
clock clockwork.Clock
}
// NewAccessListMembershipChecker will create a new access list membership checker.
func NewAccessListMembershipChecker(clock clockwork.Clock, members AccessListMemberGetter, locks LockGetter) *AccessListMembershipChecker {
return &AccessListMembershipChecker{
members: members,
locks: locks,
clock: clock,
}
}
// IsAccessListMember will return true if the user is a member for the current list.
func (a AccessListMembershipChecker) IsAccessListMember(ctx context.Context, identity tlsca.Identity, accessList *accesslist.AccessList) error {
username := identity.Username
// Allow for nil locks while we transition away from using `IsAccessListMember` outside of this struct.
if a.locks != nil {
locks, err := a.locks.GetLocks(ctx, true, types.LockTarget{
User: username,
})
if err != nil {
return trace.Wrap(err)
}
if len(locks) > 0 {
return trace.AccessDenied("user %s is currently locked", username)
}
}
member, err := a.members.GetAccessListMember(ctx, accessList.GetName(), username)
if trace.IsNotFound(err) {
// The member has not been found, so we know they're not a member of this list.
return trace.NotFound("user %s is not a member of the access list", username)
} else if err != nil {
// Some other error has occurred
return trace.Wrap(err)
}
expires := member.Spec.Expires
if !expires.IsZero() && !a.clock.Now().Before(expires) {
return trace.AccessDenied("user %s's membership has expired in the access list", username)
}
if !UserMeetsRequirements(identity, accessList.Spec.MembershipRequires) {
return trace.AccessDenied("user %s is a member, but does not have the roles or traits required to be a member of this list", username)
}
return nil
}
// TODO(mdwn): Remove this in favor of using the access list membership checker.
func IsAccessListMember(ctx context.Context, identity tlsca.Identity, clock clockwork.Clock, accessList *accesslist.AccessList, members AccessListMemberGetter) error {
// See if the member getter also implements lock getter. If so, use it. Otherwise, nil is fine.
lockGetter, _ := members.(LockGetter)
return AccessListMembershipChecker{
members: members,
locks: lockGetter,
clock: clock,
}.IsAccessListMember(ctx, identity, accessList)
}
// UserMeetsRequirements will return true if the user meets the requirements for the access list.
func UserMeetsRequirements(identity tlsca.Identity, requires accesslist.Requires) bool {
// Assemble the user's roles for easy look up.
userRolesMap := map[string]struct{}{}
for _, role := range identity.Groups {
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.Traits {
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
}
// AccessListReviews defines an interface for managing Access List reviews.
type AccessListReviews interface {
// ListAccessListReviews will list access list reviews for a particular access list.
-414
View File
@@ -19,30 +19,18 @@
package services
import (
"context"
"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"
"github.com/gravitational/teleport/api/types/trait"
"github.com/gravitational/teleport/lib/tlsca"
"github.com/gravitational/teleport/lib/utils"
)
const (
ownerUser = "owner-user"
member1 = "member1"
member2 = "member2"
member3 = "member3"
member4 = "member4"
)
// TestAccessListUnmarshal verifies an access list resource can be unmarshaled.
func TestAccessListUnmarshal(t *testing.T) {
expected, err := accesslist.NewAccessList(
@@ -197,304 +185,11 @@ func TestAccessListMemberMarshal(t *testing.T) {
require.Equal(t, expected, actual)
}
func TestIsAccessListOwner(t *testing.T) {
tests := []struct {
name string
identity tlsca.Identity
errAssertionFunc require.ErrorAssertionFunc
}{
{
name: "is owner",
identity: tlsca.Identity{
Username: ownerUser,
Groups: []string{"orole1", "orole2"},
Traits: map[string][]string{
"otrait1": {"ovalue1", "ovalue2"},
"otrait2": {"ovalue3", "ovalue4"},
},
},
errAssertionFunc: require.NoError,
},
{
name: "is not an owner",
identity: tlsca.Identity{
Username: "not-owner",
Groups: []string{"orole1", "orole2"},
Traits: map[string][]string{
"otrait1": {"ovalue1", "ovalue2"},
"otrait2": {"ovalue3", "ovalue4"},
},
},
errAssertionFunc: requireAccessDenied,
},
{
name: "is owner with missing roles",
identity: tlsca.Identity{
Username: "not-owner",
Groups: []string{"orole1"},
Traits: map[string][]string{
"otrait1": {"ovalue1", "ovalue2"},
"otrait2": {"ovalue3", "ovalue4"},
},
},
errAssertionFunc: requireAccessDenied,
},
{
name: "is owner with missing traits",
identity: tlsca.Identity{
Username: "not-owner",
Groups: []string{"orole1", "orole2"},
Traits: map[string][]string{
"otrait1": {"ovalue1"},
"otrait2": {"ovalue3"},
},
},
errAssertionFunc: requireAccessDenied,
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
accessList := newAccessList(t)
test.errAssertionFunc(t, IsAccessListOwner(test.identity, accessList))
})
}
}
// testMembersAndLockGetter implements AccessListMembersGetter and LockGetter for testing.
type testMembersAndLockGetter struct {
members map[string]map[string]*accesslist.AccessListMember
locks map[string]types.Lock
}
// ListAccessListMembers returns a paginated list of all access list members.
func (t *testMembersAndLockGetter) ListAccessListMembers(ctx context.Context, accessList string, _ int, _ string) (members []*accesslist.AccessListMember, nextToken string, err error) {
for _, member := range t.members[accessList] {
members = append(members, member)
}
return members, "", nil
}
// ListAllAccessListMembers returns a paginated list of all access list members for all access lists.
func (t *testMembersAndLockGetter) ListAllAccessListMembers(ctx context.Context, pageSize int, pageToken string) ([]*accesslist.AccessListMember, string, error) {
var allMembers []*accesslist.AccessListMember
for _, members := range t.members {
for _, member := range members {
allMembers = append(allMembers, member)
}
}
return allMembers, "", nil
}
// GetAccessListMember returns the specified access list member resource.
func (t *testMembersAndLockGetter) GetAccessListMember(ctx context.Context, accessList string, memberName string) (*accesslist.AccessListMember, error) {
members, ok := t.members[accessList]
if !ok {
return nil, trace.NotFound("not found")
}
member, ok := members[memberName]
if !ok {
return nil, trace.NotFound("not found")
}
return member, nil
}
// GetLock gets a lock by name.
func (t *testMembersAndLockGetter) GetLock(_ context.Context, name string) (types.Lock, error) {
if t.locks == nil {
return nil, trace.NotFound("not found")
}
lock, ok := t.locks[name]
if !ok {
return nil, trace.NotFound("not found")
}
return lock, nil
}
// GetLocks gets all/in-force locks that match at least one of the targets when specified.
func (t *testMembersAndLockGetter) GetLocks(ctx context.Context, inForceOnly bool, targets ...types.LockTarget) ([]types.Lock, error) {
locks := make([]types.Lock, 0, len(t.locks))
for _, lock := range t.locks {
locks = append(locks, lock)
}
return locks, nil
}
func requireAccessDenied(t require.TestingT, err error, i ...interface{}) {
require.Error(t, err)
require.True(t, trace.IsAccessDenied(err), "expected AccessDenied, got %T: %s", err, err.Error())
}
func TestIsAccessListMemberChecker(t *testing.T) {
tests := []struct {
name string
identity tlsca.Identity
memberCtx context.Context
currentTime time.Time
locks map[string]types.Lock
errAssertionFunc require.ErrorAssertionFunc
}{
{
name: "is member",
identity: tlsca.Identity{
Username: member1,
Groups: []string{"mrole1", "mrole2"},
Traits: map[string][]string{
"mtrait1": {"mvalue1", "mvalue2"},
"mtrait2": {"mvalue3", "mvalue4"},
},
},
currentTime: time.Date(2023, 2, 1, 0, 0, 0, 0, time.UTC),
errAssertionFunc: require.NoError,
},
{
name: "is locked member",
identity: tlsca.Identity{
Username: member1,
Groups: []string{"mrole1", "mrole2"},
Traits: map[string][]string{
"mtrait1": {"mvalue1", "mvalue2"},
"mtrait2": {"mvalue3", "mvalue4"},
},
},
locks: map[string]types.Lock{
"test-lock": newUserLock(t, "test-lock", member1),
},
currentTime: time.Date(2023, 2, 1, 0, 0, 0, 0, time.UTC),
errAssertionFunc: func(t require.TestingT, err error, i ...interface{}) {
require.ErrorIs(t, err, trace.AccessDenied("user %s is currently locked", member1))
},
},
{
name: "is not a member",
identity: tlsca.Identity{
Username: member4,
Groups: []string{"mrole1", "mrole2"},
Traits: map[string][]string{
"mtrait1": {"mvalue1", "mvalue2"},
"mtrait2": {"mvalue3", "mvalue4"},
},
},
currentTime: time.Date(2023, 2, 1, 0, 0, 0, 0, time.UTC),
errAssertionFunc: func(t require.TestingT, err error, i ...interface{}) {
require.True(t, trace.IsNotFound(err))
},
},
{
name: "is expired member",
identity: tlsca.Identity{
Username: member2,
Groups: []string{"mrole1", "mrole2"},
Traits: map[string][]string{
"mtrait1": {"mvalue1", "mvalue2"},
"mtrait2": {"mvalue3", "mvalue4"},
},
},
currentTime: time.Date(2026, 7, 1, 0, 0, 0, 0, time.UTC),
errAssertionFunc: requireAccessDenied,
},
{
name: "member has no expiration",
identity: tlsca.Identity{
Username: member3,
Groups: []string{"mrole1", "mrole2"},
Traits: map[string][]string{
"mtrait1": {"mvalue1", "mvalue2"},
"mtrait2": {"mvalue3", "mvalue4"},
},
},
currentTime: time.Date(2030, 7, 1, 0, 0, 0, 0, time.UTC),
errAssertionFunc: require.NoError,
},
{
name: "is member with missing roles",
identity: tlsca.Identity{
Username: member1,
Groups: []string{"mrole1"},
Traits: map[string][]string{
"mtrait1": {"mvalue1", "mvalue2"},
"mtrait2": {"mvalue3", "mvalue4"},
},
},
currentTime: time.Date(2023, 2, 1, 0, 0, 0, 0, time.UTC),
errAssertionFunc: requireAccessDenied,
},
{
name: "is member with no expiration and missing roles",
identity: tlsca.Identity{
Username: member3,
Groups: []string{"mrole1"},
Traits: map[string][]string{
"mtrait1": {"mvalue1", "mvalue2"},
"mtrait2": {"mvalue3", "mvalue4"},
},
},
currentTime: time.Date(2023, 2, 1, 0, 0, 0, 0, time.UTC),
errAssertionFunc: requireAccessDenied,
},
{
name: "is member with missing traits",
identity: tlsca.Identity{
Username: member1,
Groups: []string{"mrole1", "mrole2"},
Traits: map[string][]string{
"mtrait1": {"mvalue1"},
"mtrait2": {"mvalue3"},
},
},
currentTime: time.Date(2023, 2, 1, 0, 0, 0, 0, time.UTC),
errAssertionFunc: requireAccessDenied,
},
{
name: "is member with no expiration and missing traits",
identity: tlsca.Identity{
Username: member3,
Groups: []string{"mrole1", "mrole2"},
Traits: map[string][]string{
"mtrait1": {"mvalue1"},
"mtrait2": {"mvalue3"},
},
},
currentTime: time.Date(2023, 2, 1, 0, 0, 0, 0, time.UTC),
errAssertionFunc: requireAccessDenied,
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
ctx := context.Background()
accessList := newAccessList(t)
members := newAccessListMembers(t)
memberMap := map[string]map[string]*accesslist.AccessListMember{}
for _, member := range members {
accessListName := member.Spec.AccessList
if _, ok := memberMap[accessListName]; !ok {
memberMap[accessListName] = map[string]*accesslist.AccessListMember{}
}
memberMap[accessListName][member.Spec.Name] = member
}
getter := &testMembersAndLockGetter{members: memberMap, locks: test.locks}
checker := NewAccessListMembershipChecker(clockwork.NewFakeClockAt(test.currentTime), getter, getter)
test.errAssertionFunc(t, checker.IsAccessListMember(ctx, test.identity, accessList))
})
}
}
// TestAccessListReviewUnmarshal verifies an access list review resource can be unmarshaled.
func TestAccessListReviewUnmarshal(t *testing.T) {
expected, err := accesslist.NewReview(
@@ -596,102 +291,6 @@ func TestAccessListReviewMarshal(t *testing.T) {
require.Equal(t, expected, actual)
}
func newAccessList(t *testing.T) *accesslist.AccessList {
t.Helper()
accessList, err := accesslist.NewAccessList(
header.Metadata{
Name: "test",
},
accesslist.Spec{
Title: "title",
Description: "test access list",
Owners: []accesslist.Owner{
{
Name: ownerUser,
Description: "owner user",
},
{
Name: "test-user2",
Description: "test user 2",
},
},
Audit: accesslist.Audit{
NextAuditDate: time.Date(2024, 6, 1, 0, 0, 0, 0, time.UTC),
Recurrence: accesslist.Recurrence{
Frequency: accesslist.ThreeMonths,
DayOfMonth: accesslist.FifteenthDayOfMonth,
},
},
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 newAccessListMembers(t *testing.T) []*accesslist.AccessListMember {
t.Helper()
member1, err := accesslist.NewAccessListMember(header.Metadata{
Name: member1,
}, accesslist.AccessListMemberSpec{
AccessList: "test",
Name: member1,
Joined: time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC),
Expires: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
Reason: "because",
AddedBy: ownerUser,
})
require.NoError(t, err)
member2, err := accesslist.NewAccessListMember(header.Metadata{
Name: member2,
}, accesslist.AccessListMemberSpec{
AccessList: "test",
Name: member2,
Joined: time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC),
Expires: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC),
Reason: "because again",
AddedBy: ownerUser,
})
require.NoError(t, err)
member3, err := accesslist.NewAccessListMember(header.Metadata{
Name: member3,
}, accesslist.AccessListMemberSpec{
AccessList: "test",
Name: member3,
Joined: time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC),
Reason: "because for the third time",
AddedBy: ownerUser,
})
require.NoError(t, err)
return []*accesslist.AccessListMember{member1, member2, member3}
}
var accessListYAML = `---
kind: access_list
version: v1
@@ -787,16 +386,3 @@ spec:
review_frequency_changed: 3 months
review_day_of_month_changed: "15"
`
func newUserLock(t *testing.T, name, user string) types.Lock {
t.Helper()
lock, err := types.NewLock(name, types.LockSpecV2{
Target: types.LockTarget{
User: user,
},
})
require.NoError(t, err)
return lock
}
+479 -142
View File
@@ -20,6 +20,7 @@ package local
import (
"context"
"slices"
"time"
"github.com/google/go-cmp/cmp"
@@ -34,6 +35,7 @@ import (
"github.com/gravitational/teleport/api/types/accesslist"
"github.com/gravitational/teleport/api/types/header"
"github.com/gravitational/teleport/entitlements"
"github.com/gravitational/teleport/lib/accesslists"
"github.com/gravitational/teleport/lib/backend"
"github.com/gravitational/teleport/lib/modules"
"github.com/gravitational/teleport/lib/services"
@@ -59,6 +61,11 @@ const (
// creation or update of AccessLists in order to enforce the license limit
// on the number AccessLists in a cluster.
createAccessListLimitLockName = "createAccessListLimitLock"
// accessListResourceLockName is the lock used to prevent simultaneous
// writing to any AccessList resources (AccessLists, AccessListMembers).
// it shares the same string as createAccessListLimitLockName to ensure
// backwards compatibility.
accessListResourceLockName = createAccessListLimitLockName
)
// AccessListService manages Access List resources in the Backend. The AccessListService's
@@ -74,6 +81,18 @@ type AccessListService struct {
reviewService *generic.Service[*accesslist.Review]
}
type accessListAndMembersGetter struct {
service *generic.Service[*accesslist.AccessList]
memberService *generic.Service[*accesslist.AccessListMember]
}
func (s *accessListAndMembersGetter) ListAccessListMembers(ctx context.Context, accessListName string, pageSize int, pageToken string) ([]*accesslist.AccessListMember, string, error) {
return s.memberService.WithPrefix(accessListName).ListResources(ctx, pageSize, pageToken)
}
func (s *accessListAndMembersGetter) GetAccessList(ctx context.Context, name string) (*accesslist.AccessList, error) {
return s.service.GetResource(ctx, name)
}
// compile-time assertion that the AccessListService implements the AccessLists
// interface
var _ services.AccessLists = (*AccessListService)(nil)
@@ -138,6 +157,12 @@ func (a *AccessListService) GetAccessLists(ctx context.Context) ([]*accesslist.A
return accessLists, trace.Wrap(err)
}
// GetInheritedGrants returns grants inherited by access list accessListID from parent access lists.
// This is not implemented in the local service.
func (a *AccessListService) GetInheritedGrants(ctx context.Context, accessListID string) (*accesslist.Grants, error) {
return nil, trace.NotImplemented("GetInheritedGrants should not be called")
}
// ListAccessLists returns a paginated list of access lists.
func (a *AccessListService) ListAccessLists(ctx context.Context, pageSize int, nextToken string) ([]*accesslist.AccessList, string, error) {
return a.service.ListResources(ctx, pageSize, nextToken)
@@ -161,73 +186,197 @@ func (a *AccessListService) GetAccessListsToReview(ctx context.Context) ([]*acce
// UpsertAccessList creates or updates an access list resource.
func (a *AccessListService) UpsertAccessList(ctx context.Context, accessList *accesslist.AccessList) (*accesslist.AccessList, error) {
op := a.service.UpsertResource
return a.runOpWithLock(ctx, accessList, op)
return a.runOpWithLock(ctx, accessList, opTypeUpsert)
}
// UpdateAccessList updates an access list resource.
func (a *AccessListService) UpdateAccessList(ctx context.Context, accessList *accesslist.AccessList) (*accesslist.AccessList, error) {
op := a.service.ConditionalUpdateResource
return a.runOpWithLock(ctx, accessList, op)
return a.runOpWithLock(ctx, accessList, opTypeUpdate)
}
type opFunc func(context.Context, *accesslist.AccessList) (*accesslist.AccessList, error)
type opType int
func (a *AccessListService) runOpWithLock(ctx context.Context, accessList *accesslist.AccessList, op opFunc) (*accesslist.AccessList, error) {
const (
opTypeUpsert opType = iota
opTypeUpdate
)
func (a *AccessListService) runOpWithLock(ctx context.Context, accessList *accesslist.AccessList, op opType) (*accesslist.AccessList, error) {
if err := accessList.CheckAndSetDefaults(); err != nil {
return nil, trace.Wrap(err)
}
var upserted *accesslist.AccessList
var existingList *accesslist.AccessList
opFn := a.service.UpsertResource
if op == opTypeUpdate {
opFn = a.service.ConditionalUpdateResource
}
validateAccessList := func() error {
var err error
if op == opTypeUpdate {
existingList, err = a.service.GetResource(ctx, accessList.GetName())
if err != nil {
return trace.Wrap(err)
}
// Set memberOf / ownerOf to the existing values to prevent them from being updated.
accessList.Status.MemberOf = existingList.Status.MemberOf
accessList.Status.OwnerOf = existingList.Status.OwnerOf
} else {
// In case the MemberOf/OwnerOf fields were manually changed, set to empty.
accessList.Status.MemberOf = []string{}
accessList.Status.OwnerOf = []string{}
}
listMembers, err := a.memberService.WithPrefix(accessList.GetName()).GetResources(ctx)
if err != nil {
return trace.Wrap(err)
}
return accesslists.ValidateAccessListWithMembers(ctx, accessList, listMembers, &accessListAndMembersGetter{a.service, a.memberService})
}
updateAccessList := func() error {
return a.service.RunWhileLocked(ctx, lockName(accessList.GetName()), accessListLockTTL,
func(ctx context.Context, _ backend.Backend) error {
var err error
upserted, err = op(ctx, accessList)
return trace.Wrap(err)
})
var err error
upserted, err = opFn(ctx, accessList)
return trace.Wrap(err)
}
reconcileOwners := func() error {
// Create map to store owners for efficient lookup
originalOwnersMap := make(map[string]struct{})
if existingList != nil {
for _, owner := range existingList.Spec.Owners {
if owner.MembershipKind == accesslist.MembershipKindList {
originalOwnersMap[owner.Name] = struct{}{}
}
}
}
currentOwnersMap := make(map[string]struct{})
for _, owner := range accessList.Spec.Owners {
if owner.MembershipKind == accesslist.MembershipKindList {
currentOwnersMap[owner.Name] = struct{}{}
}
}
// update references for new owners
for ownerName := range currentOwnersMap {
if _, exists := originalOwnersMap[ownerName]; !exists {
if err := a.updateAccessListOwnerOf(ctx, accessList.GetName(), ownerName, true); err != nil {
return trace.Wrap(err)
}
}
}
// update references for old owners
for ownerName := range originalOwnersMap {
if _, exists := currentOwnersMap[ownerName]; !exists {
if err := a.updateAccessListOwnerOf(ctx, accessList.GetName(), ownerName, false); err != nil {
return trace.Wrap(err)
}
}
}
return nil
}
var actions []func() error
// If IGS is not enabled for this cluster we need to wrap the whole
// operation inside *another* lock so that we can accurately count the
// access lists in the cluster in order to prevent un-authorized use of
// the AccessList feature
action := updateAccessList
if !modules.GetModules().Features().GetEntitlement(entitlements.Identity).Enabled {
action = func() error {
err := a.service.RunWhileLocked(ctx, []string{createAccessListLimitLockName}, accessListLockTTL,
func(ctx context.Context, _ backend.Backend) error {
if err := a.VerifyAccessListCreateLimit(ctx, accessList.GetName()); err != nil {
actions = append(actions, func() error { return a.VerifyAccessListCreateLimit(ctx, accessList.GetName()) })
}
actions = append(actions, validateAccessList, updateAccessList, reconcileOwners)
err := a.service.RunWhileLocked(ctx, []string{accessListResourceLockName}, accessListLockTTL,
func(ctx context.Context, _ backend.Backend) error {
return a.service.RunWhileLocked(ctx, lockName(accessList.GetName()), accessListLockTTL, func(ctx context.Context, _ backend.Backend) error {
for _, action := range actions {
if err := action(); err != nil {
return trace.Wrap(err)
}
return trace.Wrap(updateAccessList())
})
return trace.Wrap(err)
}
}
}
return nil
})
})
if err := action(); err != nil {
return nil, trace.Wrap(err)
}
return upserted, nil
return upserted, trace.Wrap(err)
}
// DeleteAccessList removes the specified access list resource.
func (a *AccessListService) DeleteAccessList(ctx context.Context, name string) error {
err := a.service.RunWhileLocked(ctx, lockName(name), accessListLockTTL, func(ctx context.Context, _ backend.Backend) error {
// Delete all associated members.
err := a.memberService.WithPrefix(name).DeleteAllResources(ctx)
action := func(ctx context.Context, _ backend.Backend) error {
// Get list resource.
accessList, err := a.service.GetResource(ctx, name)
if err != nil {
return trace.Wrap(err)
}
return trace.Wrap(a.service.DeleteResource(ctx, name))
})
// Check if the access list is a member or owner of any other access lists.
if len(accessList.Status.MemberOf) > 0 {
for _, memberOf := range accessList.Status.MemberOf {
if _, err := a.service.GetResource(ctx, memberOf); err == nil {
return trace.AccessDenied("Cannot delete '%s', as it is a member of one or more other Access Lists", accessList.Spec.Title)
}
}
}
if len(accessList.Status.OwnerOf) > 0 {
for _, ownerOf := range accessList.Status.OwnerOf {
if _, err := a.service.GetResource(ctx, ownerOf); err == nil {
return trace.AccessDenied("Cannot delete '%s', as it is an owner of one or more other Access Lists", accessList.Spec.Title)
}
}
}
return trace.Wrap(err)
// Delete all associated members.
members, err := a.memberService.WithPrefix(name).GetResources(ctx)
if err != nil {
return trace.Wrap(err)
}
if err := a.memberService.WithPrefix(name).DeleteAllResources(ctx); err != nil {
return trace.Wrap(err)
}
// Update memberOf refs.
for _, member := range members {
if member.Spec.MembershipKind != accesslist.MembershipKindList {
continue
}
if err := a.updateAccessListMemberOf(ctx, name, member.GetName(), false); err != nil {
return trace.Wrap(err)
}
}
// Delete list itself.
if err := a.service.DeleteResource(ctx, name); err != nil {
return trace.Wrap(err)
}
// Update ownerOf refs.
for _, owner := range accessList.Spec.Owners {
if owner.MembershipKind != accesslist.MembershipKindList {
continue
}
if err := a.updateAccessListOwnerOf(ctx, name, owner.Name, false); err != nil {
return trace.Wrap(err)
}
}
return nil
}
return trace.Wrap(a.service.RunWhileLocked(ctx, []string{accessListResourceLockName}, accessListLockTTL, func(ctx context.Context, _ backend.Backend) error {
return a.service.RunWhileLocked(ctx, lockName(name), accessListLockTTL, action)
}))
}
// DeleteAllAccessLists removes all access lists.
@@ -248,15 +397,26 @@ func (a *AccessListService) GetSuggestedAccessLists(ctx context.Context, accessR
}
// CountAccessListMembers will count all access list members.
func (a *AccessListService) CountAccessListMembers(ctx context.Context, accessListName string) (uint32, error) {
func (a *AccessListService) CountAccessListMembers(ctx context.Context, accessListName string) (users uint32, lists uint32, err error) {
count := uint(0)
err := a.service.RunWhileLocked(ctx, lockName(accessListName), accessListLockTTL, func(ctx context.Context, _ backend.Backend) error {
listCount := uint(0)
err = a.service.RunWhileLocked(ctx, lockName(accessListName), accessListLockTTL, func(ctx context.Context, _ backend.Backend) error {
var err error
count, err = a.memberService.WithPrefix(accessListName).CountResources(ctx)
return trace.Wrap(err)
members, err := a.memberService.WithPrefix(accessListName).GetResources(ctx)
if err != nil {
return trace.Wrap(err)
}
for _, member := range members {
if member.Spec.MembershipKind == accesslist.MembershipKindList {
listCount++
} else {
count++
}
}
return nil
})
return uint32(count), trace.Wrap(err)
return uint32(count), uint32(listCount), trace.Wrap(err)
}
// ListAccessListMembers returns a paginated list of all access list members.
@@ -303,71 +463,198 @@ func (a *AccessListService) GetAccessListMember(ctx context.Context, accessList
return member, trace.Wrap(err)
}
// UpsertAccessListMember creates or updates an access list member resource.
func (a *AccessListService) UpsertAccessListMember(ctx context.Context, member *accesslist.AccessListMember) (*accesslist.AccessListMember, error) {
var upserted *accesslist.AccessListMember
err := a.service.RunWhileLocked(ctx, lockName(member.Spec.AccessList), accessListLockTTL, func(ctx context.Context, _ backend.Backend) error {
_, err := a.service.GetResource(ctx, member.Spec.AccessList)
// updateAccessListRefField is a helper that updates the specified field (memberOf or ownerOf) of an Access List,
// adding or removing the specified accessListName to the field of targetName.
func (a *AccessListService) updateAccessListRefField(
ctx context.Context,
accessListName string,
targetName string,
new bool,
fieldSelector func(status *accesslist.Status) *[]string,
) error {
targetAccessList, err := a.service.GetResource(ctx, targetName)
if err != nil {
if trace.IsNotFound(err) {
// If list is not found, it's possible that it was deleted. Regardless, there's nothing to update.
return nil
}
return trace.Wrap(err)
}
field := fieldSelector(&targetAccessList.Status)
// If the field already contains the Access List, and we're adding,
// or doesn't contain it, and we're removing, there's nothing to do.
if slices.Contains(*field, accessListName) == new {
return nil
}
if new {
*field = append(*field, accessListName)
} else {
*field = slices.DeleteFunc(*field, func(e string) bool {
return e == accessListName
})
}
if _, err := a.service.UpdateResource(ctx, targetAccessList); err != nil {
return trace.Wrap(err)
}
return nil
}
// updateAccessListMemberOf updates the memberOf field for the specified memberName and accessListName.
// Should only be called after the relevant member has been successfully upserted or deleted.
func (a *AccessListService) updateAccessListMemberOf(ctx context.Context, accessListName, memberName string, new bool) error {
return a.updateAccessListRefField(ctx, accessListName, memberName, new, func(status *accesslist.Status) *[]string {
return &status.MemberOf
})
}
// updateAccessListOwnerOf updates the ownerOf field for the specified ownerName and accessListName.
// Should only be called after the relevant owner has been successfully upserted or deleted.
func (a *AccessListService) updateAccessListOwnerOf(ctx context.Context, accessListName, ownerName string, new bool) error {
return a.updateAccessListRefField(ctx, accessListName, ownerName, new, func(status *accesslist.Status) *[]string {
return &status.OwnerOf
})
}
// GetAccessListOwners returns a list of all owners in an Access List, including those inherited from nested Access Lists.
//
// Returned Owners are not validated for ownership requirements – use `IsAccessListOwner` for validation.
func (a *AccessListService) GetAccessListOwners(ctx context.Context, accessListName string) ([]*accesslist.Owner, error) {
var owners []*accesslist.Owner
err := a.service.RunWhileLocked(ctx, lockName(accessListName), accessListLockTTL, func(ctx context.Context, _ backend.Backend) error {
accessList, err := a.service.GetResource(ctx, accessListName)
if err != nil {
return trace.Wrap(err)
}
upserted, err = a.memberService.WithPrefix(member.Spec.AccessList).UpsertResource(ctx, member)
owners, err = accesslists.GetOwnersFor(ctx, accessList, &accessListAndMembersGetter{a.service, a.memberService})
return trace.Wrap(err)
})
if err != nil {
return nil, trace.Wrap(err)
return owners, trace.Wrap(err)
}
// UpsertAccessListMember creates or updates an access list member resource.
func (a *AccessListService) UpsertAccessListMember(ctx context.Context, member *accesslist.AccessListMember) (*accesslist.AccessListMember, error) {
var upserted *accesslist.AccessListMember
action := func(ctx context.Context, _ backend.Backend) error {
memberList, err := a.service.GetResource(ctx, member.Spec.AccessList)
if err != nil {
return trace.Wrap(err)
}
if err := accesslists.ValidateAccessListMember(ctx, memberList, member, &accessListAndMembersGetter{a.service, a.memberService}); err != nil {
return trace.Wrap(err)
}
upserted, err = a.memberService.WithPrefix(member.Spec.AccessList).UpsertResource(ctx, member)
if err == nil && member.Spec.MembershipKind == accesslist.MembershipKindList {
if err := a.updateAccessListMemberOf(ctx, member.Spec.AccessList, member.Spec.Name, true); err != nil {
return trace.Wrap(err)
}
}
return trace.Wrap(err)
}
return upserted, nil
err := a.service.RunWhileLocked(ctx, []string{accessListResourceLockName}, accessListLockTTL, func(ctx context.Context, _ backend.Backend) error {
return a.service.RunWhileLocked(ctx, lockName(member.Spec.AccessList), accessListLockTTL, action)
})
return upserted, trace.Wrap(err)
}
// UpdateAccessListMember conditionally updates an access list member resource.
func (a *AccessListService) UpdateAccessListMember(ctx context.Context, member *accesslist.AccessListMember) (*accesslist.AccessListMember, error) {
var updated *accesslist.AccessListMember
err := a.service.RunWhileLocked(ctx, lockName(member.Spec.AccessList), accessListLockTTL, func(ctx context.Context, _ backend.Backend) error {
_, err := a.service.GetResource(ctx, member.Spec.AccessList)
if err != nil {
err := a.service.RunWhileLocked(ctx, []string{accessListResourceLockName}, accessListLockTTL, func(ctx context.Context, _ backend.Backend) error {
return a.service.RunWhileLocked(ctx, lockName(member.Spec.AccessList), accessListLockTTL, func(ctx context.Context, _ backend.Backend) error {
memberList, err := a.service.GetResource(ctx, member.Spec.AccessList)
if err != nil {
return trace.Wrap(err)
}
if err := accesslists.ValidateAccessListMember(ctx, memberList, member, &accessListAndMembersGetter{a.service, a.memberService}); err != nil {
return trace.Wrap(err)
}
updated, err = a.memberService.WithPrefix(member.Spec.AccessList).ConditionalUpdateResource(ctx, member)
return trace.Wrap(err)
}
updated, err = a.memberService.WithPrefix(member.Spec.AccessList).ConditionalUpdateResource(ctx, member)
return trace.Wrap(err)
})
})
if err != nil {
return nil, trace.Wrap(err)
}
return updated, nil
return updated, trace.Wrap(err)
}
// DeleteAccessListMember hard deletes the specified access list member resource.
func (a *AccessListService) DeleteAccessListMember(ctx context.Context, accessList string, memberName string) error {
err := a.service.RunWhileLocked(ctx, lockName(accessList), accessListLockTTL, func(ctx context.Context, _ backend.Backend) error {
_, err := a.service.GetResource(ctx, accessList)
if err != nil {
return trace.Wrap(err)
}
return trace.Wrap(a.memberService.WithPrefix(accessList).DeleteResource(ctx, memberName))
err := a.service.RunWhileLocked(ctx, []string{accessListResourceLockName}, accessListLockTTL, func(ctx context.Context, _ backend.Backend) error {
return a.service.RunWhileLocked(ctx, lockName(accessList), accessListLockTTL, func(ctx context.Context, _ backend.Backend) error {
_, err := a.service.GetResource(ctx, accessList)
if err != nil {
return trace.Wrap(err)
}
member, err := a.memberService.WithPrefix(accessList).GetResource(ctx, memberName)
if err != nil {
return trace.Wrap(err)
}
if err := a.memberService.WithPrefix(accessList).DeleteResource(ctx, memberName); err != nil {
return trace.Wrap(err)
}
if member.Spec.MembershipKind == accesslist.MembershipKindList {
if err := a.updateAccessListMemberOf(ctx, accessList, memberName, false); err != nil {
return trace.Wrap(err)
}
}
return nil
})
})
return trace.Wrap(err)
}
// DeleteAllAccessListMembersForAccessList hard deletes all access list members
// for an access list. Note that deleting all members is the only member
// operation allowed on a list with implicit membership, as it provides a
// mechanism for cleaning out the user list if a list is converted from explicit
// to implicit.
// for an access list. Note that deleting all members is the only member operation
// allowed on a list with implicit membership, as it provides a mechanism for
// cleaning out the user list if a list is converted from explicit to implicit.
func (a *AccessListService) DeleteAllAccessListMembersForAccessList(ctx context.Context, accessList string) error {
err := a.service.RunWhileLocked(ctx, lockName(accessList), accessListLockTTL, func(ctx context.Context, _ backend.Backend) error {
_, err := a.service.GetResource(ctx, accessList)
if err != nil {
return trace.Wrap(err)
}
return trace.Wrap(a.memberService.WithPrefix(accessList).DeleteAllResources(ctx))
err := a.service.RunWhileLocked(ctx, []string{accessListResourceLockName}, accessListLockTTL, func(ctx context.Context, _ backend.Backend) error {
return a.service.RunWhileLocked(ctx, lockName(accessList), accessListLockTTL, func(ctx context.Context, _ backend.Backend) error {
_, err := a.service.GetResource(ctx, accessList)
if err != nil {
return trace.Wrap(err)
}
allMembers, err := a.memberService.WithPrefix(accessList).GetResources(ctx)
if err != nil {
return trace.Wrap(err)
}
if err := a.memberService.WithPrefix(accessList).DeleteAllResources(ctx); err != nil {
return trace.Wrap(err)
}
for _, member := range allMembers {
if member.Spec.MembershipKind != accesslist.MembershipKindList {
continue
}
if err := a.updateAccessListMemberOf(ctx, accessList, member.Spec.Name, false); err != nil {
return trace.Wrap(err)
}
}
return nil
})
})
return trace.Wrap(err)
}
// DeleteAllAccessListMembers hard deletes all access list members.
func (a *AccessListService) DeleteAllAccessListMembers(ctx context.Context) error {
// Locks are not used here as this operation is more likely to be used by the cache.
return trace.Wrap(a.memberService.DeleteAllResources(ctx))
}
@@ -384,98 +671,148 @@ func (a *AccessListService) UpsertAccessListWithMembers(ctx context.Context, acc
}
}
validateAccessList := func() error {
existingList, err := a.service.GetResource(ctx, accessList.GetName())
if err != nil && !trace.IsNotFound(err) {
return trace.Wrap(err)
}
if existingList != nil {
accessList.Status.MemberOf = existingList.Status.MemberOf
accessList.Status.OwnerOf = existingList.Status.OwnerOf
} else {
// In case the MemberOf/OwnerOf fields were manually changed, set to empty.
accessList.Status.MemberOf = []string{}
accessList.Status.OwnerOf = []string{}
}
if err := accesslists.ValidateAccessListWithMembers(ctx, accessList, membersIn, &accessListAndMembersGetter{a.service, a.memberService}); err != nil {
return trace.Wrap(err)
}
return nil
}
reconcileMembers := func() error {
return a.service.RunWhileLocked(ctx, lockName(accessList.GetName()), 2*accessListLockTTL, func(ctx context.Context, _ backend.Backend) error {
// Convert the members slice to a map for easier lookup.
membersMap := utils.FromSlice(membersIn, types.GetName)
// Convert the members slice to a map for easier lookup.
membersMap := utils.FromSlice(membersIn, types.GetName)
var (
members []*accesslist.AccessListMember
membersToken string
)
var (
members []*accesslist.AccessListMember
membersToken string
)
for {
// List all members for the access list.
var err error
members, membersToken, err = a.memberService.WithPrefix(accessList.GetName()).ListResources(ctx, 0 /* default size */, membersToken)
if err != nil {
return trace.Wrap(err)
}
for {
// List all members for the access list.
var err error
members, membersToken, err = a.memberService.WithPrefix(accessList.GetName()).ListResources(ctx, 0 /* default size */, membersToken)
if err != nil {
return trace.Wrap(err)
}
for _, existingMember := range members {
// If the member is not in the new members map (request), delete it.
if newMember, ok := membersMap[existingMember.GetName()]; !ok {
err = a.memberService.WithPrefix(accessList.GetName()).DeleteResource(ctx, existingMember.GetName())
for _, existingMember := range members {
// If the member is not in the new members map (request), delete it.
if newMember, ok := membersMap[existingMember.GetName()]; !ok {
err = a.memberService.WithPrefix(accessList.GetName()).DeleteResource(ctx, existingMember.GetName())
if err != nil {
return trace.Wrap(err)
}
// Update memberOf field if nested list.
if existingMember.Spec.MembershipKind == accesslist.MembershipKindList {
if err := a.updateAccessListMemberOf(ctx, accessList.GetName(), existingMember.GetName(), false); err != nil {
return trace.Wrap(err)
}
}
} else {
// Preserve the membership metadata for any existing members
// to suppress member records flipping back and forth due
// due SCIM pushes or Sync Service updates.
if !existingMember.Spec.Expires.IsZero() {
newMember.Spec.Expires = existingMember.Spec.Expires
}
if existingMember.Spec.Reason != "" {
newMember.Spec.Reason = existingMember.Spec.Reason
}
newMember.Spec.AddedBy = existingMember.Spec.AddedBy
// Compare members and update if necessary.
if !cmp.Equal(newMember, existingMember) {
// Update the member.
upserted, err := a.memberService.WithPrefix(accessList.GetName()).UpsertResource(ctx, newMember)
if err != nil {
return trace.Wrap(err)
}
} else {
// Preserve the membership metadata for any existing members
// to suppress member records flipping back and forth due
// due SCIM pushes or Sync Service updates.
if !existingMember.Spec.Expires.IsZero() {
newMember.Spec.Expires = existingMember.Spec.Expires
}
if existingMember.Spec.Reason != "" {
newMember.Spec.Reason = existingMember.Spec.Reason
}
newMember.Spec.AddedBy = existingMember.Spec.AddedBy
// Compare members and update if necessary.
if !cmp.Equal(newMember, existingMember) {
// Update the member.
upserted, err := a.memberService.WithPrefix(accessList.GetName()).UpsertResource(ctx, newMember)
if err != nil {
return trace.Wrap(err)
}
existingMember.SetRevision(upserted.GetRevision())
}
existingMember.SetRevision(upserted.GetRevision())
}
// Remove the member from the map.
delete(membersMap, existingMember.GetName())
}
if membersToken == "" {
break
}
// Remove the member from the map.
delete(membersMap, existingMember.GetName())
}
// Add any remaining members to the access list.
for _, member := range membersMap {
upserted, err := a.memberService.WithPrefix(accessList.GetName()).UpsertResource(ctx, member)
if err != nil {
if membersToken == "" {
break
}
}
// Add any remaining members to the access list.
for _, member := range membersMap {
upserted, err := a.memberService.WithPrefix(accessList.GetName()).UpsertResource(ctx, member)
if err != nil {
return trace.Wrap(err)
}
// Update memberOf field if nested list.
if member.Spec.MembershipKind == accesslist.MembershipKindList {
if err := a.updateAccessListMemberOf(ctx, accessList.GetName(), member.Spec.Name, true); err != nil {
return trace.Wrap(err)
}
member.SetRevision(upserted.GetRevision())
}
member.SetRevision(upserted.GetRevision())
}
var err error
accessList, err = a.service.UpsertResource(ctx, accessList)
return trace.Wrap(err)
})
return nil
}
reconcileOwners := func() error {
// update references for new owners
for _, owner := range accessList.Spec.Owners {
if owner.MembershipKind == accesslist.MembershipKindList {
if err := a.updateAccessListOwnerOf(ctx, accessList.GetName(), owner.Name, true); err != nil {
return trace.Wrap(err)
}
}
}
return nil
}
updateAccessList := func() error {
var err error
accessList, err = a.service.UpsertResource(ctx, accessList)
return trace.Wrap(err)
}
var actions []func() error
// If IGS is not enabled for this cluster we need to wrap the whole update and
// member reconciliation in *another* lock so that we can accurately count the
// access lists in the cluster in order to prevent un-authorized use of the
// AccessList feature
action := reconcileMembers
if !modules.GetModules().Features().GetEntitlement(entitlements.Identity).Enabled {
action = func() error {
return a.service.RunWhileLocked(ctx, []string{createAccessListLimitLockName}, 2*accessListLockTTL,
func(ctx context.Context, _ backend.Backend) error {
if err := a.VerifyAccessListCreateLimit(ctx, accessList.GetName()); err != nil {
return trace.Wrap(err)
}
return trace.Wrap(reconcileMembers())
})
}
actions = append(actions, func() error { return a.VerifyAccessListCreateLimit(ctx, accessList.GetName()) })
}
if err := action(); err != nil {
actions = append(actions, validateAccessList, reconcileMembers, updateAccessList, reconcileOwners)
if err := a.service.RunWhileLocked(ctx, []string{accessListResourceLockName}, 2*accessListLockTTL, func(ctx context.Context, _ backend.Backend) error {
return a.service.RunWhileLocked(ctx, lockName(accessList.GetName()), 2*accessListLockTTL, func(ctx context.Context, _ backend.Backend) error {
for _, action := range actions {
if err := action(); err != nil {
return trace.Wrap(err)
}
}
return nil
})
}); err != nil {
return nil, nil, trace.Wrap(err)
}
@@ -664,7 +1001,7 @@ func (a *AccessListService) VerifyAccessListCreateLimit(ctx context.Context, tar
return nil // unlimited
}
lists, err := a.GetAccessLists(ctx)
lists, err := a.service.GetResources(ctx)
if err != nil {
return trace.Wrap(err)
}
+42 -3
View File
@@ -132,9 +132,24 @@ func (a *AccessListService) DeleteAllAccessLists(ctx context.Context) error {
}
// CountAccessListMembers will count all access list members.
func (a *AccessListService) CountAccessListMembers(ctx context.Context, accessListName string) (uint32, error) {
count, err := a.memberService.WithPrefix(accessListName).CountResources(ctx)
return uint32(count), trace.Wrap(err)
func (a *AccessListService) CountAccessListMembers(ctx context.Context, accessListName string) (uint32, uint32, error) {
members, err := a.memberService.WithPrefix(accessListName).GetResources(ctx)
if err != nil {
return 0, 0, trace.Wrap(err)
}
var count uint32
var listCount uint32
for _, member := range members {
if member.Spec.MembershipKind == accesslist.MembershipKindList {
listCount++
} else {
count++
}
}
return count, listCount, nil
}
// ListAccessListMembers returns a paginated list of all access list members.
@@ -189,3 +204,27 @@ func (a *AccessListService) ListAllAccessListMembers(ctx context.Context, pageSi
members, nextToken, err := a.memberService.ListResources(ctx, pageSize, pageToken)
return members, nextToken, trace.Wrap(err)
}
// UnconditionalUpsertAccessList creates or updates an Access List resource without any validation.
// It should only ever be used by the cache.
func (a *AccessListService) UnconditionalUpsertAccessList(ctx context.Context, accessList *accesslist.AccessList) (*accesslist.AccessList, error) {
return a.service.UpsertResource(ctx, accessList)
}
// UnconditionalDeleteAccessList removes the specified Access List resource without any validation.
// It should only ever be used by the cache.
func (a *AccessListService) UnconditionalDeleteAccessList(ctx context.Context, name string) error {
return a.service.DeleteResource(ctx, name)
}
// UnconditionalUpsertAccessListMember creates or updates an Access List Member resource without any validation.
// It should only ever be used by the cache.
func (a *AccessListService) UnconditionalUpsertAccessListMember(ctx context.Context, member *accesslist.AccessListMember) (*accesslist.AccessListMember, error) {
return a.memberService.WithPrefix(member.Spec.AccessList).UpsertResource(ctx, member)
}
// UnconditionalDeleteAccessListMember removes the specified Access List Member resource without any validation.
// It should only ever be used by the cache.
func (a *AccessListService) UnconditionalDeleteAccessListMember(ctx context.Context, accessList, memberName string) error {
return a.memberService.WithPrefix(accessList).DeleteResource(ctx, memberName)
}
+19 -9
View File
@@ -749,7 +749,8 @@ func (e *AccessListMemberCreateEvent) Anonymize(a utils.Anonymizer) prehogv1a.Su
return prehogv1a.SubmitEventRequest{
Event: &prehogv1a.SubmitEventRequest_AccessListMemberCreate{
AccessListMemberCreate: &prehogv1a.AccessListMemberCreateEvent{
UserName: a.AnonymizeString(e.UserName),
UserName: a.AnonymizeString(e.UserName),
MemberKind: e.MemberKind,
Metadata: &prehogv1a.AccessListMetadata{
Id: a.AnonymizeString(e.Metadata.Id),
},
@@ -765,7 +766,8 @@ func (e *AccessListMemberUpdateEvent) Anonymize(a utils.Anonymizer) prehogv1a.Su
return prehogv1a.SubmitEventRequest{
Event: &prehogv1a.SubmitEventRequest_AccessListMemberUpdate{
AccessListMemberUpdate: &prehogv1a.AccessListMemberUpdateEvent{
UserName: a.AnonymizeString(e.UserName),
UserName: a.AnonymizeString(e.UserName),
MemberKind: e.MemberKind,
Metadata: &prehogv1a.AccessListMetadata{
Id: a.AnonymizeString(e.Metadata.Id),
},
@@ -781,7 +783,8 @@ func (e *AccessListMemberDeleteEvent) Anonymize(a utils.Anonymizer) prehogv1a.Su
return prehogv1a.SubmitEventRequest{
Event: &prehogv1a.SubmitEventRequest_AccessListMemberDelete{
AccessListMemberDelete: &prehogv1a.AccessListMemberDeleteEvent{
UserName: a.AnonymizeString(e.UserName),
UserName: a.AnonymizeString(e.UserName),
MemberKind: e.MemberKind,
Metadata: &prehogv1a.AccessListMetadata{
Id: a.AnonymizeString(e.Metadata.Id),
},
@@ -797,9 +800,11 @@ func (e *AccessListGrantsToUserEvent) Anonymize(a utils.Anonymizer) prehogv1a.Su
return prehogv1a.SubmitEventRequest{
Event: &prehogv1a.SubmitEventRequest_AccessListGrantsToUser{
AccessListGrantsToUser: &prehogv1a.AccessListGrantsToUserEvent{
UserName: a.AnonymizeString(e.UserName),
CountRolesGranted: e.CountRolesGranted,
CountTraitsGranted: e.CountTraitsGranted,
UserName: a.AnonymizeString(e.UserName),
CountRolesGranted: e.CountRolesGranted,
CountTraitsGranted: e.CountTraitsGranted,
CountInheritedRolesGranted: e.CountInheritedRolesGranted,
CountInheritedTraitsGranted: e.CountInheritedTraitsGranted,
},
},
}
@@ -1677,6 +1682,7 @@ func ConvertUsageEvent(event *usageeventsv1.UsageEventOneOf, userMD UserMetadata
Metadata: &prehogv1a.AccessListMetadata{
Id: e.AccessListMemberCreate.Metadata.Id,
},
MemberKind: e.AccessListMemberCreate.MemberMetadata.MembershipKind.String(),
}
return ret, nil
case *usageeventsv1.UsageEventOneOf_AccessListMemberUpdate:
@@ -1685,6 +1691,7 @@ func ConvertUsageEvent(event *usageeventsv1.UsageEventOneOf, userMD UserMetadata
Metadata: &prehogv1a.AccessListMetadata{
Id: e.AccessListMemberUpdate.Metadata.Id,
},
MemberKind: e.AccessListMemberUpdate.MemberMetadata.MembershipKind.String(),
}
return ret, nil
case *usageeventsv1.UsageEventOneOf_AccessListMemberDelete:
@@ -1693,13 +1700,16 @@ func ConvertUsageEvent(event *usageeventsv1.UsageEventOneOf, userMD UserMetadata
Metadata: &prehogv1a.AccessListMetadata{
Id: e.AccessListMemberDelete.Metadata.Id,
},
MemberKind: e.AccessListMemberDelete.MemberMetadata.MembershipKind.String(),
}
return ret, nil
case *usageeventsv1.UsageEventOneOf_AccessListGrantsToUser:
ret := &AccessListGrantsToUserEvent{
UserName: userMD.Username,
CountRolesGranted: e.AccessListGrantsToUser.CountRolesGranted,
CountTraitsGranted: e.AccessListGrantsToUser.CountTraitsGranted,
UserName: userMD.Username,
CountRolesGranted: e.AccessListGrantsToUser.CountRolesGranted,
CountTraitsGranted: e.AccessListGrantsToUser.CountTraitsGranted,
CountInheritedRolesGranted: e.AccessListGrantsToUser.CountInheritedRolesGranted,
CountInheritedTraitsGranted: e.AccessListGrantsToUser.CountInheritedTraitsGranted,
}
return ret, nil
case *usageeventsv1.UsageEventOneOf_TagExecuteQuery:
@@ -25,7 +25,9 @@ import (
"github.com/stretchr/testify/require"
"github.com/gravitational/teleport"
accesslistv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/accesslist/v1"
usageeventsv1 "github.com/gravitational/teleport/api/gen/proto/go/usageevents/v1"
"github.com/gravitational/teleport/api/types/accesslist"
prehogv1a "github.com/gravitational/teleport/gen/proto/go/prehog/v1alpha"
"github.com/gravitational/teleport/lib/utils"
)
@@ -448,6 +450,9 @@ func TestConvertUsageEvent(t *testing.T) {
Metadata: &usageeventsv1.AccessListMetadata{
Id: "someid",
},
MemberMetadata: &usageeventsv1.AccessListMemberMetadata{
MembershipKind: accesslistv1.MembershipKind_MEMBERSHIP_KIND_USER,
},
},
}},
identityUsername: "myuser",
@@ -458,6 +463,7 @@ func TestConvertUsageEvent(t *testing.T) {
Metadata: &prehogv1a.AccessListMetadata{
Id: expectedAnonymizedAccessListIDString,
},
MemberKind: accesslist.MembershipKindUser,
},
}},
},
@@ -468,6 +474,9 @@ func TestConvertUsageEvent(t *testing.T) {
Metadata: &usageeventsv1.AccessListMetadata{
Id: "someid",
},
MemberMetadata: &usageeventsv1.AccessListMemberMetadata{
MembershipKind: accesslistv1.MembershipKind_MEMBERSHIP_KIND_USER,
},
},
}},
identityUsername: "myuser",
@@ -478,6 +487,7 @@ func TestConvertUsageEvent(t *testing.T) {
Metadata: &prehogv1a.AccessListMetadata{
Id: expectedAnonymizedAccessListIDString,
},
MemberKind: accesslist.MembershipKindUser,
},
}},
},
@@ -488,6 +498,9 @@ func TestConvertUsageEvent(t *testing.T) {
Metadata: &usageeventsv1.AccessListMetadata{
Id: "someid",
},
MemberMetadata: &usageeventsv1.AccessListMemberMetadata{
MembershipKind: accesslistv1.MembershipKind_MEMBERSHIP_KIND_USER,
},
},
}},
identityUsername: "myuser",
@@ -498,6 +511,7 @@ func TestConvertUsageEvent(t *testing.T) {
Metadata: &prehogv1a.AccessListMetadata{
Id: expectedAnonymizedAccessListIDString,
},
MemberKind: accesslist.MembershipKindUser,
},
}},
},
@@ -505,17 +519,21 @@ func TestConvertUsageEvent(t *testing.T) {
name: "access list grants to user event",
event: &usageeventsv1.UsageEventOneOf{Event: &usageeventsv1.UsageEventOneOf_AccessListGrantsToUser{
AccessListGrantsToUser: &usageeventsv1.AccessListGrantsToUser{
CountRolesGranted: 5,
CountTraitsGranted: 6,
CountRolesGranted: 5,
CountTraitsGranted: 6,
CountInheritedRolesGranted: 0,
CountInheritedTraitsGranted: 0,
},
}},
identityUsername: "myuser",
errCheck: require.NoError,
expected: &prehogv1a.SubmitEventRequest{Event: &prehogv1a.SubmitEventRequest_AccessListGrantsToUser{
AccessListGrantsToUser: &prehogv1a.AccessListGrantsToUserEvent{
UserName: expectedAnonymizedUserString,
CountRolesGranted: 5,
CountTraitsGranted: 6,
UserName: expectedAnonymizedUserString,
CountRolesGranted: 5,
CountTraitsGranted: 6,
CountInheritedRolesGranted: 0,
CountInheritedTraitsGranted: 0,
},
}},
},
+12
View File
@@ -934,6 +934,8 @@ message AccessListMemberCreateEvent {
// user_name is the anonymized user name
string user_name = 1;
AccessListMetadata metadata = 2;
// member_kind is the type of membership of the created member in the parent access list.
string member_kind = 3;
}
// AccessListMemberUpdate is an event that is emitted when a member is updated in an access list.
@@ -941,6 +943,8 @@ message AccessListMemberUpdateEvent {
// user_name is the anonymized user name
string user_name = 1;
AccessListMetadata metadata = 2;
// membership_kind is the type of membership of the updated member in the parent access list.
string member_kind = 3;
}
// AccessListMemberDelete is an event that is emitted when a member is removed from an access list.
@@ -948,6 +952,8 @@ message AccessListMemberDeleteEvent {
// user_name is anonymized user name
string user_name = 1;
AccessListMetadata metadata = 2;
// member_kind is the type of membership of the deleted user in the parent access list.
string member_kind = 3;
}
// AccessListGrantsToUser is an event that is emitted when access list permissions are granted to a user
@@ -961,6 +967,12 @@ message AccessListGrantsToUserEvent {
// count_traits_granted is the number of traits granted to a user.
int32 count_traits_granted = 3;
// count_inherited_roles_granted is the number of roles granted to a user inherited from nested access lists.
int32 count_inherited_roles_granted = 4;
// count_inherited_traits_granted is the number of traits granted to a user inherited from nested access lists.
int32 count_inherited_traits_granted = 5;
}
// AccessListReviewCreateEvent is an event that is emitted when an access list review is created.
+25 -3
View File
@@ -49,6 +49,8 @@ type ACLCommand struct {
// Used for managing a particular access list.
accessListName string
// Used to add an access list to another one
memberKind string
// Used for managing membership to an access list.
userName string
@@ -56,6 +58,11 @@ type ACLCommand struct {
reason string
}
const (
memberKindUser = "user"
memberKindList = "list"
)
// Initialize allows ACLCommand to plug itself into the CLI parser
func (c *ACLCommand) Initialize(app *kingpin.Application, _ *servicecfg.Config) {
acl := app.Command("acl", "Manage access lists.").Alias("access-lists")
@@ -70,6 +77,7 @@ func (c *ACLCommand) Initialize(app *kingpin.Application, _ *servicecfg.Config)
users := acl.Command("users", "Manage user membership to access lists.")
c.usersAdd = users.Command("add", "Add a user to an access list.")
c.usersAdd.Flag("kind", "Access list member kind, 'user' or 'list'").Default(memberKindUser).EnumVar(&c.memberKind, memberKindUser, memberKindList)
c.usersAdd.Arg("access-list-name", "The access list name.").Required().StringVar(&c.accessListName)
c.usersAdd.Arg("user", "The user to add to the access list.").Required().StringVar(&c.userName)
c.usersAdd.Arg("expires", "When the user's access expires (must be in RFC3339). Defaults to the expiration time of the access list.").StringVar(&c.expires)
@@ -151,6 +159,14 @@ func (c *ACLCommand) UsersAdd(ctx context.Context, client *authclient.Client) er
}
}
var membershipKind string
switch c.memberKind {
case memberKindList:
membershipKind = accesslist.MembershipKindList
case "", memberKindUser:
membershipKind = accesslist.MembershipKindUser
}
member, err := accesslist.NewAccessListMember(header.Metadata{
Name: c.userName,
}, accesslist.AccessListMemberSpec{
@@ -160,8 +176,9 @@ func (c *ACLCommand) UsersAdd(ctx context.Context, client *authclient.Client) er
Expires: expires,
// The following fields will be updated in the backend, so their values here don't matter.
Joined: time.Now(),
AddedBy: "dummy",
Joined: time.Now(),
AddedBy: "dummy",
MembershipKind: membershipKind,
})
if err != nil {
return trace.Wrap(err)
@@ -219,7 +236,11 @@ func (c *ACLCommand) UsersList(ctx context.Context, client *authclient.Client) e
}
fmt.Printf("Members of %s:\n", c.accessListName)
for _, member := range allMembers {
fmt.Printf("- %s\n", member.Spec.Name)
if member.Spec.MembershipKind == accesslist.MembershipKindList {
fmt.Printf("- (Access List) %s \n", member.Spec.Name)
} else {
fmt.Printf("- %s\n", member.Spec.Name)
}
}
return nil
default:
@@ -249,6 +270,7 @@ func displayAccessListsText(accessLists ...*accesslist.AccessList) error {
for k, values := range accessList.GetGrants().Traits {
traitStrings = append(traitStrings, fmt.Sprintf("%s:{%s}", k, strings.Join(values, ",")))
}
grantedTraits := strings.Join(traitStrings, ",")
table.AddRow([]string{
accessList.GetName(),
+1 -1
View File
@@ -446,7 +446,7 @@ func (p *cliModules) GenerateAccessRequestPromotions(_ context.Context, _ module
return &types.AccessRequestAllowedPromotions{}, nil
}
func (p *cliModules) GetSuggestedAccessLists(ctx context.Context, _ *tlsca.Identity, _ modules.AccessListSuggestionClient, _ modules.AccessListGetter, _ string) ([]*accesslist.AccessList, error) {
func (p *cliModules) GetSuggestedAccessLists(ctx context.Context, _ *tlsca.Identity, _ modules.AccessListSuggestionClient, _ modules.AccessListAndMembersGetter, _ string) ([]*accesslist.AccessList, error) {
return []*accesslist.AccessList{}, nil
}
+1 -1
View File
@@ -218,7 +218,7 @@ func (p *cliModules) GenerateAccessRequestPromotions(_ context.Context, _ module
return &types.AccessRequestAllowedPromotions{}, nil
}
func (p *cliModules) GetSuggestedAccessLists(ctx context.Context, _ *tlsca.Identity, _ modules.AccessListSuggestionClient, _ modules.AccessListGetter, _ string) ([]*accesslist.AccessList, error) {
func (p *cliModules) GetSuggestedAccessLists(ctx context.Context, _ *tlsca.Identity, _ modules.AccessListSuggestionClient, _ modules.AccessListAndMembersGetter, _ string) ([]*accesslist.AccessList, error) {
return []*accesslist.AccessList{}, nil
}
@@ -28,7 +28,7 @@ import parseError, { ApiError } from './parseError';
export const MFA_HEADER = 'Teleport-Mfa-Response';
const api = {
get(url, abortSignal?) {
get(url: string, abortSignal?: AbortSignal) {
return api.fetchJsonWithMfaAuthnRetry(url, { signal: abortSignal });
},