Skip to content

Commit

Permalink
feat: Update AccessList Proto with new fields, add utils (#47828)
Browse files Browse the repository at this point in the history
- Add Hierarchy utils for nested AccessList validation and handling
- Update AccessList Protos with new fields
- Update AccessList and AccessListMember types and converters with new
  fields
  • Loading branch information
kiosion authored Oct 30, 2024
1 parent 824a5f0 commit a0540c5
Show file tree
Hide file tree
Showing 14 changed files with 2,065 additions and 251 deletions.
570 changes: 349 additions & 221 deletions api/gen/proto/go/teleport/accesslist/v1/accesslist.pb.go

Large diffs are not rendered by default.

27 changes: 26 additions & 1 deletion api/proto/teleport/accesslist/v1/accesslist.proto
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ message AccessListOwner {
// ineligible_status describes if this owner is eligible or not
// and if not, describes how they're lacking eligibility.
IneligibleStatus ineligible_status = 3;

// membership_kind describes the type of membership, either
// `MEMBERSHIP_KIND_USER` or `MEMBERSHIP_KIND_LIST`.
MembershipKind membership_kind = 4;
}

// AccessListAudit describes the audit configuration for an Access List.
Expand Down Expand Up @@ -197,6 +201,21 @@ message MemberSpec {
// ineligible_status describes if this member is eligible or not
// and if not, describes how they're lacking eligibility.
IneligibleStatus ineligible_status = 7;

// membership_kind describes the type of membership, either
// `MEMBERSHIP_KIND_USER` or `MEMBERSHIP_KIND_LIST`.
MembershipKind membership_kind = 9;
}

// MembershipKind represents the different kinds of list membership
enum MembershipKind {
// MEMBERSHIP_KIND_UNSPECIFIED represents list members that are of
// unknown membership kind, defaulting to being treated as type USER
MEMBERSHIP_KIND_UNSPECIFIED = 0;
// MEMBERSHIP_KIND_USER represents list members that are normal users
MEMBERSHIP_KIND_USER = 1;
// MEMBERSHIP_KIND_LIST represents list members that are nested Access Lists
MEMBERSHIP_KIND_LIST = 2;
}

// IneligibleStatus describes how the user is ineligible.
Expand Down Expand Up @@ -268,6 +287,12 @@ message ReviewChanges {

// AccessListStatus contains dynamic fields calculated during retrieval.
message AccessListStatus {
// member_count is the number of members in the in the Access List.
// member_count is the number of members in the Access List.
optional uint32 member_count = 1;
// member_list_count is the number of nested list members in the Access List.
optional uint32 member_list_count = 2;
// owner_of describes Access Lists where this Access List is an explicit owner.
repeated string owner_of = 3;
// member_of describes Access Lists where this Access List is an explicit member.
repeated string member_of = 4;
}
44 changes: 37 additions & 7 deletions api/types/accesslist/accesslist.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"

accesslistv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/accesslist/v1"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/types/header"
"github.com/gravitational/teleport/api/types/header/convert/legacy"
Expand Down Expand Up @@ -75,6 +76,20 @@ func parseReviewFrequency(input string) ReviewFrequency {
return 0
}

// MaxAllowedDepth is the maximum allowed depth for nested access lists.
const MaxAllowedDepth = 10

var (
// MembershipKindUnspecified is the default membership kind (treated as 'user').
MembershipKindUnspecified = accesslistv1.MembershipKind_MEMBERSHIP_KIND_UNSPECIFIED.String()

// MembershipKindUser is the user membership kind.
MembershipKindUser = accesslistv1.MembershipKind_MEMBERSHIP_KIND_USER.String()

// MembershipKindList is the list membership kind.
MembershipKindList = accesslistv1.MembershipKind_MEMBERSHIP_KIND_LIST.String()
)

// ReviewDayOfMonth is the day of month the review should be repeated on.
type ReviewDayOfMonth int

Expand Down Expand Up @@ -123,7 +138,7 @@ type AccessList struct {
Spec Spec `json:"spec" yaml:"spec"`

// Status contains dynamically calculated fields.
Status Status `json:"-" yaml:"-"`
Status Status `json:"status" yaml:"status"`
}

// Spec is the specification for an access list.
Expand Down Expand Up @@ -167,6 +182,10 @@ type Owner struct {

// IneligibleStatus describes the reason why this owner is not eligible.
IneligibleStatus string `json:"ineligible_status" yaml:"ineligible_status"`

// MembershipKind describes the kind of ownership,
// either "MEMBERSHIP_KIND_USER" or "MEMBERSHIP_KIND_LIST".
MembershipKind string `json:"membership_kind" yaml:"membership_kind"`
}

// Audit describes the audit configuration for an access list.
Expand Down Expand Up @@ -224,7 +243,14 @@ type Grants struct {
// Status contains dynamic fields calculated during retrieval.
type Status struct {
// MemberCount is the number of members in the access list.
MemberCount *uint32
MemberCount *uint32 `json:"-" yaml:"-"`
// MemberListCount is the number of members in the access list that are lists themselves.
MemberListCount *uint32 `json:"-" yaml:"-"`

// OwnerOf is a list of Access List UUIDs where this access list is an explicit owner.
OwnerOf []string `json:"owner_of" yaml:"owner_of"`
// MemberOf is a list of Access List UUIDs where this access list is an explicit member.
MemberOf []string `json:"member_of" yaml:"member_of"`
}

// NewAccessList will create a new access list.
Expand Down Expand Up @@ -286,10 +312,6 @@ func (a *AccessList) CheckAndSetDefaults() error {
a.Spec.Audit.Notifications.Start = twoWeeks
}

if len(a.Spec.Grants.Roles) == 0 && len(a.Spec.Grants.Traits) == 0 {
return trace.BadParameter("grants must specify at least one role or trait")
}

// Deduplicate owners. The backend will currently prevent this, but it's possible that access lists
// were created with duplicated owners before the backend checked for duplicate owners. In order to
// ensure that these access lists are backwards compatible, we'll deduplicate them here.
Expand All @@ -299,6 +321,9 @@ func (a *AccessList) CheckAndSetDefaults() error {
if owner.Name == "" {
return trace.BadParameter("owner name is missing")
}
if owner.MembershipKind == "" {
owner.MembershipKind = MembershipKindUser
}

if _, ok := ownerMap[owner.Name]; ok {
continue
Expand All @@ -317,7 +342,7 @@ func (a *AccessList) GetOwners() []Owner {
return a.Spec.Owners
}

// GetOwners returns the list of owners from the access list.
// SetOwners sets the owners of the access list.
func (a *AccessList) SetOwners(owners []Owner) {
a.Spec.Owners = owners
}
Expand All @@ -337,6 +362,11 @@ func (a *AccessList) GetGrants() Grants {
return a.Spec.Grants
}

// GetOwnerGrants returns the owner grants from the access list.
func (a *AccessList) GetOwnerGrants() Grants {
return a.Spec.OwnerGrants
}

// GetMetadata returns metadata. This is specifically for conforming to the Resource interface,
// and should be removed when possible.
func (a *AccessList) GetMetadata() types.Metadata {
Expand Down
98 changes: 80 additions & 18 deletions api/types/accesslist/convert/v1/accesslist.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,10 @@ func FromProto(msg *accesslistv1.AccessList, opts ...AccessListOption) (*accessl

owners := make([]accesslist.Owner, len(msg.Spec.Owners))
for i, owner := range msg.Spec.Owners {
owners[i] = accesslist.Owner{
Name: owner.Name,
Description: owner.Description,
// Set it to empty as default.
// Must provide as options to set it with the provided value.
IneligibleStatus: "",
}
owners[i] = FromOwnerProto(owner)
// Set IneligibleStatus to empty as default.
// Must provide as options to set it with the provided value.
owners[i].IneligibleStatus = ""
}

var ownerGrants accesslist.Grants
Expand All @@ -94,10 +91,24 @@ func FromProto(msg *accesslistv1.AccessList, opts ...AccessListOption) (*accessl
}

var memberCount *uint32
var memberListCount *uint32
if msg.Status != nil && msg.Status.MemberCount != nil {
memberCount = new(uint32)
*memberCount = *msg.Status.MemberCount
}
if msg.Status != nil && msg.Status.MemberListCount != nil {
memberListCount = new(uint32)
*memberListCount = *msg.Status.MemberListCount
}

var ownerOf []string
var memberOf []string
if msg.Status != nil && msg.Status.OwnerOf != nil {
ownerOf = msg.Status.OwnerOf
}
if msg.Status != nil && msg.Status.MemberOf != nil {
memberOf = msg.Status.MemberOf
}

accessList, err := accesslist.NewAccessList(headerv1.FromMetadataProto(msg.Header.Metadata), accesslist.Spec{
Title: msg.Spec.Title,
Expand Down Expand Up @@ -126,7 +137,10 @@ func FromProto(msg *accesslistv1.AccessList, opts ...AccessListOption) (*accessl
return nil, trace.Wrap(err)
}
accessList.Status = accesslist.Status{
MemberCount: memberCount,
MemberCount: memberCount,
MemberListCount: memberListCount,
OwnerOf: ownerOf,
MemberOf: memberOf,
}

for _, opt := range opts {
Expand All @@ -140,15 +154,7 @@ func FromProto(msg *accesslistv1.AccessList, opts ...AccessListOption) (*accessl
func ToProto(accessList *accesslist.AccessList) *accesslistv1.AccessList {
owners := make([]*accesslistv1.AccessListOwner, len(accessList.Spec.Owners))
for i, owner := range accessList.Spec.Owners {
var ineligibleStatus accesslistv1.IneligibleStatus
if enumVal, ok := accesslistv1.IneligibleStatus_value[owner.IneligibleStatus]; ok {
ineligibleStatus = accesslistv1.IneligibleStatus(enumVal)
}
owners[i] = &accesslistv1.AccessListOwner{
Name: owner.Name,
Description: owner.Description,
IneligibleStatus: ineligibleStatus,
}
owners[i] = ToOwnerProto(owner)
}

var ownerGrants *accesslistv1.AccessListGrants
Expand All @@ -173,10 +179,24 @@ func ToProto(accessList *accesslist.AccessList) *accesslistv1.AccessList {
}

var memberCount *uint32
var memberListCount *uint32
if accessList.Status.MemberCount != nil {
memberCount = new(uint32)
*memberCount = *accessList.Status.MemberCount
}
if accessList.Status.MemberListCount != nil {
memberListCount = new(uint32)
*memberListCount = *accessList.Status.MemberListCount
}

var ownerOf []string
var memberOf []string
if accessList.Status.OwnerOf != nil {
ownerOf = accessList.Status.OwnerOf
}
if accessList.Status.MemberOf != nil {
memberOf = accessList.Status.MemberOf
}

return &accesslistv1.AccessList{
Header: headerv1.ToResourceHeaderProto(accessList.ResourceHeader),
Expand Down Expand Up @@ -209,11 +229,53 @@ func ToProto(accessList *accesslist.AccessList) *accesslistv1.AccessList {
OwnerGrants: ownerGrants,
},
Status: &accesslistv1.AccessListStatus{
MemberCount: memberCount,
MemberCount: memberCount,
MemberListCount: memberListCount,
OwnerOf: ownerOf,
MemberOf: memberOf,
},
}
}

// ToOwnerProto converts an internal access list owner into a v1 access list owner object.
func ToOwnerProto(owner accesslist.Owner) *accesslistv1.AccessListOwner {
var ineligibleStatus accesslistv1.IneligibleStatus
if owner.IneligibleStatus != "" {
if enumVal, ok := accesslistv1.IneligibleStatus_value[owner.IneligibleStatus]; ok {
ineligibleStatus = accesslistv1.IneligibleStatus(enumVal)
}
} else {
ineligibleStatus = accesslistv1.IneligibleStatus_INELIGIBLE_STATUS_UNSPECIFIED
}

var kind accesslistv1.MembershipKind
if enumVal, ok := accesslistv1.MembershipKind_value[owner.MembershipKind]; ok {
kind = accesslistv1.MembershipKind(enumVal)
}

return &accesslistv1.AccessListOwner{
Name: owner.Name,
Description: owner.Description,
IneligibleStatus: ineligibleStatus,
MembershipKind: kind,
}
}

// FromOwnerProto converts a v1 access list owner into an internal access list owner object.
func FromOwnerProto(protoOwner *accesslistv1.AccessListOwner) accesslist.Owner {
ineligibleStatus := ""
if protoOwner.IneligibleStatus != accesslistv1.IneligibleStatus_INELIGIBLE_STATUS_UNSPECIFIED {
ineligibleStatus = protoOwner.IneligibleStatus.String()
}

return accesslist.Owner{
Name: protoOwner.Name,
Description: protoOwner.Description,
IneligibleStatus: ineligibleStatus,
MembershipKind: protoOwner.MembershipKind.String(),
}
}

// WithOwnersIneligibleStatusField sets the "ineligibleStatus" field to the provided proto value.
func WithOwnersIneligibleStatusField(protoOwners []*accesslistv1.AccessListOwner) AccessListOption {
return func(a *accesslist.AccessList) {
Expand Down
8 changes: 8 additions & 0 deletions api/types/accesslist/member.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ type AccessListMemberSpec struct {

// IneligibleStatus describes the reason why this member is not eligible.
IneligibleStatus string `json:"ineligible_status" yaml:"ineligible_status"`

// MembershipKind describes the kind of membership,
// either "MEMBERSHIP_KIND_USER" or "MEMBERSHIP_KIND_LIST".
MembershipKind string `json:"membership_kind" yaml:"membership_kind"`
}

// NewAccessListMember will create a new access listm member.
Expand All @@ -86,6 +90,10 @@ func (a *AccessListMember) CheckAndSetDefaults() error {
return trace.Wrap(err)
}

if a.Spec.MembershipKind == "" {
a.Spec.MembershipKind = MembershipKindUser
}

if a.Spec.AccessList == "" {
return trace.BadParameter("access list is missing")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ resource, which you can apply after installing the Teleport Kubernetes operator.
|---|---|---|
|description|string|description is the plaintext description of the owner and why they are an owner.|
|ineligible_status|string or integer|ineligible_status describes if this owner is eligible or not and if not, describes how they're lacking eligibility. Can be either the string or the integer representation of each option.|
|membership_kind|string or integer|membership_kind describes the type of membership, either `MEMBERSHIP_KIND_USER` or `MEMBERSHIP_KIND_LIST`. Can be either the string or the integer representation of each option.|
|name|string|name is the username of the owner.|

### spec.ownership_requires
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ Optional:
Optional:

- `description` (String) description is the plaintext description of the owner and why they are an owner.
- `membership_kind` (Number) membership_kind describes the type of membership, either `MEMBERSHIP_KIND_USER` or `MEMBERSHIP_KIND_LIST`.
- `name` (String) name is the username of the owner.


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ Optional:
Optional:

- `description` (String) description is the plaintext description of the owner and why they are an owner.
- `membership_kind` (Number) membership_kind describes the type of membership, either `MEMBERSHIP_KIND_USER` or `MEMBERSHIP_KIND_LIST`.
- `name` (String) name is the username of the owner.


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,10 @@ spec:
description: ineligible_status describes if this owner is eligible
or not and if not, describes how they're lacking eligibility.
x-kubernetes-int-or-string: true
membership_kind:
description: membership_kind describes the type of membership,
either `MEMBERSHIP_KIND_USER` or `MEMBERSHIP_KIND_LIST`.
x-kubernetes-int-or-string: true
name:
description: name is the username of the owner.
type: string
Expand Down
Loading

0 comments on commit a0540c5

Please sign in to comment.