scoped watcher/caching prep (#68320)

This commit is contained in:
Forrest
2026-07-09 19:24:10 +00:00
committed by GitHub
parent 6ad3987fea
commit bc7cd3aa03
38 changed files with 1653 additions and 583 deletions
@@ -176,12 +176,17 @@ type ListScopedRolesRequest struct {
// PageToken is the pagination cursor used to start from where a previous request left off.
PageToken string `protobuf:"bytes,2,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"`
// ResourceScope filters roles by their resource scope if specified.
//
// Deprecated: Marked as deprecated in teleport/scopes/access/v1/service.proto.
ResourceScope *v1.Filter `protobuf:"bytes,3,opt,name=resource_scope,json=resourceScope,proto3" json:"resource_scope,omitempty"`
// AssignableScope filters roles by their assignable scope if specified.
AssignableScope *v1.Filter `protobuf:"bytes,4,opt,name=assignable_scope,json=assignableScope,proto3" json:"assignable_scope,omitempty"`
// NameFilter filters roles by their name using a case-insensitive substring
// match if specified.
NameFilter string `protobuf:"bytes,5,opt,name=name_filter,json=nameFilter,proto3" json:"name_filter,omitempty"`
NameFilter string `protobuf:"bytes,5,opt,name=name_filter,json=nameFilter,proto3" json:"name_filter,omitempty"`
// ScopeFilter is the primary scope filter. Filtering is performed against the scope of the
// resource itself. Defaults to one of EXACT or UNSCOPED depending on wether the caller is
// a scoped or unscoped identity. Exhaustive user-facing views (e.g. `tctl get`) should
// specify mode ALL.
ScopeFilter *v1.Filter `protobuf:"bytes,6,opt,name=scope_filter,json=scopeFilter,proto3" json:"scope_filter,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -225,6 +230,7 @@ func (x *ListScopedRolesRequest) GetPageToken() string {
return ""
}
// Deprecated: Marked as deprecated in teleport/scopes/access/v1/service.proto.
func (x *ListScopedRolesRequest) GetResourceScope() *v1.Filter {
if x != nil {
return x.ResourceScope
@@ -232,13 +238,6 @@ func (x *ListScopedRolesRequest) GetResourceScope() *v1.Filter {
return nil
}
func (x *ListScopedRolesRequest) GetAssignableScope() *v1.Filter {
if x != nil {
return x.AssignableScope
}
return nil
}
func (x *ListScopedRolesRequest) GetNameFilter() string {
if x != nil {
return x.NameFilter
@@ -246,6 +245,13 @@ func (x *ListScopedRolesRequest) GetNameFilter() string {
return ""
}
func (x *ListScopedRolesRequest) GetScopeFilter() *v1.Filter {
if x != nil {
return x.ScopeFilter
}
return nil
}
func (x *ListScopedRolesRequest) SetPageSize(v int32) {
x.PageSize = v
}
@@ -254,18 +260,20 @@ func (x *ListScopedRolesRequest) SetPageToken(v string) {
x.PageToken = v
}
// Deprecated: Marked as deprecated in teleport/scopes/access/v1/service.proto.
func (x *ListScopedRolesRequest) SetResourceScope(v *v1.Filter) {
x.ResourceScope = v
}
func (x *ListScopedRolesRequest) SetAssignableScope(v *v1.Filter) {
x.AssignableScope = v
}
func (x *ListScopedRolesRequest) SetNameFilter(v string) {
x.NameFilter = v
}
func (x *ListScopedRolesRequest) SetScopeFilter(v *v1.Filter) {
x.ScopeFilter = v
}
// Deprecated: Marked as deprecated in teleport/scopes/access/v1/service.proto.
func (x *ListScopedRolesRequest) HasResourceScope() bool {
if x == nil {
return false
@@ -273,19 +281,20 @@ func (x *ListScopedRolesRequest) HasResourceScope() bool {
return x.ResourceScope != nil
}
func (x *ListScopedRolesRequest) HasAssignableScope() bool {
func (x *ListScopedRolesRequest) HasScopeFilter() bool {
if x == nil {
return false
}
return x.AssignableScope != nil
return x.ScopeFilter != nil
}
// Deprecated: Marked as deprecated in teleport/scopes/access/v1/service.proto.
func (x *ListScopedRolesRequest) ClearResourceScope() {
x.ResourceScope = nil
}
func (x *ListScopedRolesRequest) ClearAssignableScope() {
x.AssignableScope = nil
func (x *ListScopedRolesRequest) ClearScopeFilter() {
x.ScopeFilter = nil
}
type ListScopedRolesRequest_builder struct {
@@ -296,12 +305,17 @@ type ListScopedRolesRequest_builder struct {
// PageToken is the pagination cursor used to start from where a previous request left off.
PageToken string
// ResourceScope filters roles by their resource scope if specified.
//
// Deprecated: Marked as deprecated in teleport/scopes/access/v1/service.proto.
ResourceScope *v1.Filter
// AssignableScope filters roles by their assignable scope if specified.
AssignableScope *v1.Filter
// NameFilter filters roles by their name using a case-insensitive substring
// match if specified.
NameFilter string
// ScopeFilter is the primary scope filter. Filtering is performed against the scope of the
// resource itself. Defaults to one of EXACT or UNSCOPED depending on wether the caller is
// a scoped or unscoped identity. Exhaustive user-facing views (e.g. `tctl get`) should
// specify mode ALL.
ScopeFilter *v1.Filter
}
func (b0 ListScopedRolesRequest_builder) Build() *ListScopedRolesRequest {
@@ -311,8 +325,8 @@ func (b0 ListScopedRolesRequest_builder) Build() *ListScopedRolesRequest {
x.PageSize = b.PageSize
x.PageToken = b.PageToken
x.ResourceScope = b.ResourceScope
x.AssignableScope = b.AssignableScope
x.NameFilter = b.NameFilter
x.ScopeFilter = b.ScopeFilter
return m0
}
@@ -1092,11 +1106,6 @@ type ListScopedRoleAssignmentsRequest struct {
PageSize int32 `protobuf:"varint,1,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"`
// PageToken is the pagination cursor used to start from where a previous request left off.
PageToken string `protobuf:"bytes,2,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"`
// ResourceScope filters assignments by their resource scope if specified.
ResourceScope *v1.Filter `protobuf:"bytes,3,opt,name=resource_scope,json=resourceScope,proto3" json:"resource_scope,omitempty"`
// AssignedScope filters assignments by the scopes they assign to if specified (note: matches assignment
// resources with 1 or more maching scopes, not all scopes within the assignment will necessarily match).
AssignedScope *v1.Filter `protobuf:"bytes,4,opt,name=assigned_scope,json=assignedScope,proto3" json:"assigned_scope,omitempty"`
// User optionally limits the list to assignments for a specific user.
User string `protobuf:"bytes,5,opt,name=user,proto3" json:"user,omitempty"`
// Role optionally limits the list to assignments for a specific role.
@@ -1106,8 +1115,17 @@ type ListScopedRoleAssignmentsRequest struct {
// is specifically used to support users discovering where they have been granted privileges across scopes,
// and is not intended for general use.
AllCallerAssignments bool `protobuf:"varint,7,opt,name=all_caller_assignments,json=allCallerAssignments,proto3" json:"all_caller_assignments,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
// ScopeFilter is the primary scope filter. Filtering is performed against the scope of the
// resource itself. Defaults to one of EXACT or UNSCOPED depending on wether the caller is
// a scoped or unscoped identity. Exhaustive user-facing views (e.g. `tctl get`) should
// specify mode ALL.
ScopeFilter *v1.Filter `protobuf:"bytes,8,opt,name=scope_filter,json=scopeFilter,proto3" json:"scope_filter,omitempty"`
// AssignedScopeFilter filters assignments by the scopes they assign to if specified (note: matches
// assignment resources with 1 or more matching scopes, not all scopes within the assignment will
// necessarily match).
AssignedScopeFilter *v1.Filter `protobuf:"bytes,9,opt,name=assigned_scope_filter,json=assignedScopeFilter,proto3" json:"assigned_scope_filter,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListScopedRoleAssignmentsRequest) Reset() {
@@ -1149,20 +1167,6 @@ func (x *ListScopedRoleAssignmentsRequest) GetPageToken() string {
return ""
}
func (x *ListScopedRoleAssignmentsRequest) GetResourceScope() *v1.Filter {
if x != nil {
return x.ResourceScope
}
return nil
}
func (x *ListScopedRoleAssignmentsRequest) GetAssignedScope() *v1.Filter {
if x != nil {
return x.AssignedScope
}
return nil
}
func (x *ListScopedRoleAssignmentsRequest) GetUser() string {
if x != nil {
return x.User
@@ -1184,6 +1188,20 @@ func (x *ListScopedRoleAssignmentsRequest) GetAllCallerAssignments() bool {
return false
}
func (x *ListScopedRoleAssignmentsRequest) GetScopeFilter() *v1.Filter {
if x != nil {
return x.ScopeFilter
}
return nil
}
func (x *ListScopedRoleAssignmentsRequest) GetAssignedScopeFilter() *v1.Filter {
if x != nil {
return x.AssignedScopeFilter
}
return nil
}
func (x *ListScopedRoleAssignmentsRequest) SetPageSize(v int32) {
x.PageSize = v
}
@@ -1192,14 +1210,6 @@ func (x *ListScopedRoleAssignmentsRequest) SetPageToken(v string) {
x.PageToken = v
}
func (x *ListScopedRoleAssignmentsRequest) SetResourceScope(v *v1.Filter) {
x.ResourceScope = v
}
func (x *ListScopedRoleAssignmentsRequest) SetAssignedScope(v *v1.Filter) {
x.AssignedScope = v
}
func (x *ListScopedRoleAssignmentsRequest) SetUser(v string) {
x.User = v
}
@@ -1212,26 +1222,34 @@ func (x *ListScopedRoleAssignmentsRequest) SetAllCallerAssignments(v bool) {
x.AllCallerAssignments = v
}
func (x *ListScopedRoleAssignmentsRequest) HasResourceScope() bool {
func (x *ListScopedRoleAssignmentsRequest) SetScopeFilter(v *v1.Filter) {
x.ScopeFilter = v
}
func (x *ListScopedRoleAssignmentsRequest) SetAssignedScopeFilter(v *v1.Filter) {
x.AssignedScopeFilter = v
}
func (x *ListScopedRoleAssignmentsRequest) HasScopeFilter() bool {
if x == nil {
return false
}
return x.ResourceScope != nil
return x.ScopeFilter != nil
}
func (x *ListScopedRoleAssignmentsRequest) HasAssignedScope() bool {
func (x *ListScopedRoleAssignmentsRequest) HasAssignedScopeFilter() bool {
if x == nil {
return false
}
return x.AssignedScope != nil
return x.AssignedScopeFilter != nil
}
func (x *ListScopedRoleAssignmentsRequest) ClearResourceScope() {
x.ResourceScope = nil
func (x *ListScopedRoleAssignmentsRequest) ClearScopeFilter() {
x.ScopeFilter = nil
}
func (x *ListScopedRoleAssignmentsRequest) ClearAssignedScope() {
x.AssignedScope = nil
func (x *ListScopedRoleAssignmentsRequest) ClearAssignedScopeFilter() {
x.AssignedScopeFilter = nil
}
type ListScopedRoleAssignmentsRequest_builder struct {
@@ -1241,11 +1259,6 @@ type ListScopedRoleAssignmentsRequest_builder struct {
PageSize int32
// PageToken is the pagination cursor used to start from where a previous request left off.
PageToken string
// ResourceScope filters assignments by their resource scope if specified.
ResourceScope *v1.Filter
// AssignedScope filters assignments by the scopes they assign to if specified (note: matches assignment
// resources with 1 or more maching scopes, not all scopes within the assignment will necessarily match).
AssignedScope *v1.Filter
// User optionally limits the list to assignments for a specific user.
User string
// Role optionally limits the list to assignments for a specific role.
@@ -1255,6 +1268,15 @@ type ListScopedRoleAssignmentsRequest_builder struct {
// is specifically used to support users discovering where they have been granted privileges across scopes,
// and is not intended for general use.
AllCallerAssignments bool
// ScopeFilter is the primary scope filter. Filtering is performed against the scope of the
// resource itself. Defaults to one of EXACT or UNSCOPED depending on wether the caller is
// a scoped or unscoped identity. Exhaustive user-facing views (e.g. `tctl get`) should
// specify mode ALL.
ScopeFilter *v1.Filter
// AssignedScopeFilter filters assignments by the scopes they assign to if specified (note: matches
// assignment resources with 1 or more matching scopes, not all scopes within the assignment will
// necessarily match).
AssignedScopeFilter *v1.Filter
}
func (b0 ListScopedRoleAssignmentsRequest_builder) Build() *ListScopedRoleAssignmentsRequest {
@@ -1263,11 +1285,11 @@ func (b0 ListScopedRoleAssignmentsRequest_builder) Build() *ListScopedRoleAssign
_, _ = b, x
x.PageSize = b.PageSize
x.PageToken = b.PageToken
x.ResourceScope = b.ResourceScope
x.AssignedScope = b.AssignedScope
x.User = b.User
x.Role = b.Role
x.AllCallerAssignments = b.AllCallerAssignments
x.ScopeFilter = b.ScopeFilter
x.AssignedScopeFilter = b.AssignedScopeFilter
return m0
}
@@ -1917,15 +1939,15 @@ const file_teleport_scopes_access_v1_service_proto_rawDesc = "" +
"\x14GetScopedRoleRequest\x12\x12\n" +
"\x04name\x18\x01 \x01(\tR\x04name\"R\n" +
"\x15GetScopedRoleResponse\x129\n" +
"\x04role\x18\x01 \x01(\v2%.teleport.scopes.access.v1.ScopedRoleR\x04role\"\xff\x01\n" +
"\x04role\x18\x01 \x01(\v2%.teleport.scopes.access.v1.ScopedRoleR\x04role\"\x93\x02\n" +
"\x16ListScopedRolesRequest\x12\x1b\n" +
"\tpage_size\x18\x01 \x01(\x05R\bpageSize\x12\x1d\n" +
"\n" +
"page_token\x18\x02 \x01(\tR\tpageToken\x12A\n" +
"\x0eresource_scope\x18\x03 \x01(\v2\x1a.teleport.scopes.v1.FilterR\rresourceScope\x12E\n" +
"\x10assignable_scope\x18\x04 \x01(\v2\x1a.teleport.scopes.v1.FilterR\x0fassignableScope\x12\x1f\n" +
"page_token\x18\x02 \x01(\tR\tpageToken\x12E\n" +
"\x0eresource_scope\x18\x03 \x01(\v2\x1a.teleport.scopes.v1.FilterB\x02\x18\x01R\rresourceScope\x12\x1f\n" +
"\vname_filter\x18\x05 \x01(\tR\n" +
"nameFilter\"~\n" +
"nameFilter\x12=\n" +
"\fscope_filter\x18\x06 \x01(\v2\x1a.teleport.scopes.v1.FilterR\vscopeFilterJ\x04\b\x04\x10\x05R\x10assignable_scope\"~\n" +
"\x17ListScopedRolesResponse\x12;\n" +
"\x05roles\x18\x01 \x03(\v2%.teleport.scopes.access.v1.ScopedRoleR\x05roles\x12&\n" +
"\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\"T\n" +
@@ -1951,16 +1973,16 @@ const file_teleport_scopes_access_v1_service_proto_rawDesc = "" +
"\x1fGetScopedRoleAssignmentResponse\x12O\n" +
"\n" +
"assignment\x18\x01 \x01(\v2/.teleport.scopes.access.v1.ScopedRoleAssignmentR\n" +
"assignment\"\xc2\x02\n" +
"assignment\"\xf7\x02\n" +
" ListScopedRoleAssignmentsRequest\x12\x1b\n" +
"\tpage_size\x18\x01 \x01(\x05R\bpageSize\x12\x1d\n" +
"\n" +
"page_token\x18\x02 \x01(\tR\tpageToken\x12A\n" +
"\x0eresource_scope\x18\x03 \x01(\v2\x1a.teleport.scopes.v1.FilterR\rresourceScope\x12A\n" +
"\x0eassigned_scope\x18\x04 \x01(\v2\x1a.teleport.scopes.v1.FilterR\rassignedScope\x12\x12\n" +
"page_token\x18\x02 \x01(\tR\tpageToken\x12\x12\n" +
"\x04user\x18\x05 \x01(\tR\x04user\x12\x12\n" +
"\x04role\x18\x06 \x01(\tR\x04role\x124\n" +
"\x16all_caller_assignments\x18\a \x01(\bR\x14allCallerAssignments\"\x9e\x01\n" +
"\x16all_caller_assignments\x18\a \x01(\bR\x14allCallerAssignments\x12=\n" +
"\fscope_filter\x18\b \x01(\v2\x1a.teleport.scopes.v1.FilterR\vscopeFilter\x12N\n" +
"\x15assigned_scope_filter\x18\t \x01(\v2\x1a.teleport.scopes.v1.FilterR\x13assignedScopeFilterJ\x04\b\x03\x10\x04J\x04\b\x04\x10\x05R\x0eresource_scopeR\x0eassigned_scope\"\x9e\x01\n" +
"!ListScopedRoleAssignmentsResponse\x12Q\n" +
"\vassignments\x18\x01 \x03(\v2/.teleport.scopes.access.v1.ScopedRoleAssignmentR\vassignments\x12&\n" +
"\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\"\x8a\x01\n" +
@@ -2040,7 +2062,7 @@ var file_teleport_scopes_access_v1_service_proto_goTypes = []any{
var file_teleport_scopes_access_v1_service_proto_depIdxs = []int32{
24, // 0: teleport.scopes.access.v1.GetScopedRoleResponse.role:type_name -> teleport.scopes.access.v1.ScopedRole
25, // 1: teleport.scopes.access.v1.ListScopedRolesRequest.resource_scope:type_name -> teleport.scopes.v1.Filter
25, // 2: teleport.scopes.access.v1.ListScopedRolesRequest.assignable_scope:type_name -> teleport.scopes.v1.Filter
25, // 2: teleport.scopes.access.v1.ListScopedRolesRequest.scope_filter:type_name -> teleport.scopes.v1.Filter
24, // 3: teleport.scopes.access.v1.ListScopedRolesResponse.roles:type_name -> teleport.scopes.access.v1.ScopedRole
24, // 4: teleport.scopes.access.v1.CreateScopedRoleRequest.role:type_name -> teleport.scopes.access.v1.ScopedRole
24, // 5: teleport.scopes.access.v1.CreateScopedRoleResponse.role:type_name -> teleport.scopes.access.v1.ScopedRole
@@ -2049,8 +2071,8 @@ var file_teleport_scopes_access_v1_service_proto_depIdxs = []int32{
24, // 8: teleport.scopes.access.v1.UpsertScopedRoleRequest.role:type_name -> teleport.scopes.access.v1.ScopedRole
24, // 9: teleport.scopes.access.v1.UpsertScopedRoleResponse.role:type_name -> teleport.scopes.access.v1.ScopedRole
26, // 10: teleport.scopes.access.v1.GetScopedRoleAssignmentResponse.assignment:type_name -> teleport.scopes.access.v1.ScopedRoleAssignment
25, // 11: teleport.scopes.access.v1.ListScopedRoleAssignmentsRequest.resource_scope:type_name -> teleport.scopes.v1.Filter
25, // 12: teleport.scopes.access.v1.ListScopedRoleAssignmentsRequest.assigned_scope:type_name -> teleport.scopes.v1.Filter
25, // 11: teleport.scopes.access.v1.ListScopedRoleAssignmentsRequest.scope_filter:type_name -> teleport.scopes.v1.Filter
25, // 12: teleport.scopes.access.v1.ListScopedRoleAssignmentsRequest.assigned_scope_filter:type_name -> teleport.scopes.v1.Filter
26, // 13: teleport.scopes.access.v1.ListScopedRoleAssignmentsResponse.assignments:type_name -> teleport.scopes.access.v1.ScopedRoleAssignment
26, // 14: teleport.scopes.access.v1.CreateScopedRoleAssignmentRequest.assignment:type_name -> teleport.scopes.access.v1.ScopedRoleAssignment
26, // 15: teleport.scopes.access.v1.CreateScopedRoleAssignmentResponse.assignment:type_name -> teleport.scopes.access.v1.ScopedRoleAssignment
@@ -168,14 +168,14 @@ func (b0 GetScopedRoleResponse_builder) Build() *GetScopedRoleResponse {
// ListScopedRolesRequest is the request to list scoped roles.
type ListScopedRolesRequest struct {
state protoimpl.MessageState `protogen:"opaque.v1"`
xxx_hidden_PageSize int32 `protobuf:"varint,1,opt,name=page_size,json=pageSize,proto3"`
xxx_hidden_PageToken string `protobuf:"bytes,2,opt,name=page_token,json=pageToken,proto3"`
xxx_hidden_ResourceScope *v1.Filter `protobuf:"bytes,3,opt,name=resource_scope,json=resourceScope,proto3"`
xxx_hidden_AssignableScope *v1.Filter `protobuf:"bytes,4,opt,name=assignable_scope,json=assignableScope,proto3"`
xxx_hidden_NameFilter string `protobuf:"bytes,5,opt,name=name_filter,json=nameFilter,proto3"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"opaque.v1"`
xxx_hidden_PageSize int32 `protobuf:"varint,1,opt,name=page_size,json=pageSize,proto3"`
xxx_hidden_PageToken string `protobuf:"bytes,2,opt,name=page_token,json=pageToken,proto3"`
xxx_hidden_ResourceScope *v1.Filter `protobuf:"bytes,3,opt,name=resource_scope,json=resourceScope,proto3"`
xxx_hidden_NameFilter string `protobuf:"bytes,5,opt,name=name_filter,json=nameFilter,proto3"`
xxx_hidden_ScopeFilter *v1.Filter `protobuf:"bytes,6,opt,name=scope_filter,json=scopeFilter,proto3"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListScopedRolesRequest) Reset() {
@@ -217,6 +217,7 @@ func (x *ListScopedRolesRequest) GetPageToken() string {
return ""
}
// Deprecated: Marked as deprecated in teleport/scopes/access/v1/service.proto.
func (x *ListScopedRolesRequest) GetResourceScope() *v1.Filter {
if x != nil {
return x.xxx_hidden_ResourceScope
@@ -224,13 +225,6 @@ func (x *ListScopedRolesRequest) GetResourceScope() *v1.Filter {
return nil
}
func (x *ListScopedRolesRequest) GetAssignableScope() *v1.Filter {
if x != nil {
return x.xxx_hidden_AssignableScope
}
return nil
}
func (x *ListScopedRolesRequest) GetNameFilter() string {
if x != nil {
return x.xxx_hidden_NameFilter
@@ -238,6 +232,13 @@ func (x *ListScopedRolesRequest) GetNameFilter() string {
return ""
}
func (x *ListScopedRolesRequest) GetScopeFilter() *v1.Filter {
if x != nil {
return x.xxx_hidden_ScopeFilter
}
return nil
}
func (x *ListScopedRolesRequest) SetPageSize(v int32) {
x.xxx_hidden_PageSize = v
}
@@ -246,18 +247,20 @@ func (x *ListScopedRolesRequest) SetPageToken(v string) {
x.xxx_hidden_PageToken = v
}
// Deprecated: Marked as deprecated in teleport/scopes/access/v1/service.proto.
func (x *ListScopedRolesRequest) SetResourceScope(v *v1.Filter) {
x.xxx_hidden_ResourceScope = v
}
func (x *ListScopedRolesRequest) SetAssignableScope(v *v1.Filter) {
x.xxx_hidden_AssignableScope = v
}
func (x *ListScopedRolesRequest) SetNameFilter(v string) {
x.xxx_hidden_NameFilter = v
}
func (x *ListScopedRolesRequest) SetScopeFilter(v *v1.Filter) {
x.xxx_hidden_ScopeFilter = v
}
// Deprecated: Marked as deprecated in teleport/scopes/access/v1/service.proto.
func (x *ListScopedRolesRequest) HasResourceScope() bool {
if x == nil {
return false
@@ -265,19 +268,20 @@ func (x *ListScopedRolesRequest) HasResourceScope() bool {
return x.xxx_hidden_ResourceScope != nil
}
func (x *ListScopedRolesRequest) HasAssignableScope() bool {
func (x *ListScopedRolesRequest) HasScopeFilter() bool {
if x == nil {
return false
}
return x.xxx_hidden_AssignableScope != nil
return x.xxx_hidden_ScopeFilter != nil
}
// Deprecated: Marked as deprecated in teleport/scopes/access/v1/service.proto.
func (x *ListScopedRolesRequest) ClearResourceScope() {
x.xxx_hidden_ResourceScope = nil
}
func (x *ListScopedRolesRequest) ClearAssignableScope() {
x.xxx_hidden_AssignableScope = nil
func (x *ListScopedRolesRequest) ClearScopeFilter() {
x.xxx_hidden_ScopeFilter = nil
}
type ListScopedRolesRequest_builder struct {
@@ -288,12 +292,17 @@ type ListScopedRolesRequest_builder struct {
// PageToken is the pagination cursor used to start from where a previous request left off.
PageToken string
// ResourceScope filters roles by their resource scope if specified.
//
// Deprecated: Marked as deprecated in teleport/scopes/access/v1/service.proto.
ResourceScope *v1.Filter
// AssignableScope filters roles by their assignable scope if specified.
AssignableScope *v1.Filter
// NameFilter filters roles by their name using a case-insensitive substring
// match if specified.
NameFilter string
// ScopeFilter is the primary scope filter. Filtering is performed against the scope of the
// resource itself. Defaults to one of EXACT or UNSCOPED depending on wether the caller is
// a scoped or unscoped identity. Exhaustive user-facing views (e.g. `tctl get`) should
// specify mode ALL.
ScopeFilter *v1.Filter
}
func (b0 ListScopedRolesRequest_builder) Build() *ListScopedRolesRequest {
@@ -303,8 +312,8 @@ func (b0 ListScopedRolesRequest_builder) Build() *ListScopedRolesRequest {
x.xxx_hidden_PageSize = b.PageSize
x.xxx_hidden_PageToken = b.PageToken
x.xxx_hidden_ResourceScope = b.ResourceScope
x.xxx_hidden_AssignableScope = b.AssignableScope
x.xxx_hidden_NameFilter = b.NameFilter
x.xxx_hidden_ScopeFilter = b.ScopeFilter
return m0
}
@@ -1071,11 +1080,11 @@ type ListScopedRoleAssignmentsRequest struct {
state protoimpl.MessageState `protogen:"opaque.v1"`
xxx_hidden_PageSize int32 `protobuf:"varint,1,opt,name=page_size,json=pageSize,proto3"`
xxx_hidden_PageToken string `protobuf:"bytes,2,opt,name=page_token,json=pageToken,proto3"`
xxx_hidden_ResourceScope *v1.Filter `protobuf:"bytes,3,opt,name=resource_scope,json=resourceScope,proto3"`
xxx_hidden_AssignedScope *v1.Filter `protobuf:"bytes,4,opt,name=assigned_scope,json=assignedScope,proto3"`
xxx_hidden_User string `protobuf:"bytes,5,opt,name=user,proto3"`
xxx_hidden_Role string `protobuf:"bytes,6,opt,name=role,proto3"`
xxx_hidden_AllCallerAssignments bool `protobuf:"varint,7,opt,name=all_caller_assignments,json=allCallerAssignments,proto3"`
xxx_hidden_ScopeFilter *v1.Filter `protobuf:"bytes,8,opt,name=scope_filter,json=scopeFilter,proto3"`
xxx_hidden_AssignedScopeFilter *v1.Filter `protobuf:"bytes,9,opt,name=assigned_scope_filter,json=assignedScopeFilter,proto3"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -1119,20 +1128,6 @@ func (x *ListScopedRoleAssignmentsRequest) GetPageToken() string {
return ""
}
func (x *ListScopedRoleAssignmentsRequest) GetResourceScope() *v1.Filter {
if x != nil {
return x.xxx_hidden_ResourceScope
}
return nil
}
func (x *ListScopedRoleAssignmentsRequest) GetAssignedScope() *v1.Filter {
if x != nil {
return x.xxx_hidden_AssignedScope
}
return nil
}
func (x *ListScopedRoleAssignmentsRequest) GetUser() string {
if x != nil {
return x.xxx_hidden_User
@@ -1154,6 +1149,20 @@ func (x *ListScopedRoleAssignmentsRequest) GetAllCallerAssignments() bool {
return false
}
func (x *ListScopedRoleAssignmentsRequest) GetScopeFilter() *v1.Filter {
if x != nil {
return x.xxx_hidden_ScopeFilter
}
return nil
}
func (x *ListScopedRoleAssignmentsRequest) GetAssignedScopeFilter() *v1.Filter {
if x != nil {
return x.xxx_hidden_AssignedScopeFilter
}
return nil
}
func (x *ListScopedRoleAssignmentsRequest) SetPageSize(v int32) {
x.xxx_hidden_PageSize = v
}
@@ -1162,14 +1171,6 @@ func (x *ListScopedRoleAssignmentsRequest) SetPageToken(v string) {
x.xxx_hidden_PageToken = v
}
func (x *ListScopedRoleAssignmentsRequest) SetResourceScope(v *v1.Filter) {
x.xxx_hidden_ResourceScope = v
}
func (x *ListScopedRoleAssignmentsRequest) SetAssignedScope(v *v1.Filter) {
x.xxx_hidden_AssignedScope = v
}
func (x *ListScopedRoleAssignmentsRequest) SetUser(v string) {
x.xxx_hidden_User = v
}
@@ -1182,26 +1183,34 @@ func (x *ListScopedRoleAssignmentsRequest) SetAllCallerAssignments(v bool) {
x.xxx_hidden_AllCallerAssignments = v
}
func (x *ListScopedRoleAssignmentsRequest) HasResourceScope() bool {
func (x *ListScopedRoleAssignmentsRequest) SetScopeFilter(v *v1.Filter) {
x.xxx_hidden_ScopeFilter = v
}
func (x *ListScopedRoleAssignmentsRequest) SetAssignedScopeFilter(v *v1.Filter) {
x.xxx_hidden_AssignedScopeFilter = v
}
func (x *ListScopedRoleAssignmentsRequest) HasScopeFilter() bool {
if x == nil {
return false
}
return x.xxx_hidden_ResourceScope != nil
return x.xxx_hidden_ScopeFilter != nil
}
func (x *ListScopedRoleAssignmentsRequest) HasAssignedScope() bool {
func (x *ListScopedRoleAssignmentsRequest) HasAssignedScopeFilter() bool {
if x == nil {
return false
}
return x.xxx_hidden_AssignedScope != nil
return x.xxx_hidden_AssignedScopeFilter != nil
}
func (x *ListScopedRoleAssignmentsRequest) ClearResourceScope() {
x.xxx_hidden_ResourceScope = nil
func (x *ListScopedRoleAssignmentsRequest) ClearScopeFilter() {
x.xxx_hidden_ScopeFilter = nil
}
func (x *ListScopedRoleAssignmentsRequest) ClearAssignedScope() {
x.xxx_hidden_AssignedScope = nil
func (x *ListScopedRoleAssignmentsRequest) ClearAssignedScopeFilter() {
x.xxx_hidden_AssignedScopeFilter = nil
}
type ListScopedRoleAssignmentsRequest_builder struct {
@@ -1211,11 +1220,6 @@ type ListScopedRoleAssignmentsRequest_builder struct {
PageSize int32
// PageToken is the pagination cursor used to start from where a previous request left off.
PageToken string
// ResourceScope filters assignments by their resource scope if specified.
ResourceScope *v1.Filter
// AssignedScope filters assignments by the scopes they assign to if specified (note: matches assignment
// resources with 1 or more maching scopes, not all scopes within the assignment will necessarily match).
AssignedScope *v1.Filter
// User optionally limits the list to assignments for a specific user.
User string
// Role optionally limits the list to assignments for a specific role.
@@ -1225,6 +1229,15 @@ type ListScopedRoleAssignmentsRequest_builder struct {
// is specifically used to support users discovering where they have been granted privileges across scopes,
// and is not intended for general use.
AllCallerAssignments bool
// ScopeFilter is the primary scope filter. Filtering is performed against the scope of the
// resource itself. Defaults to one of EXACT or UNSCOPED depending on wether the caller is
// a scoped or unscoped identity. Exhaustive user-facing views (e.g. `tctl get`) should
// specify mode ALL.
ScopeFilter *v1.Filter
// AssignedScopeFilter filters assignments by the scopes they assign to if specified (note: matches
// assignment resources with 1 or more matching scopes, not all scopes within the assignment will
// necessarily match).
AssignedScopeFilter *v1.Filter
}
func (b0 ListScopedRoleAssignmentsRequest_builder) Build() *ListScopedRoleAssignmentsRequest {
@@ -1233,11 +1246,11 @@ func (b0 ListScopedRoleAssignmentsRequest_builder) Build() *ListScopedRoleAssign
_, _ = b, x
x.xxx_hidden_PageSize = b.PageSize
x.xxx_hidden_PageToken = b.PageToken
x.xxx_hidden_ResourceScope = b.ResourceScope
x.xxx_hidden_AssignedScope = b.AssignedScope
x.xxx_hidden_User = b.User
x.xxx_hidden_Role = b.Role
x.xxx_hidden_AllCallerAssignments = b.AllCallerAssignments
x.xxx_hidden_ScopeFilter = b.ScopeFilter
x.xxx_hidden_AssignedScopeFilter = b.AssignedScopeFilter
return m0
}
@@ -1878,15 +1891,15 @@ const file_teleport_scopes_access_v1_service_proto_rawDesc = "" +
"\x14GetScopedRoleRequest\x12\x12\n" +
"\x04name\x18\x01 \x01(\tR\x04name\"R\n" +
"\x15GetScopedRoleResponse\x129\n" +
"\x04role\x18\x01 \x01(\v2%.teleport.scopes.access.v1.ScopedRoleR\x04role\"\xff\x01\n" +
"\x04role\x18\x01 \x01(\v2%.teleport.scopes.access.v1.ScopedRoleR\x04role\"\x93\x02\n" +
"\x16ListScopedRolesRequest\x12\x1b\n" +
"\tpage_size\x18\x01 \x01(\x05R\bpageSize\x12\x1d\n" +
"\n" +
"page_token\x18\x02 \x01(\tR\tpageToken\x12A\n" +
"\x0eresource_scope\x18\x03 \x01(\v2\x1a.teleport.scopes.v1.FilterR\rresourceScope\x12E\n" +
"\x10assignable_scope\x18\x04 \x01(\v2\x1a.teleport.scopes.v1.FilterR\x0fassignableScope\x12\x1f\n" +
"page_token\x18\x02 \x01(\tR\tpageToken\x12E\n" +
"\x0eresource_scope\x18\x03 \x01(\v2\x1a.teleport.scopes.v1.FilterB\x02\x18\x01R\rresourceScope\x12\x1f\n" +
"\vname_filter\x18\x05 \x01(\tR\n" +
"nameFilter\"~\n" +
"nameFilter\x12=\n" +
"\fscope_filter\x18\x06 \x01(\v2\x1a.teleport.scopes.v1.FilterR\vscopeFilterJ\x04\b\x04\x10\x05R\x10assignable_scope\"~\n" +
"\x17ListScopedRolesResponse\x12;\n" +
"\x05roles\x18\x01 \x03(\v2%.teleport.scopes.access.v1.ScopedRoleR\x05roles\x12&\n" +
"\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\"T\n" +
@@ -1912,16 +1925,16 @@ const file_teleport_scopes_access_v1_service_proto_rawDesc = "" +
"\x1fGetScopedRoleAssignmentResponse\x12O\n" +
"\n" +
"assignment\x18\x01 \x01(\v2/.teleport.scopes.access.v1.ScopedRoleAssignmentR\n" +
"assignment\"\xc2\x02\n" +
"assignment\"\xf7\x02\n" +
" ListScopedRoleAssignmentsRequest\x12\x1b\n" +
"\tpage_size\x18\x01 \x01(\x05R\bpageSize\x12\x1d\n" +
"\n" +
"page_token\x18\x02 \x01(\tR\tpageToken\x12A\n" +
"\x0eresource_scope\x18\x03 \x01(\v2\x1a.teleport.scopes.v1.FilterR\rresourceScope\x12A\n" +
"\x0eassigned_scope\x18\x04 \x01(\v2\x1a.teleport.scopes.v1.FilterR\rassignedScope\x12\x12\n" +
"page_token\x18\x02 \x01(\tR\tpageToken\x12\x12\n" +
"\x04user\x18\x05 \x01(\tR\x04user\x12\x12\n" +
"\x04role\x18\x06 \x01(\tR\x04role\x124\n" +
"\x16all_caller_assignments\x18\a \x01(\bR\x14allCallerAssignments\"\x9e\x01\n" +
"\x16all_caller_assignments\x18\a \x01(\bR\x14allCallerAssignments\x12=\n" +
"\fscope_filter\x18\b \x01(\v2\x1a.teleport.scopes.v1.FilterR\vscopeFilter\x12N\n" +
"\x15assigned_scope_filter\x18\t \x01(\v2\x1a.teleport.scopes.v1.FilterR\x13assignedScopeFilterJ\x04\b\x03\x10\x04J\x04\b\x04\x10\x05R\x0eresource_scopeR\x0eassigned_scope\"\x9e\x01\n" +
"!ListScopedRoleAssignmentsResponse\x12Q\n" +
"\vassignments\x18\x01 \x03(\v2/.teleport.scopes.access.v1.ScopedRoleAssignmentR\vassignments\x12&\n" +
"\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\"\x8a\x01\n" +
@@ -2001,7 +2014,7 @@ var file_teleport_scopes_access_v1_service_proto_goTypes = []any{
var file_teleport_scopes_access_v1_service_proto_depIdxs = []int32{
24, // 0: teleport.scopes.access.v1.GetScopedRoleResponse.role:type_name -> teleport.scopes.access.v1.ScopedRole
25, // 1: teleport.scopes.access.v1.ListScopedRolesRequest.resource_scope:type_name -> teleport.scopes.v1.Filter
25, // 2: teleport.scopes.access.v1.ListScopedRolesRequest.assignable_scope:type_name -> teleport.scopes.v1.Filter
25, // 2: teleport.scopes.access.v1.ListScopedRolesRequest.scope_filter:type_name -> teleport.scopes.v1.Filter
24, // 3: teleport.scopes.access.v1.ListScopedRolesResponse.roles:type_name -> teleport.scopes.access.v1.ScopedRole
24, // 4: teleport.scopes.access.v1.CreateScopedRoleRequest.role:type_name -> teleport.scopes.access.v1.ScopedRole
24, // 5: teleport.scopes.access.v1.CreateScopedRoleResponse.role:type_name -> teleport.scopes.access.v1.ScopedRole
@@ -2010,8 +2023,8 @@ var file_teleport_scopes_access_v1_service_proto_depIdxs = []int32{
24, // 8: teleport.scopes.access.v1.UpsertScopedRoleRequest.role:type_name -> teleport.scopes.access.v1.ScopedRole
24, // 9: teleport.scopes.access.v1.UpsertScopedRoleResponse.role:type_name -> teleport.scopes.access.v1.ScopedRole
26, // 10: teleport.scopes.access.v1.GetScopedRoleAssignmentResponse.assignment:type_name -> teleport.scopes.access.v1.ScopedRoleAssignment
25, // 11: teleport.scopes.access.v1.ListScopedRoleAssignmentsRequest.resource_scope:type_name -> teleport.scopes.v1.Filter
25, // 12: teleport.scopes.access.v1.ListScopedRoleAssignmentsRequest.assigned_scope:type_name -> teleport.scopes.v1.Filter
25, // 11: teleport.scopes.access.v1.ListScopedRoleAssignmentsRequest.scope_filter:type_name -> teleport.scopes.v1.Filter
25, // 12: teleport.scopes.access.v1.ListScopedRoleAssignmentsRequest.assigned_scope_filter:type_name -> teleport.scopes.v1.Filter
26, // 13: teleport.scopes.access.v1.ListScopedRoleAssignmentsResponse.assignments:type_name -> teleport.scopes.access.v1.ScopedRoleAssignment
26, // 14: teleport.scopes.access.v1.CreateScopedRoleAssignmentRequest.assignment:type_name -> teleport.scopes.access.v1.ScopedRoleAssignment
26, // 15: teleport.scopes.access.v1.CreateScopedRoleAssignmentResponse.assignment:type_name -> teleport.scopes.access.v1.ScopedRoleAssignment
@@ -203,10 +203,6 @@ func (b0 GetScopedTokenResponse_builder) Build() *GetScopedTokenResponse {
// ListScopedTokensRequest is the request to list scoped tokens.
type ListScopedTokensRequest struct {
state protoimpl.MessageState `protogen:"hybrid.v1"`
// Filter tokens by their resource scope.
ResourceScope *v1.Filter `protobuf:"bytes,1,opt,name=resource_scope,json=resourceScope,proto3" json:"resource_scope,omitempty"`
// Filter tokens by their assigned scope.
AssignedScope *v1.Filter `protobuf:"bytes,2,opt,name=assigned_scope,json=assignedScope,proto3" json:"assigned_scope,omitempty"`
// The pagination cursor.
Cursor string `protobuf:"bytes,3,opt,name=cursor,proto3" json:"cursor,omitempty"`
// The maximum number of results to return.
@@ -216,9 +212,16 @@ type ListScopedTokensRequest struct {
// Filter tokens that match all provided labels.
Labels map[string]string `protobuf:"bytes,6,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
// If true, include the token secrets in the response.
WithSecrets bool `protobuf:"varint,7,opt,name=with_secrets,json=withSecrets,proto3" json:"with_secrets,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
WithSecrets bool `protobuf:"varint,7,opt,name=with_secrets,json=withSecrets,proto3" json:"with_secrets,omitempty"`
// ScopeFilter is the primary scope filter. Filtering is performed against the scope of the
// resource itself. Defaults to one of EXACT or UNSCOPED depending on wether the caller is
// a scoped or unscoped identity. Exhaustive user-facing views (e.g. `tctl get`) should
// specify mode ALL.
ScopeFilter *v1.Filter `protobuf:"bytes,8,opt,name=scope_filter,json=scopeFilter,proto3" json:"scope_filter,omitempty"`
// AssignedScopeFilter filters tokens by their assigned scope if specified.
AssignedScopeFilter *v1.Filter `protobuf:"bytes,9,opt,name=assigned_scope_filter,json=assignedScopeFilter,proto3" json:"assigned_scope_filter,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListScopedTokensRequest) Reset() {
@@ -246,20 +249,6 @@ func (x *ListScopedTokensRequest) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x)
}
func (x *ListScopedTokensRequest) GetResourceScope() *v1.Filter {
if x != nil {
return x.ResourceScope
}
return nil
}
func (x *ListScopedTokensRequest) GetAssignedScope() *v1.Filter {
if x != nil {
return x.AssignedScope
}
return nil
}
func (x *ListScopedTokensRequest) GetCursor() string {
if x != nil {
return x.Cursor
@@ -295,12 +284,18 @@ func (x *ListScopedTokensRequest) GetWithSecrets() bool {
return false
}
func (x *ListScopedTokensRequest) SetResourceScope(v *v1.Filter) {
x.ResourceScope = v
func (x *ListScopedTokensRequest) GetScopeFilter() *v1.Filter {
if x != nil {
return x.ScopeFilter
}
return nil
}
func (x *ListScopedTokensRequest) SetAssignedScope(v *v1.Filter) {
x.AssignedScope = v
func (x *ListScopedTokensRequest) GetAssignedScopeFilter() *v1.Filter {
if x != nil {
return x.AssignedScopeFilter
}
return nil
}
func (x *ListScopedTokensRequest) SetCursor(v string) {
@@ -323,35 +318,39 @@ func (x *ListScopedTokensRequest) SetWithSecrets(v bool) {
x.WithSecrets = v
}
func (x *ListScopedTokensRequest) HasResourceScope() bool {
func (x *ListScopedTokensRequest) SetScopeFilter(v *v1.Filter) {
x.ScopeFilter = v
}
func (x *ListScopedTokensRequest) SetAssignedScopeFilter(v *v1.Filter) {
x.AssignedScopeFilter = v
}
func (x *ListScopedTokensRequest) HasScopeFilter() bool {
if x == nil {
return false
}
return x.ResourceScope != nil
return x.ScopeFilter != nil
}
func (x *ListScopedTokensRequest) HasAssignedScope() bool {
func (x *ListScopedTokensRequest) HasAssignedScopeFilter() bool {
if x == nil {
return false
}
return x.AssignedScope != nil
return x.AssignedScopeFilter != nil
}
func (x *ListScopedTokensRequest) ClearResourceScope() {
x.ResourceScope = nil
func (x *ListScopedTokensRequest) ClearScopeFilter() {
x.ScopeFilter = nil
}
func (x *ListScopedTokensRequest) ClearAssignedScope() {
x.AssignedScope = nil
func (x *ListScopedTokensRequest) ClearAssignedScopeFilter() {
x.AssignedScopeFilter = nil
}
type ListScopedTokensRequest_builder struct {
_ [0]func() // Prevents comparability and use of unkeyed literals for the builder.
// Filter tokens by their resource scope.
ResourceScope *v1.Filter
// Filter tokens by their assigned scope.
AssignedScope *v1.Filter
// The pagination cursor.
Cursor string
// The maximum number of results to return.
@@ -362,19 +361,26 @@ type ListScopedTokensRequest_builder struct {
Labels map[string]string
// If true, include the token secrets in the response.
WithSecrets bool
// ScopeFilter is the primary scope filter. Filtering is performed against the scope of the
// resource itself. Defaults to one of EXACT or UNSCOPED depending on wether the caller is
// a scoped or unscoped identity. Exhaustive user-facing views (e.g. `tctl get`) should
// specify mode ALL.
ScopeFilter *v1.Filter
// AssignedScopeFilter filters tokens by their assigned scope if specified.
AssignedScopeFilter *v1.Filter
}
func (b0 ListScopedTokensRequest_builder) Build() *ListScopedTokensRequest {
m0 := &ListScopedTokensRequest{}
b, x := &b0, m0
_, _ = b, x
x.ResourceScope = b.ResourceScope
x.AssignedScope = b.AssignedScope
x.Cursor = b.Cursor
x.Limit = b.Limit
x.Roles = b.Roles
x.Labels = b.Labels
x.WithSecrets = b.WithSecrets
x.ScopeFilter = b.ScopeFilter
x.AssignedScopeFilter = b.AssignedScopeFilter
return m0
}
@@ -1027,18 +1033,18 @@ const file_teleport_scopes_joining_v1_service_proto_rawDesc = "" +
"withSecret\x12\x14\n" +
"\x05scope\x18\x03 \x01(\tR\x05scope\"W\n" +
"\x16GetScopedTokenResponse\x12=\n" +
"\x05token\x18\x01 \x01(\v2'.teleport.scopes.joining.v1.ScopedTokenR\x05token\"\x9a\x03\n" +
"\x17ListScopedTokensRequest\x12A\n" +
"\x0eresource_scope\x18\x01 \x01(\v2\x1a.teleport.scopes.v1.FilterR\rresourceScope\x12A\n" +
"\x0eassigned_scope\x18\x02 \x01(\v2\x1a.teleport.scopes.v1.FilterR\rassignedScope\x12\x16\n" +
"\x05token\x18\x01 \x01(\v2'.teleport.scopes.joining.v1.ScopedTokenR\x05token\"\xcf\x03\n" +
"\x17ListScopedTokensRequest\x12\x16\n" +
"\x06cursor\x18\x03 \x01(\tR\x06cursor\x12\x14\n" +
"\x05limit\x18\x04 \x01(\rR\x05limit\x12\x14\n" +
"\x05roles\x18\x05 \x03(\tR\x05roles\x12W\n" +
"\x06labels\x18\x06 \x03(\v2?.teleport.scopes.joining.v1.ListScopedTokensRequest.LabelsEntryR\x06labels\x12!\n" +
"\fwith_secrets\x18\a \x01(\bR\vwithSecrets\x1a9\n" +
"\fwith_secrets\x18\a \x01(\bR\vwithSecrets\x12=\n" +
"\fscope_filter\x18\b \x01(\v2\x1a.teleport.scopes.v1.FilterR\vscopeFilter\x12N\n" +
"\x15assigned_scope_filter\x18\t \x01(\v2\x1a.teleport.scopes.v1.FilterR\x13assignedScopeFilter\x1a9\n" +
"\vLabelsEntry\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"s\n" +
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01J\x04\b\x01\x10\x02J\x04\b\x02\x10\x03R\x0eresource_scopeR\x0eassigned_scope\"s\n" +
"\x18ListScopedTokensResponse\x12?\n" +
"\x06tokens\x18\x01 \x03(\v2'.teleport.scopes.joining.v1.ScopedTokenR\x06tokens\x12\x16\n" +
"\x06cursor\x18\x02 \x01(\tR\x06cursor\"Y\n" +
@@ -1087,9 +1093,9 @@ var file_teleport_scopes_joining_v1_service_proto_goTypes = []any{
}
var file_teleport_scopes_joining_v1_service_proto_depIdxs = []int32{
13, // 0: teleport.scopes.joining.v1.GetScopedTokenResponse.token:type_name -> teleport.scopes.joining.v1.ScopedToken
14, // 1: teleport.scopes.joining.v1.ListScopedTokensRequest.resource_scope:type_name -> teleport.scopes.v1.Filter
14, // 2: teleport.scopes.joining.v1.ListScopedTokensRequest.assigned_scope:type_name -> teleport.scopes.v1.Filter
12, // 3: teleport.scopes.joining.v1.ListScopedTokensRequest.labels:type_name -> teleport.scopes.joining.v1.ListScopedTokensRequest.LabelsEntry
12, // 1: teleport.scopes.joining.v1.ListScopedTokensRequest.labels:type_name -> teleport.scopes.joining.v1.ListScopedTokensRequest.LabelsEntry
14, // 2: teleport.scopes.joining.v1.ListScopedTokensRequest.scope_filter:type_name -> teleport.scopes.v1.Filter
14, // 3: teleport.scopes.joining.v1.ListScopedTokensRequest.assigned_scope_filter:type_name -> teleport.scopes.v1.Filter
13, // 4: teleport.scopes.joining.v1.ListScopedTokensResponse.tokens:type_name -> teleport.scopes.joining.v1.ScopedToken
13, // 5: teleport.scopes.joining.v1.CreateScopedTokenRequest.token:type_name -> teleport.scopes.joining.v1.ScopedToken
13, // 6: teleport.scopes.joining.v1.CreateScopedTokenResponse.token:type_name -> teleport.scopes.joining.v1.ScopedToken
@@ -198,16 +198,16 @@ func (b0 GetScopedTokenResponse_builder) Build() *GetScopedTokenResponse {
// ListScopedTokensRequest is the request to list scoped tokens.
type ListScopedTokensRequest struct {
state protoimpl.MessageState `protogen:"opaque.v1"`
xxx_hidden_ResourceScope *v1.Filter `protobuf:"bytes,1,opt,name=resource_scope,json=resourceScope,proto3"`
xxx_hidden_AssignedScope *v1.Filter `protobuf:"bytes,2,opt,name=assigned_scope,json=assignedScope,proto3"`
xxx_hidden_Cursor string `protobuf:"bytes,3,opt,name=cursor,proto3"`
xxx_hidden_Limit uint32 `protobuf:"varint,4,opt,name=limit,proto3"`
xxx_hidden_Roles []string `protobuf:"bytes,5,rep,name=roles,proto3"`
xxx_hidden_Labels map[string]string `protobuf:"bytes,6,rep,name=labels,proto3" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
xxx_hidden_WithSecrets bool `protobuf:"varint,7,opt,name=with_secrets,json=withSecrets,proto3"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"opaque.v1"`
xxx_hidden_Cursor string `protobuf:"bytes,3,opt,name=cursor,proto3"`
xxx_hidden_Limit uint32 `protobuf:"varint,4,opt,name=limit,proto3"`
xxx_hidden_Roles []string `protobuf:"bytes,5,rep,name=roles,proto3"`
xxx_hidden_Labels map[string]string `protobuf:"bytes,6,rep,name=labels,proto3" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
xxx_hidden_WithSecrets bool `protobuf:"varint,7,opt,name=with_secrets,json=withSecrets,proto3"`
xxx_hidden_ScopeFilter *v1.Filter `protobuf:"bytes,8,opt,name=scope_filter,json=scopeFilter,proto3"`
xxx_hidden_AssignedScopeFilter *v1.Filter `protobuf:"bytes,9,opt,name=assigned_scope_filter,json=assignedScopeFilter,proto3"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListScopedTokensRequest) Reset() {
@@ -235,20 +235,6 @@ func (x *ListScopedTokensRequest) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x)
}
func (x *ListScopedTokensRequest) GetResourceScope() *v1.Filter {
if x != nil {
return x.xxx_hidden_ResourceScope
}
return nil
}
func (x *ListScopedTokensRequest) GetAssignedScope() *v1.Filter {
if x != nil {
return x.xxx_hidden_AssignedScope
}
return nil
}
func (x *ListScopedTokensRequest) GetCursor() string {
if x != nil {
return x.xxx_hidden_Cursor
@@ -284,12 +270,18 @@ func (x *ListScopedTokensRequest) GetWithSecrets() bool {
return false
}
func (x *ListScopedTokensRequest) SetResourceScope(v *v1.Filter) {
x.xxx_hidden_ResourceScope = v
func (x *ListScopedTokensRequest) GetScopeFilter() *v1.Filter {
if x != nil {
return x.xxx_hidden_ScopeFilter
}
return nil
}
func (x *ListScopedTokensRequest) SetAssignedScope(v *v1.Filter) {
x.xxx_hidden_AssignedScope = v
func (x *ListScopedTokensRequest) GetAssignedScopeFilter() *v1.Filter {
if x != nil {
return x.xxx_hidden_AssignedScopeFilter
}
return nil
}
func (x *ListScopedTokensRequest) SetCursor(v string) {
@@ -312,35 +304,39 @@ func (x *ListScopedTokensRequest) SetWithSecrets(v bool) {
x.xxx_hidden_WithSecrets = v
}
func (x *ListScopedTokensRequest) HasResourceScope() bool {
func (x *ListScopedTokensRequest) SetScopeFilter(v *v1.Filter) {
x.xxx_hidden_ScopeFilter = v
}
func (x *ListScopedTokensRequest) SetAssignedScopeFilter(v *v1.Filter) {
x.xxx_hidden_AssignedScopeFilter = v
}
func (x *ListScopedTokensRequest) HasScopeFilter() bool {
if x == nil {
return false
}
return x.xxx_hidden_ResourceScope != nil
return x.xxx_hidden_ScopeFilter != nil
}
func (x *ListScopedTokensRequest) HasAssignedScope() bool {
func (x *ListScopedTokensRequest) HasAssignedScopeFilter() bool {
if x == nil {
return false
}
return x.xxx_hidden_AssignedScope != nil
return x.xxx_hidden_AssignedScopeFilter != nil
}
func (x *ListScopedTokensRequest) ClearResourceScope() {
x.xxx_hidden_ResourceScope = nil
func (x *ListScopedTokensRequest) ClearScopeFilter() {
x.xxx_hidden_ScopeFilter = nil
}
func (x *ListScopedTokensRequest) ClearAssignedScope() {
x.xxx_hidden_AssignedScope = nil
func (x *ListScopedTokensRequest) ClearAssignedScopeFilter() {
x.xxx_hidden_AssignedScopeFilter = nil
}
type ListScopedTokensRequest_builder struct {
_ [0]func() // Prevents comparability and use of unkeyed literals for the builder.
// Filter tokens by their resource scope.
ResourceScope *v1.Filter
// Filter tokens by their assigned scope.
AssignedScope *v1.Filter
// The pagination cursor.
Cursor string
// The maximum number of results to return.
@@ -351,19 +347,26 @@ type ListScopedTokensRequest_builder struct {
Labels map[string]string
// If true, include the token secrets in the response.
WithSecrets bool
// ScopeFilter is the primary scope filter. Filtering is performed against the scope of the
// resource itself. Defaults to one of EXACT or UNSCOPED depending on wether the caller is
// a scoped or unscoped identity. Exhaustive user-facing views (e.g. `tctl get`) should
// specify mode ALL.
ScopeFilter *v1.Filter
// AssignedScopeFilter filters tokens by their assigned scope if specified.
AssignedScopeFilter *v1.Filter
}
func (b0 ListScopedTokensRequest_builder) Build() *ListScopedTokensRequest {
m0 := &ListScopedTokensRequest{}
b, x := &b0, m0
_, _ = b, x
x.xxx_hidden_ResourceScope = b.ResourceScope
x.xxx_hidden_AssignedScope = b.AssignedScope
x.xxx_hidden_Cursor = b.Cursor
x.xxx_hidden_Limit = b.Limit
x.xxx_hidden_Roles = b.Roles
x.xxx_hidden_Labels = b.Labels
x.xxx_hidden_WithSecrets = b.WithSecrets
x.xxx_hidden_ScopeFilter = b.ScopeFilter
x.xxx_hidden_AssignedScopeFilter = b.AssignedScopeFilter
return m0
}
@@ -1007,18 +1010,18 @@ const file_teleport_scopes_joining_v1_service_proto_rawDesc = "" +
"withSecret\x12\x14\n" +
"\x05scope\x18\x03 \x01(\tR\x05scope\"W\n" +
"\x16GetScopedTokenResponse\x12=\n" +
"\x05token\x18\x01 \x01(\v2'.teleport.scopes.joining.v1.ScopedTokenR\x05token\"\x9a\x03\n" +
"\x17ListScopedTokensRequest\x12A\n" +
"\x0eresource_scope\x18\x01 \x01(\v2\x1a.teleport.scopes.v1.FilterR\rresourceScope\x12A\n" +
"\x0eassigned_scope\x18\x02 \x01(\v2\x1a.teleport.scopes.v1.FilterR\rassignedScope\x12\x16\n" +
"\x05token\x18\x01 \x01(\v2'.teleport.scopes.joining.v1.ScopedTokenR\x05token\"\xcf\x03\n" +
"\x17ListScopedTokensRequest\x12\x16\n" +
"\x06cursor\x18\x03 \x01(\tR\x06cursor\x12\x14\n" +
"\x05limit\x18\x04 \x01(\rR\x05limit\x12\x14\n" +
"\x05roles\x18\x05 \x03(\tR\x05roles\x12W\n" +
"\x06labels\x18\x06 \x03(\v2?.teleport.scopes.joining.v1.ListScopedTokensRequest.LabelsEntryR\x06labels\x12!\n" +
"\fwith_secrets\x18\a \x01(\bR\vwithSecrets\x1a9\n" +
"\fwith_secrets\x18\a \x01(\bR\vwithSecrets\x12=\n" +
"\fscope_filter\x18\b \x01(\v2\x1a.teleport.scopes.v1.FilterR\vscopeFilter\x12N\n" +
"\x15assigned_scope_filter\x18\t \x01(\v2\x1a.teleport.scopes.v1.FilterR\x13assignedScopeFilter\x1a9\n" +
"\vLabelsEntry\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"s\n" +
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01J\x04\b\x01\x10\x02J\x04\b\x02\x10\x03R\x0eresource_scopeR\x0eassigned_scope\"s\n" +
"\x18ListScopedTokensResponse\x12?\n" +
"\x06tokens\x18\x01 \x03(\v2'.teleport.scopes.joining.v1.ScopedTokenR\x06tokens\x12\x16\n" +
"\x06cursor\x18\x02 \x01(\tR\x06cursor\"Y\n" +
@@ -1067,9 +1070,9 @@ var file_teleport_scopes_joining_v1_service_proto_goTypes = []any{
}
var file_teleport_scopes_joining_v1_service_proto_depIdxs = []int32{
13, // 0: teleport.scopes.joining.v1.GetScopedTokenResponse.token:type_name -> teleport.scopes.joining.v1.ScopedToken
14, // 1: teleport.scopes.joining.v1.ListScopedTokensRequest.resource_scope:type_name -> teleport.scopes.v1.Filter
14, // 2: teleport.scopes.joining.v1.ListScopedTokensRequest.assigned_scope:type_name -> teleport.scopes.v1.Filter
12, // 3: teleport.scopes.joining.v1.ListScopedTokensRequest.labels:type_name -> teleport.scopes.joining.v1.ListScopedTokensRequest.LabelsEntry
12, // 1: teleport.scopes.joining.v1.ListScopedTokensRequest.labels:type_name -> teleport.scopes.joining.v1.ListScopedTokensRequest.LabelsEntry
14, // 2: teleport.scopes.joining.v1.ListScopedTokensRequest.scope_filter:type_name -> teleport.scopes.v1.Filter
14, // 3: teleport.scopes.joining.v1.ListScopedTokensRequest.assigned_scope_filter:type_name -> teleport.scopes.v1.Filter
13, // 4: teleport.scopes.joining.v1.ListScopedTokensResponse.tokens:type_name -> teleport.scopes.joining.v1.ScopedToken
13, // 5: teleport.scopes.joining.v1.CreateScopedTokenRequest.token:type_name -> teleport.scopes.joining.v1.ScopedToken
13, // 6: teleport.scopes.joining.v1.CreateScopedTokenResponse.token:type_name -> teleport.scopes.joining.v1.ScopedToken
@@ -86,37 +86,79 @@ func (x PinKind) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Mode determines the mode of scoping when a query specifies a scope. When a query specifies a scope,
// one of two questions is typically trying to be answered. Either, what resources are "in" and/or "subject to"
// a given scope, or what policies are "applicable to" a given scope.
// Mode determines how a Filter matches resource scopes relative to the scope specified by the
// filter. Most modes select resources by their relationship to Filter.Scope (exact, descendants,
// ancestors, or all relatives). Two special modes (MODE_UNSCOPED and MODE_ALL) do not select by
// relationship and instead match the unscoped subset or everything the caller may see, respectively.
//
// NOTE: Our internals treat an unspecified filter and a MODE_ALL filter is both being "match all",
// *however* the API/authz layer treats them differently. A MODE_ALL filter is interpreted as being
// intentional. However, an omitted filter is replaced by an identity-dependent default (MODE_EXACT
// for scoped callers and MODE_UNSCOPED for unscoped callers). This conservative defaulting is a key
// component of the backwards comatibility and safety story for scope-enabled APIs. Any callsite being
// updated to use an inclusive filtering mode like MODE_ALL must be carefully validated to ensure that
// it is safe in the context of issues like the "confused deputy" problem.
type Mode int32
const (
// MODE_UNSPECIFIED indicates that no scope-based filtering has been specified.
// MODE_UNSPECIFIED indicates that no scope-based filtering has been specified. Cache/backend impls
// treat this as equivalent to MODE_ALL. The API/authz layer replaces this with one of MODE_EXACT or
// MODE_UNSCOPED depending on whether the caller is scoped or unscoped, respectively.
Mode_MODE_UNSPECIFIED Mode = 0
// MODE_RESOURCES_SUBJECT_TO_SCOPE matches scopes by the resource subjugation rules. In the
// terminology of scope comparison, this means Equivalent and Descendant scopes. This is the mode that most
// user-facing scoped queries should use, as it intuitively answers the question of "what is the contents of
// scope X?". See the 'lib/scopes' package for more detailed discussion of scope comparison/heirarchy.
Mode_MODE_RESOURCES_SUBJECT_TO_SCOPE Mode = 1
// MODE_POLICIES_APPLICABLE_TO_SCOPE matches scopes by the policy application rules. In the
// terminology of scope comparison, this means Ancestor and Equivalent scopes. This is the mode that most caching
// and access-control related queries should use as it answers the question "what policies might affect access to
// this resource at scope X?". See the 'lib/scopes' package for more detailed discussion of scope comparison/heirarchy.
//
// Deprecated: Marked as deprecated in teleport/scopes/v1/scopes.proto.
Mode_MODE_POLICIES_APPLICABLE_TO_SCOPE Mode = 2
// MODE_EXACT matches resources in the exact scope specified by Filter.Scope (Equivalent only). This is
// the minimal/safe default for watch/list requests initiated by scoped callers that do not specify an
// explicit filter.
Mode_MODE_EXACT Mode = 3
// MODE_DESCENDANTS matches the scope specified by Filter.Scope and all of its descendants. For example,
// a Filter.Scope of `/staging/west` would match `/staging/west` and `/staging/west/test`, but not
// `/staging` or `/prod/west`.
Mode_MODE_DESCENDANTS Mode = 4
// MODE_ANCESTORS matches the scope specified by Filter.Scope and all of its ancestors. For example,
// a Filter.Scope of `/staging/west` would match `/staging/west` and `/staging`, but not
// `/staging/west/test` or `/prod/west`.
Mode_MODE_ANCESTORS Mode = 5
// MODE_RELATIVES matches the scope specified by Filter.Scope, all of its ancestors, and all of its
// descendants (i.e. any non-orthogonal scope). For example, a Filter.Scope of `/staging/west` would match
// `/staging/west`, `/staging`, and `/staging/west/test, but not `/prod/west`.
Mode_MODE_RELATIVES Mode = 6
// MODE_UNSCOPED matches unscoped resources only. Filter.Scope must be empty when this mode is selected.
// This is the safe default for watch/list requests initiated by unscoped callers that do not specify an
// explicit filter.
Mode_MODE_UNSCOPED Mode = 7
// MODE_ALL matches all resources that the caller has permission to see (scoped and unscoped). Filter.Scope
// must be empty when this mode is selected. This is the mode that most exhaustive user-facing views (e.g. `tctl get`)
// should use.
Mode_MODE_ALL Mode = 8
)
// Enum value maps for Mode.
var (
Mode_name = map[int32]string{
0: "MODE_UNSPECIFIED",
1: "MODE_RESOURCES_SUBJECT_TO_SCOPE",
2: "MODE_POLICIES_APPLICABLE_TO_SCOPE",
3: "MODE_EXACT",
4: "MODE_DESCENDANTS",
5: "MODE_ANCESTORS",
6: "MODE_RELATIVES",
7: "MODE_UNSCOPED",
8: "MODE_ALL",
}
Mode_value = map[string]int32{
"MODE_UNSPECIFIED": 0,
"MODE_RESOURCES_SUBJECT_TO_SCOPE": 1,
"MODE_POLICIES_APPLICABLE_TO_SCOPE": 2,
"MODE_EXACT": 3,
"MODE_DESCENDANTS": 4,
"MODE_ANCESTORS": 5,
"MODE_RELATIVES": 6,
"MODE_UNSCOPED": 7,
"MODE_ALL": 8,
}
)
@@ -589,11 +631,17 @@ func (b0 SystemRoles_builder) Build() *SystemRoles {
return m0
}
// Filter is a query parameter that matches other scopes based on a specified scope and mode. Used for
// filtering resources that are subject to or policies that apply to a given scope.
// Filter is a query parameter that matches scopes based on a specified scope and mode. See the
// Mode enum for the semantics of each matching mode, and the 'lib/scopes' package (MatchScope/ValidateFilter)
// for the authoritative matching and validation logic.
//
// Conventionally, list and watch requests for scope-aware types include a `scope_filter` field of this type
// which matches the resource scope of the associated resource type. Some APIs may also support additional
// filters (e.g. scoped roles can be filtered by assignable scope).
type Filter struct {
state protoimpl.MessageState `protogen:"hybrid.v1"`
// Scope is the scope value to match against.
// Scope is the scope value to match against. Must be empty for MODE_UNSCOPED, MODE_ALL, and
// MODE_UNSPECIFIED, and non-empty for all relationship-based modes.
Scope string `protobuf:"bytes,1,opt,name=scope,proto3" json:"scope,omitempty"`
// Mode determines the mode of matching.
Mode Mode `protobuf:"varint,2,opt,name=mode,proto3,enum=teleport.scopes.v1.Mode" json:"mode,omitempty"`
@@ -651,7 +699,8 @@ func (x *Filter) SetMode(v Mode) {
type Filter_builder struct {
_ [0]func() // Prevents comparability and use of unkeyed literals for the builder.
// Scope is the scope value to match against.
// Scope is the scope value to match against. Must be empty for MODE_UNSCOPED, MODE_ALL, and
// MODE_UNSPECIFIED, and non-empty for all relationship-based modes.
Scope string
// Mode determines the mode of matching.
Mode Mode
@@ -701,11 +750,17 @@ const file_teleport_scopes_v1_scopes_proto_rawDesc = "" +
"\aPinKind\x12\x18\n" +
"\x14PIN_KIND_UNSPECIFIED\x10\x00\x12\x11\n" +
"\rPIN_KIND_USER\x10\x01\x12\x12\n" +
"\x0ePIN_KIND_AGENT\x10\x02*h\n" +
"\x0ePIN_KIND_AGENT\x10\x02*\xdd\x01\n" +
"\x04Mode\x12\x14\n" +
"\x10MODE_UNSPECIFIED\x10\x00\x12#\n" +
"\x1fMODE_RESOURCES_SUBJECT_TO_SCOPE\x10\x01\x12%\n" +
"!MODE_POLICIES_APPLICABLE_TO_SCOPE\x10\x02BPZNgithub.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/v1;scopesv1b\x06proto3"
"\x10MODE_UNSPECIFIED\x10\x00\x12)\n" +
"!MODE_POLICIES_APPLICABLE_TO_SCOPE\x10\x02\x1a\x02\b\x01\x12\x0e\n" +
"\n" +
"MODE_EXACT\x10\x03\x12\x14\n" +
"\x10MODE_DESCENDANTS\x10\x04\x12\x12\n" +
"\x0eMODE_ANCESTORS\x10\x05\x12\x12\n" +
"\x0eMODE_RELATIVES\x10\x06\x12\x11\n" +
"\rMODE_UNSCOPED\x10\a\x12\f\n" +
"\bMODE_ALL\x10\b\"\x04\b\x01\x10\x01*\x1fMODE_RESOURCES_SUBJECT_TO_SCOPEBPZNgithub.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/v1;scopesv1b\x06proto3"
var file_teleport_scopes_v1_scopes_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
var file_teleport_scopes_v1_scopes_proto_msgTypes = make([]protoimpl.MessageInfo, 8)
@@ -86,37 +86,79 @@ func (x PinKind) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Mode determines the mode of scoping when a query specifies a scope. When a query specifies a scope,
// one of two questions is typically trying to be answered. Either, what resources are "in" and/or "subject to"
// a given scope, or what policies are "applicable to" a given scope.
// Mode determines how a Filter matches resource scopes relative to the scope specified by the
// filter. Most modes select resources by their relationship to Filter.Scope (exact, descendants,
// ancestors, or all relatives). Two special modes (MODE_UNSCOPED and MODE_ALL) do not select by
// relationship and instead match the unscoped subset or everything the caller may see, respectively.
//
// NOTE: Our internals treat an unspecified filter and a MODE_ALL filter is both being "match all",
// *however* the API/authz layer treats them differently. A MODE_ALL filter is interpreted as being
// intentional. However, an omitted filter is replaced by an identity-dependent default (MODE_EXACT
// for scoped callers and MODE_UNSCOPED for unscoped callers). This conservative defaulting is a key
// component of the backwards comatibility and safety story for scope-enabled APIs. Any callsite being
// updated to use an inclusive filtering mode like MODE_ALL must be carefully validated to ensure that
// it is safe in the context of issues like the "confused deputy" problem.
type Mode int32
const (
// MODE_UNSPECIFIED indicates that no scope-based filtering has been specified.
// MODE_UNSPECIFIED indicates that no scope-based filtering has been specified. Cache/backend impls
// treat this as equivalent to MODE_ALL. The API/authz layer replaces this with one of MODE_EXACT or
// MODE_UNSCOPED depending on whether the caller is scoped or unscoped, respectively.
Mode_MODE_UNSPECIFIED Mode = 0
// MODE_RESOURCES_SUBJECT_TO_SCOPE matches scopes by the resource subjugation rules. In the
// terminology of scope comparison, this means Equivalent and Descendant scopes. This is the mode that most
// user-facing scoped queries should use, as it intuitively answers the question of "what is the contents of
// scope X?". See the 'lib/scopes' package for more detailed discussion of scope comparison/heirarchy.
Mode_MODE_RESOURCES_SUBJECT_TO_SCOPE Mode = 1
// MODE_POLICIES_APPLICABLE_TO_SCOPE matches scopes by the policy application rules. In the
// terminology of scope comparison, this means Ancestor and Equivalent scopes. This is the mode that most caching
// and access-control related queries should use as it answers the question "what policies might affect access to
// this resource at scope X?". See the 'lib/scopes' package for more detailed discussion of scope comparison/heirarchy.
//
// Deprecated: Marked as deprecated in teleport/scopes/v1/scopes.proto.
Mode_MODE_POLICIES_APPLICABLE_TO_SCOPE Mode = 2
// MODE_EXACT matches resources in the exact scope specified by Filter.Scope (Equivalent only). This is
// the minimal/safe default for watch/list requests initiated by scoped callers that do not specify an
// explicit filter.
Mode_MODE_EXACT Mode = 3
// MODE_DESCENDANTS matches the scope specified by Filter.Scope and all of its descendants. For example,
// a Filter.Scope of `/staging/west` would match `/staging/west` and `/staging/west/test`, but not
// `/staging` or `/prod/west`.
Mode_MODE_DESCENDANTS Mode = 4
// MODE_ANCESTORS matches the scope specified by Filter.Scope and all of its ancestors. For example,
// a Filter.Scope of `/staging/west` would match `/staging/west` and `/staging`, but not
// `/staging/west/test` or `/prod/west`.
Mode_MODE_ANCESTORS Mode = 5
// MODE_RELATIVES matches the scope specified by Filter.Scope, all of its ancestors, and all of its
// descendants (i.e. any non-orthogonal scope). For example, a Filter.Scope of `/staging/west` would match
// `/staging/west`, `/staging`, and `/staging/west/test, but not `/prod/west`.
Mode_MODE_RELATIVES Mode = 6
// MODE_UNSCOPED matches unscoped resources only. Filter.Scope must be empty when this mode is selected.
// This is the safe default for watch/list requests initiated by unscoped callers that do not specify an
// explicit filter.
Mode_MODE_UNSCOPED Mode = 7
// MODE_ALL matches all resources that the caller has permission to see (scoped and unscoped). Filter.Scope
// must be empty when this mode is selected. This is the mode that most exhaustive user-facing views (e.g. `tctl get`)
// should use.
Mode_MODE_ALL Mode = 8
)
// Enum value maps for Mode.
var (
Mode_name = map[int32]string{
0: "MODE_UNSPECIFIED",
1: "MODE_RESOURCES_SUBJECT_TO_SCOPE",
2: "MODE_POLICIES_APPLICABLE_TO_SCOPE",
3: "MODE_EXACT",
4: "MODE_DESCENDANTS",
5: "MODE_ANCESTORS",
6: "MODE_RELATIVES",
7: "MODE_UNSCOPED",
8: "MODE_ALL",
}
Mode_value = map[string]int32{
"MODE_UNSPECIFIED": 0,
"MODE_RESOURCES_SUBJECT_TO_SCOPE": 1,
"MODE_POLICIES_APPLICABLE_TO_SCOPE": 2,
"MODE_EXACT": 3,
"MODE_DESCENDANTS": 4,
"MODE_ANCESTORS": 5,
"MODE_RELATIVES": 6,
"MODE_UNSCOPED": 7,
"MODE_ALL": 8,
}
)
@@ -571,8 +613,13 @@ func (b0 SystemRoles_builder) Build() *SystemRoles {
return m0
}
// Filter is a query parameter that matches other scopes based on a specified scope and mode. Used for
// filtering resources that are subject to or policies that apply to a given scope.
// Filter is a query parameter that matches scopes based on a specified scope and mode. See the
// Mode enum for the semantics of each matching mode, and the 'lib/scopes' package (MatchScope/ValidateFilter)
// for the authoritative matching and validation logic.
//
// Conventionally, list and watch requests for scope-aware types include a `scope_filter` field of this type
// which matches the resource scope of the associated resource type. Some APIs may also support additional
// filters (e.g. scoped roles can be filtered by assignable scope).
type Filter struct {
state protoimpl.MessageState `protogen:"opaque.v1"`
xxx_hidden_Scope string `protobuf:"bytes,1,opt,name=scope,proto3"`
@@ -631,7 +678,8 @@ func (x *Filter) SetMode(v Mode) {
type Filter_builder struct {
_ [0]func() // Prevents comparability and use of unkeyed literals for the builder.
// Scope is the scope value to match against.
// Scope is the scope value to match against. Must be empty for MODE_UNSCOPED, MODE_ALL, and
// MODE_UNSPECIFIED, and non-empty for all relationship-based modes.
Scope string
// Mode determines the mode of matching.
Mode Mode
@@ -681,11 +729,17 @@ const file_teleport_scopes_v1_scopes_proto_rawDesc = "" +
"\aPinKind\x12\x18\n" +
"\x14PIN_KIND_UNSPECIFIED\x10\x00\x12\x11\n" +
"\rPIN_KIND_USER\x10\x01\x12\x12\n" +
"\x0ePIN_KIND_AGENT\x10\x02*h\n" +
"\x0ePIN_KIND_AGENT\x10\x02*\xdd\x01\n" +
"\x04Mode\x12\x14\n" +
"\x10MODE_UNSPECIFIED\x10\x00\x12#\n" +
"\x1fMODE_RESOURCES_SUBJECT_TO_SCOPE\x10\x01\x12%\n" +
"!MODE_POLICIES_APPLICABLE_TO_SCOPE\x10\x02BPZNgithub.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/v1;scopesv1b\x06proto3"
"\x10MODE_UNSPECIFIED\x10\x00\x12)\n" +
"!MODE_POLICIES_APPLICABLE_TO_SCOPE\x10\x02\x1a\x02\b\x01\x12\x0e\n" +
"\n" +
"MODE_EXACT\x10\x03\x12\x14\n" +
"\x10MODE_DESCENDANTS\x10\x04\x12\x12\n" +
"\x0eMODE_ANCESTORS\x10\x05\x12\x12\n" +
"\x0eMODE_RELATIVES\x10\x06\x12\x11\n" +
"\rMODE_UNSCOPED\x10\a\x12\f\n" +
"\bMODE_ALL\x10\b\"\x04\b\x01\x10\x01*\x1fMODE_RESOURCES_SUBJECT_TO_SCOPEBPZNgithub.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/v1;scopesv1b\x06proto3"
var file_teleport_scopes_v1_scopes_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
var file_teleport_scopes_v1_scopes_proto_msgTypes = make([]protoimpl.MessageInfo, 8)
@@ -76,6 +76,9 @@ message GetScopedRoleResponse {
// ListScopedRolesRequest is the request to list scoped roles.
message ListScopedRolesRequest {
reserved 4;
reserved "assignable_scope";
// PageSize is the maximum number of results to return.
int32 page_size = 1;
@@ -83,14 +86,17 @@ message ListScopedRolesRequest {
string page_token = 2;
// ResourceScope filters roles by their resource scope if specified.
teleport.scopes.v1.Filter resource_scope = 3;
// AssignableScope filters roles by their assignable scope if specified.
teleport.scopes.v1.Filter assignable_scope = 4;
teleport.scopes.v1.Filter resource_scope = 3 [deprecated = true];
// NameFilter filters roles by their name using a case-insensitive substring
// match if specified.
string name_filter = 5;
// ScopeFilter is the primary scope filter. Filtering is performed against the scope of the
// resource itself. Defaults to one of EXACT or UNSCOPED depending on wether the caller is
// a scoped or unscoped identity. Exhaustive user-facing views (e.g. `tctl get`) should
// specify mode ALL.
teleport.scopes.v1.Filter scope_filter = 6;
}
// ListScopedRolesResponse is the response to list scoped roles.
@@ -166,19 +172,15 @@ message GetScopedRoleAssignmentResponse {
// ListScopedRoleAssignmentsRequest is the request to list scoped role assignments.
message ListScopedRoleAssignmentsRequest {
reserved 3, 4;
reserved "resource_scope", "assigned_scope";
// PageSize is the maximum number of results to return.
int32 page_size = 1;
// PageToken is the pagination cursor used to start from where a previous request left off.
string page_token = 2;
// ResourceScope filters assignments by their resource scope if specified.
teleport.scopes.v1.Filter resource_scope = 3;
// AssignedScope filters assignments by the scopes they assign to if specified (note: matches assignment
// resources with 1 or more maching scopes, not all scopes within the assignment will necessarily match).
teleport.scopes.v1.Filter assigned_scope = 4;
// User optionally limits the list to assignments for a specific user.
string user = 5;
@@ -190,6 +192,17 @@ message ListScopedRoleAssignmentsRequest {
// is specifically used to support users discovering where they have been granted privileges across scopes,
// and is not intended for general use.
bool all_caller_assignments = 7;
// ScopeFilter is the primary scope filter. Filtering is performed against the scope of the
// resource itself. Defaults to one of EXACT or UNSCOPED depending on wether the caller is
// a scoped or unscoped identity. Exhaustive user-facing views (e.g. `tctl get`) should
// specify mode ALL.
teleport.scopes.v1.Filter scope_filter = 8;
// AssignedScopeFilter filters assignments by the scopes they assign to if specified (note: matches
// assignment resources with 1 or more matching scopes, not all scopes within the assignment will
// necessarily match).
teleport.scopes.v1.Filter assigned_scope_filter = 9;
}
// ListScopedRoleAssignmentsResponse is the response to list scoped role assignments.
@@ -62,11 +62,8 @@ message GetScopedTokenResponse {
// ListScopedTokensRequest is the request to list scoped tokens.
message ListScopedTokensRequest {
// Filter tokens by their resource scope.
teleport.scopes.v1.Filter resource_scope = 1;
// Filter tokens by their assigned scope.
teleport.scopes.v1.Filter assigned_scope = 2;
reserved 1, 2;
reserved "resource_scope", "assigned_scope";
// The pagination cursor.
string cursor = 3;
@@ -82,6 +79,15 @@ message ListScopedTokensRequest {
// If true, include the token secrets in the response.
bool with_secrets = 7;
// ScopeFilter is the primary scope filter. Filtering is performed against the scope of the
// resource itself. Defaults to one of EXACT or UNSCOPED depending on wether the caller is
// a scoped or unscoped identity. Exhaustive user-facing views (e.g. `tctl get`) should
// specify mode ALL.
teleport.scopes.v1.Filter scope_filter = 8;
// AssignedScopeFilter filters tokens by their assigned scope if specified.
teleport.scopes.v1.Filter assigned_scope_filter = 9;
}
// ListScopedTokensResponse is the response to list scoped tokens.
+58 -14
View File
@@ -92,30 +92,74 @@ message SystemRoles {
repeated string additional = 2;
}
// Mode determines the mode of scoping when a query specifies a scope. When a query specifies a scope,
// one of two questions is typically trying to be answered. Either, what resources are "in" and/or "subject to"
// a given scope, or what policies are "applicable to" a given scope.
// Mode determines how a Filter matches resource scopes relative to the scope specified by the
// filter. Most modes select resources by their relationship to Filter.Scope (exact, descendants,
// ancestors, or all relatives). Two special modes (MODE_UNSCOPED and MODE_ALL) do not select by
// relationship and instead match the unscoped subset or everything the caller may see, respectively.
//
// NOTE: Our internals treat an unspecified filter and a MODE_ALL filter is both being "match all",
// *however* the API/authz layer treats them differently. A MODE_ALL filter is interpreted as being
// intentional. However, an omitted filter is replaced by an identity-dependent default (MODE_EXACT
// for scoped callers and MODE_UNSCOPED for unscoped callers). This conservative defaulting is a key
// component of the backwards comatibility and safety story for scope-enabled APIs. Any callsite being
// updated to use an inclusive filtering mode like MODE_ALL must be carefully validated to ensure that
// it is safe in the context of issues like the "confused deputy" problem.
enum Mode {
// MODE_UNSPECIFIED indicates that no scope-based filtering has been specified.
MODE_UNSPECIFIED = 0;
reserved 1;
reserved "MODE_RESOURCES_SUBJECT_TO_SCOPE";
// MODE_RESOURCES_SUBJECT_TO_SCOPE matches scopes by the resource subjugation rules. In the
// terminology of scope comparison, this means Equivalent and Descendant scopes. This is the mode that most
// user-facing scoped queries should use, as it intuitively answers the question of "what is the contents of
// scope X?". See the 'lib/scopes' package for more detailed discussion of scope comparison/heirarchy.
MODE_RESOURCES_SUBJECT_TO_SCOPE = 1;
// MODE_UNSPECIFIED indicates that no scope-based filtering has been specified. Cache/backend impls
// treat this as equivalent to MODE_ALL. The API/authz layer replaces this with one of MODE_EXACT or
// MODE_UNSCOPED depending on whether the caller is scoped or unscoped, respectively.
MODE_UNSPECIFIED = 0;
// MODE_POLICIES_APPLICABLE_TO_SCOPE matches scopes by the policy application rules. In the
// terminology of scope comparison, this means Ancestor and Equivalent scopes. This is the mode that most caching
// and access-control related queries should use as it answers the question "what policies might affect access to
// this resource at scope X?". See the 'lib/scopes' package for more detailed discussion of scope comparison/heirarchy.
MODE_POLICIES_APPLICABLE_TO_SCOPE = 2;
MODE_POLICIES_APPLICABLE_TO_SCOPE = 2 [deprecated = true];
// MODE_EXACT matches resources in the exact scope specified by Filter.Scope (Equivalent only). This is
// the minimal/safe default for watch/list requests initiated by scoped callers that do not specify an
// explicit filter.
MODE_EXACT = 3;
// MODE_DESCENDANTS matches the scope specified by Filter.Scope and all of its descendants. For example,
// a Filter.Scope of `/staging/west` would match `/staging/west` and `/staging/west/test`, but not
// `/staging` or `/prod/west`.
MODE_DESCENDANTS = 4;
// MODE_ANCESTORS matches the scope specified by Filter.Scope and all of its ancestors. For example,
// a Filter.Scope of `/staging/west` would match `/staging/west` and `/staging`, but not
// `/staging/west/test` or `/prod/west`.
MODE_ANCESTORS = 5;
// MODE_RELATIVES matches the scope specified by Filter.Scope, all of its ancestors, and all of its
// descendants (i.e. any non-orthogonal scope). For example, a Filter.Scope of `/staging/west` would match
// `/staging/west`, `/staging`, and `/staging/west/test, but not `/prod/west`.
MODE_RELATIVES = 6;
// MODE_UNSCOPED matches unscoped resources only. Filter.Scope must be empty when this mode is selected.
// This is the safe default for watch/list requests initiated by unscoped callers that do not specify an
// explicit filter.
MODE_UNSCOPED = 7;
// MODE_ALL matches all resources that the caller has permission to see (scoped and unscoped). Filter.Scope
// must be empty when this mode is selected. This is the mode that most exhaustive user-facing views (e.g. `tctl get`)
// should use.
MODE_ALL = 8;
}
// Filter is a query parameter that matches other scopes based on a specified scope and mode. Used for
// filtering resources that are subject to or policies that apply to a given scope.
// Filter is a query parameter that matches scopes based on a specified scope and mode. See the
// Mode enum for the semantics of each matching mode, and the 'lib/scopes' package (MatchScope/ValidateFilter)
// for the authoritative matching and validation logic.
//
// Conventionally, list and watch requests for scope-aware types include a `scope_filter` field of this type
// which matches the resource scope of the associated resource type. Some APIs may also support additional
// filters (e.g. scoped roles can be filtered by assignable scope).
message Filter {
// Scope is the scope value to match against.
// Scope is the scope value to match against. Must be empty for MODE_UNSCOPED, MODE_ALL, and
// MODE_UNSPECIFIED, and non-empty for all relationship-based modes.
string scope = 1;
// Mode determines the mode of matching.
+3 -1
View File
@@ -1072,7 +1072,9 @@ func TestBasicSSHScopedLogin(t *testing.T) {
timeout := time.After(30 * time.Second)
for {
unseen := slices.Clone(assignmentIDs)
for assignment, err := range scopedutils.RangeScopedRoleAssignments(ctx, adminClient.ScopedAccessServiceClient(), &scopedaccessv1.ListScopedRoleAssignmentsRequest{}) {
for assignment, err := range scopedutils.RangeScopedRoleAssignments(ctx, adminClient.ScopedAccessServiceClient(), scopedaccessv1.ListScopedRoleAssignmentsRequest_builder{
ScopeFilter: scopesv1.Filter_builder{Mode: scopesv1.Mode_MODE_ALL}.Build(),
}.Build()) {
require.NoError(t, err)
id := assignment.GetMetadata().GetName()
unseen = slices.DeleteFunc(unseen, func(unseenID string) bool { return id == unseenID })
+14 -1
View File
@@ -397,12 +397,16 @@ func (s *Server) ListScopedRoleAssignments(ctx context.Context, req *scopedacces
}
}
// list method scope filters must use identity-based defaults per RFD 0229i
req.SetScopeFilter(authzContext.CheckerContext.ResolveScopeFilter(req.GetScopeFilter()))
// list scoped role assignments with a filter that only passes assignments the user has access to.
rsp, err := s.cfg.Reader.ListScopedRoleAssignmentsWithFilter(ctx, req, func(assignment *scopedaccessv1.ScopedRoleAssignment) bool {
if req.GetAllCallerAssignments() {
// note that this short-circuit doesn't just bypass verb checks, it also bypasses scope pinning. this is
// intended behavior and an important part of what makes the all_caller_assignments mode useful, as it allows
// users to get an overview of their available privileges across all scopes.
// users to get an overview of their available privileges across all scopes (assuming the scope filter mode
// has been set to ALL).
return authzContext.User.GetName() == assignment.GetSpec().GetUser()
}
err := authzContext.CheckerContext.Decision(ctx, assignment.GetScope(), func(checker *services.ScopedAccessChecker) error {
@@ -431,6 +435,15 @@ func (s *Server) ListScopedRoles(ctx context.Context, req *scopedaccessv1.ListSc
return nil, trace.Wrap(err)
}
// the resource_scope field was renamed to scope_filter and is now deprecated. honor it as equivalent to
// scope_filter for back-compat with clients that have not yet been updated to set scope_filter.
if req.HasResourceScope() && !req.HasScopeFilter() { //nolint:staticcheck // SA1019. Reading deprecated field for backwards compatibility.
req.SetScopeFilter(req.GetResourceScope()) //nolint:staticcheck // SA1019. Reading deprecated field for backwards compatibility.
}
// list method scope filters must use identity-based defaults per RFD 0229i
req.SetScopeFilter(authzContext.CheckerContext.ResolveScopeFilter(req.GetScopeFilter()))
// list scoped roles with a filter that only passes roles the user has access to.
rsp, err := s.cfg.Reader.ListScopedRolesWithFilter(ctx, req, func(role *scopedaccessv1.ScopedRole) bool {
err := authzContext.CheckerContext.Decision(ctx, role.GetScope(), func(checker *services.ScopedAccessChecker) error {
+96 -4
View File
@@ -981,12 +981,20 @@ func TestUnscopedBasics(t *testing.T) {
require.Equal(t, "some-role", rrsp.GetRole().GetMetadata().GetName())
// verify that admin can list the role
lrsp, err := srvAlice.ListScopedRoles(ctx, &scopedaccessv1.ListScopedRolesRequest{})
lrsp, err := srvAlice.ListScopedRoles(ctx, scopedaccessv1.ListScopedRolesRequest_builder{
ScopeFilter: scopesv1.Filter_builder{Mode: scopesv1.Mode_MODE_ALL}.Build(),
}.Build())
require.NoError(t, err)
require.Empty(t, lrsp.GetNextPageToken())
require.Len(t, lrsp.GetRoles(), 1)
require.Equal(t, "some-role", lrsp.GetRoles()[0].GetMetadata().GetName())
// verify that an omitted filter defaults to MODE_UNSCOPED for unscoped caller, which matches no
// scoped resources.
lrsp, err = srvAlice.ListScopedRoles(ctx, &scopedaccessv1.ListScopedRolesRequest{})
require.NoError(t, err)
require.Empty(t, lrsp.GetRoles())
// verify that auditor cannot create a role
_, err = srvBob.CreateScopedRole(ctx, scopedaccessv1.CreateScopedRoleRequest_builder{
Role: scopedaccessv1.ScopedRole_builder{
@@ -1012,7 +1020,9 @@ func TestUnscopedBasics(t *testing.T) {
require.Equal(t, crsp.GetRole().GetMetadata().GetName(), rrsp.GetRole().GetMetadata().GetName())
// verify that auditor can list the admin-created role
lrsp, err = srvBob.ListScopedRoles(ctx, &scopedaccessv1.ListScopedRolesRequest{})
lrsp, err = srvBob.ListScopedRoles(ctx, scopedaccessv1.ListScopedRolesRequest_builder{
ScopeFilter: scopesv1.Filter_builder{Mode: scopesv1.Mode_MODE_ALL}.Build(),
}.Build())
require.NoError(t, err)
require.Empty(t, lrsp.GetNextPageToken())
require.Len(t, lrsp.GetRoles(), 1)
@@ -1056,7 +1066,9 @@ func TestUnscopedBasics(t *testing.T) {
require.Equal(t, acrsp.GetAssignment().GetMetadata().GetName(), rasp.GetAssignment().GetMetadata().GetName())
// verify that admin can list the assignment
lasp, err := srvAlice.ListScopedRoleAssignments(ctx, &scopedaccessv1.ListScopedRoleAssignmentsRequest{})
lasp, err := srvAlice.ListScopedRoleAssignments(ctx, scopedaccessv1.ListScopedRoleAssignmentsRequest_builder{
ScopeFilter: scopesv1.Filter_builder{Mode: scopesv1.Mode_MODE_ALL}.Build(),
}.Build())
require.NoError(t, err)
require.Empty(t, lasp.GetNextPageToken())
require.Len(t, lasp.GetAssignments(), 1)
@@ -1095,7 +1107,9 @@ func TestUnscopedBasics(t *testing.T) {
require.Equal(t, acrsp.GetAssignment().GetMetadata().GetName(), rasp.GetAssignment().GetMetadata().GetName())
// verify that auditor can list the admin-created assignment
lasp, err = srvBob.ListScopedRoleAssignments(ctx, &scopedaccessv1.ListScopedRoleAssignmentsRequest{})
lasp, err = srvBob.ListScopedRoleAssignments(ctx, scopedaccessv1.ListScopedRoleAssignmentsRequest_builder{
ScopeFilter: scopesv1.Filter_builder{Mode: scopesv1.Mode_MODE_ALL}.Build(),
}.Build())
require.NoError(t, err)
require.Empty(t, lasp.GetNextPageToken())
require.Len(t, lasp.GetAssignments(), 1)
@@ -1359,3 +1373,81 @@ func waitForAssignmentCondition(t *testing.T, reader services.ScopedRoleAssignme
}
}
}
// TestListScopedRolesFilterDefaulting verifies the identity-based defaulting for the primary scope filter
// is in effect for ListScopedRoles.
func TestListScopedRolesFilterDefaulting(t *testing.T) {
t.Parallel()
ctx := t.Context()
bk := newBackendPack(t)
defer bk.Close()
// staging-admin lives at /staging and grants read/list on scoped roles. west-role lives at the
// descendant scope /staging/west and exists only as data to be listed.
roles := []*scopedaccessv1.ScopedRole{
scopedaccessv1.ScopedRole_builder{
Kind: scopedaccess.KindScopedRole,
Metadata: headerv1.Metadata_builder{Name: "staging-admin"}.Build(),
Scope: "/staging",
Spec: scopedaccessv1.ScopedRoleSpec_builder{
AssignableScopes: []string{"/staging"},
Rules: []*scopedaccessv1.ScopedRule{
scopedaccessv1.ScopedRule_builder{
Resources: []string{scopedaccess.KindScopedRole},
Verbs: []string{types.VerbReadNoSecrets, types.VerbList},
}.Build(),
},
}.Build(),
Version: types.V1,
}.Build(),
scopedaccessv1.ScopedRole_builder{
Kind: scopedaccess.KindScopedRole,
Metadata: headerv1.Metadata_builder{Name: "west-role"}.Build(),
Scope: "/staging/west",
Spec: scopedaccessv1.ScopedRoleSpec_builder{
AssignableScopes: []string{"/staging/west"},
}.Build(),
Version: types.V1,
}.Build(),
}
for _, role := range roles {
_, err := bk.service.CreateScopedRole(ctx, scopedaccessv1.CreateScopedRoleRequest_builder{Role: role}.Build())
require.NoError(t, err)
}
waitForRoleCondition(t, bk.cache, func(roles []*scopedaccessv1.ScopedRole) bool {
return len(roles) == 2
})
// caller is pinned to /staging and holds staging-admin, so it can read roles at /staging and its
// descendants (including west-role at /staging/west).
srv := newServerForIdentity(t, bk, &services.AccessInfo{
ScopePin: scopesv1.Pin_builder{
Kind: scopesv1.PinKind_PIN_KIND_USER,
Scope: "/staging",
AssignmentTree: pinning.AssignmentTreeFromMap(map[string]map[string][]string{
"/staging": {"/staging": {"staging-admin"}},
}),
}.Build(),
Username: "alice",
})
collectNames := func(req *scopedaccessv1.ListScopedRolesRequest) []string {
rsp, err := srv.ListScopedRoles(ctx, req)
require.NoError(t, err)
var names []string
for _, role := range rsp.GetRoles() {
names = append(names, role.GetMetadata().GetName())
}
return names
}
// omitted filter defaults to MODE_EXACT at the pinned scope (/staging), so only the /staging role is
// returned - the readable descendant role at /staging/west is excluded.
require.Equal(t, []string{"staging-admin"}, collectNames(&scopedaccessv1.ListScopedRolesRequest{}))
// MODE_ALL returns everything the caller may read across scopes, including the descendant role.
require.ElementsMatch(t, []string{"staging-admin", "west-role"}, collectNames(scopedaccessv1.ListScopedRolesRequest_builder{
ScopeFilter: scopesv1.Filter_builder{Mode: scopesv1.Mode_MODE_ALL}.Build(),
}.Build()))
}
+3
View File
@@ -268,6 +268,9 @@ func (s *Server) ListScopedTokens(ctx context.Context, req *scopedjoiningv1.List
return nil, trace.Wrap(err)
}
// list method scope filters must use identity-based defaults per RFD 0229i
req.SetScopeFilter(authzContext.CheckerContext.ResolveScopeFilter(req.GetScopeFilter()))
limit := int(req.GetLimit())
if limit == 0 {
limit = defaultTokenPageSize
+2 -2
View File
@@ -159,8 +159,8 @@ func TestScopedJoiningService(t *testing.T) {
// list tokens while filtering their resource scope
res, err := service.ListScopedTokens(ctx, joiningv1.ListScopedTokensRequest_builder{
WithSecrets: true,
ResourceScope: scopesv1.Filter_builder{
Mode: scopesv1.Mode_MODE_RESOURCES_SUBJECT_TO_SCOPE,
ScopeFilter: scopesv1.Filter_builder{
Mode: scopesv1.Mode_MODE_DESCENDANTS,
Scope: "/staging/cc",
}.Build(),
}.Build())
+55
View File
@@ -0,0 +1,55 @@
/*
* Teleport
* Copyright (C) 2026 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package access
import (
"slices"
scopedaccessv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/access/v1"
"github.com/gravitational/teleport/lib/scopes"
)
// MatcHSecondaryAssignmentFilters provides a single source of truth for a subset of the matching logic needed to implement
// the ListScopedRoleAssignments RPC. How the primary scope filter functions differs between cache/backend impls, but the
// secondary filters (e.g. user name) function identically, so we centralize them here.
func MatchSecondaryAssignmentFilters(req *scopedaccessv1.ListScopedRoleAssignmentsRequest, assignment *scopedaccessv1.ScopedRoleAssignment) bool {
if req.GetUser() != "" && assignment.GetSpec().GetUser() != req.GetUser() {
return false
}
if req.GetRole() != "" {
if !slices.ContainsFunc(assignment.GetSpec().GetAssignments(), func(a *scopedaccessv1.Assignment) bool {
return a.GetRole() == req.GetRole()
}) {
return false
}
}
if asf := req.GetAssignedScopeFilter(); !scopes.IsMatchAll(asf) {
// we only evaluate non-wildcard filters here because we don't want to suppress assignments with
// empty sub-assignment sets unless the filter is restrictive.
if !slices.ContainsFunc(assignment.GetSpec().GetAssignments(), func(a *scopedaccessv1.Assignment) bool {
return scopes.MatchScope(asf, a.GetScope())
}) {
return false
}
}
return true
}
+103
View File
@@ -0,0 +1,103 @@
/*
* Teleport
* Copyright (C) 2026 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package access
import (
"testing"
"github.com/stretchr/testify/require"
scopedaccessv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/access/v1"
scopesv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/v1"
)
func TestMatchSecondaryAssignmentFilters(t *testing.T) {
t.Parallel()
assignment := scopedaccessv1.ScopedRoleAssignment_builder{
Spec: scopedaccessv1.ScopedRoleAssignmentSpec_builder{
User: "alice",
Assignments: []*scopedaccessv1.Assignment{
scopedaccessv1.Assignment_builder{Role: "role-a", Scope: "/aa"}.Build(),
scopedaccessv1.Assignment_builder{Role: "role-b", Scope: "/aa/bb"}.Build(),
},
}.Build(),
}.Build()
tests := []struct {
name string
req *scopedaccessv1.ListScopedRoleAssignmentsRequest
want bool
}{
{
name: "no filters matches",
req: scopedaccessv1.ListScopedRoleAssignmentsRequest_builder{}.Build(),
want: true,
},
{
name: "matching user",
req: scopedaccessv1.ListScopedRoleAssignmentsRequest_builder{User: "alice"}.Build(),
want: true,
},
{
name: "non-matching user",
req: scopedaccessv1.ListScopedRoleAssignmentsRequest_builder{User: "bob"}.Build(),
want: false,
},
{
name: "matching role",
req: scopedaccessv1.ListScopedRoleAssignmentsRequest_builder{Role: "role-b"}.Build(),
want: true,
},
{
name: "non-matching role",
req: scopedaccessv1.ListScopedRoleAssignmentsRequest_builder{Role: "role-x"}.Build(),
want: false,
},
{
name: "matching assigned scope",
req: scopedaccessv1.ListScopedRoleAssignmentsRequest_builder{
AssignedScopeFilter: scopesv1.Filter_builder{Mode: scopesv1.Mode_MODE_EXACT, Scope: "/aa/bb"}.Build(),
}.Build(),
want: true,
},
{
name: "non-matching assigned scope",
req: scopedaccessv1.ListScopedRoleAssignmentsRequest_builder{
AssignedScopeFilter: scopesv1.Filter_builder{Mode: scopesv1.Mode_MODE_EXACT, Scope: "/cc"}.Build(),
}.Build(),
want: false,
},
{
name: "all filters together match",
req: scopedaccessv1.ListScopedRoleAssignmentsRequest_builder{
User: "alice",
Role: "role-a",
AssignedScopeFilter: scopesv1.Filter_builder{Mode: scopesv1.Mode_MODE_DESCENDANTS, Scope: "/aa"}.Build(),
}.Build(),
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.want, MatchSecondaryAssignmentFilters(tt.req, assignment))
})
}
}
+14 -41
View File
@@ -21,14 +21,12 @@ package assignments
import (
"context"
"os"
"slices"
"strconv"
"github.com/gravitational/trace"
"google.golang.org/protobuf/proto"
scopedaccessv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/access/v1"
scopesv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/v1"
"github.com/gravitational/teleport/lib/scopes"
scopedaccess "github.com/gravitational/teleport/lib/scopes/access"
"github.com/gravitational/teleport/lib/scopes/cache"
@@ -119,23 +117,21 @@ func (c *AssignmentCache) ListScopedRoleAssignmentsWithFilter(ctx context.Contex
pageSize = maxPageSize
}
if req.GetResourceScope() != nil && req.GetAssignedScope() != nil {
return nil, trace.BadParameter("cannot filter by both resource scope and assigned scope simultaneously")
// validate the secondary assigned-scope filter. the primary scope filter is validated by the
// Cache.ResourcesMatchingScopeFilter method invoked below.
if err := scopes.ValidateFilter(req.GetAssignedScopeFilter()); err != nil {
return nil, trace.Wrap(err)
}
filter := func(assignment *scopedaccessv1.ScopedRoleAssignment) bool {
if req.GetUser() != "" && assignment.GetSpec().GetUser() != req.GetUser() {
// primary scope filter matching is handled by the Cache.ResourcesMatchingScopeFilter method
// invoked below. secondary filters (e.g. username) are applied here *after* the primary
// has already been applied.
if !scopedaccess.MatchSecondaryAssignmentFilters(req, assignment) {
return false
}
if req.GetRole() != "" {
if !slices.ContainsFunc(assignment.GetSpec().GetAssignments(), func(a *scopedaccessv1.Assignment) bool { return a.GetRole() == req.GetRole() }) {
// if the assignment does not have the requested role, skip it
return false
}
}
// apply the external filter after the basic request filter as the externally provided filter
// apply the external filter after the secondary filters as the externally provided filter
// is often more expensive to evaluate.
if !externalFilter(assignment) {
return false
@@ -149,39 +145,16 @@ func (c *AssignmentCache) ListScopedRoleAssignmentsWithFilter(ctx context.Contex
return nil, trace.Wrap(err)
}
// resources subject to policy scope root is basically the scoped resource equivalent
// of a "get all". this is a reasonable default for most queries.
getter := c.cache.ResourcesSubjectToPolicyScope
scope := scopes.Root
// if a resource scope filter has been provided, the user has specified a custom scope/mode to
// query by.
if req.GetResourceScope() != nil {
// a resource-scope based filter has been provided
switch req.GetResourceScope().GetMode() {
case scopesv1.Mode_MODE_RESOURCES_SUBJECT_TO_SCOPE:
getter = c.cache.ResourcesSubjectToPolicyScope
case scopesv1.Mode_MODE_POLICIES_APPLICABLE_TO_SCOPE:
getter = c.cache.PoliciesApplicableToResourceScope
default:
return nil, trace.BadParameter("unsupported or unspecified scoping mode %q in scoped role assignment resource scope filter", req.GetResourceScope().GetMode())
}
scope = req.GetResourceScope().GetScope()
}
if req.GetAssignedScope() != nil {
// NOTE: we eventually want to be able to query assignments by the scopes at which they assign roles,
// instead of just resource scope. this is important for introspection/auditing but not necessary for
// the core funcationality of teleport until we've fully migrated to PDP. For now, the implementation has
// been deferred. (will require implementation of a more complex caching structure where each resource
// will be associable with multiple scopes)
return nil, trace.NotImplemented("assigned scope filtering for scoped role assignments is not yet supported")
// the primary scope filter selects the cache traversal strategy.
resources, err := c.cache.ResourcesMatchingScopeFilter(req.GetScopeFilter(), c.cache.WithFilter(filter), c.cache.WithCursor(cursor))
if err != nil {
return nil, trace.Wrap(err)
}
var out []*scopedaccessv1.ScopedRoleAssignment
var nextCursor cache.Cursor[string]
Outer:
for scope := range getter(scope, c.cache.WithFilter(filter), c.cache.WithCursor(cursor)) {
for scope := range resources {
for assignment := range scope.Items() {
if len(out) == pageSize {
nextCursor = cache.Cursor[string]{
+22 -22
View File
@@ -214,8 +214,8 @@ func TestListScopedRoleAssignmentsScenarios(t *testing.T) {
// verify expected behavior for standard cursors in resources subject to scope mode
rsp, err := cache.ListScopedRoleAssignments(t.Context(), scopedaccessv1.ListScopedRoleAssignmentsRequest_builder{
ResourceScope: scopespb.Filter_builder{
Mode: scopespb.Mode_MODE_RESOURCES_SUBJECT_TO_SCOPE,
ScopeFilter: scopespb.Filter_builder{
Mode: scopespb.Mode_MODE_DESCENDANTS,
Scope: "/aa",
}.Build(),
PageToken: "v1:bob-02/dynamic@/aa",
@@ -226,8 +226,8 @@ func TestListScopedRoleAssignmentsScenarios(t *testing.T) {
// try to inject a malicious root out-of-band cursor in resources subject to scope mode (no effect)
rsp, err = cache.ListScopedRoleAssignments(t.Context(), scopedaccessv1.ListScopedRoleAssignmentsRequest_builder{
ResourceScope: scopespb.Filter_builder{
Mode: scopespb.Mode_MODE_RESOURCES_SUBJECT_TO_SCOPE,
ScopeFilter: scopespb.Filter_builder{
Mode: scopespb.Mode_MODE_DESCENDANTS,
Scope: "/aa",
}.Build(),
PageToken: "v1:bob-01/dynamic@/",
@@ -238,8 +238,8 @@ func TestListScopedRoleAssignmentsScenarios(t *testing.T) {
// try to inject a malicious orthogonal out-of-band cursor in resources subject to scope mode (no effect)
rsp, err = cache.ListScopedRoleAssignments(t.Context(), scopedaccessv1.ListScopedRoleAssignmentsRequest_builder{
ResourceScope: scopespb.Filter_builder{
Mode: scopespb.Mode_MODE_RESOURCES_SUBJECT_TO_SCOPE,
ScopeFilter: scopespb.Filter_builder{
Mode: scopespb.Mode_MODE_DESCENDANTS,
Scope: "/aa",
}.Build(),
PageToken: "v1:carol-01/dynamic@/bb",
@@ -250,8 +250,8 @@ func TestListScopedRoleAssignmentsScenarios(t *testing.T) {
// verify expected behavior for standard cursors in policies applicable to scope mode
rsp, err = cache.ListScopedRoleAssignments(t.Context(), scopedaccessv1.ListScopedRoleAssignmentsRequest_builder{
ResourceScope: scopespb.Filter_builder{
Mode: scopespb.Mode_MODE_POLICIES_APPLICABLE_TO_SCOPE,
ScopeFilter: scopespb.Filter_builder{
Mode: scopespb.Mode_MODE_ANCESTORS,
Scope: "/aa",
}.Build(),
PageToken: "v1:bob-01/dynamic@/",
@@ -263,8 +263,8 @@ func TestListScopedRoleAssignmentsScenarios(t *testing.T) {
// try to inject a malicious child out-of-band cursor in policies applicable to scope mode (effect is
// to ignore all items in valid query path).
rsp, err = cache.ListScopedRoleAssignments(t.Context(), scopedaccessv1.ListScopedRoleAssignmentsRequest_builder{
ResourceScope: scopespb.Filter_builder{
Mode: scopespb.Mode_MODE_POLICIES_APPLICABLE_TO_SCOPE,
ScopeFilter: scopespb.Filter_builder{
Mode: scopespb.Mode_MODE_ANCESTORS,
Scope: "/aa",
}.Build(),
PageToken: "v1:bob-03/dynamic@/aa/bb",
@@ -276,8 +276,8 @@ func TestListScopedRoleAssignmentsScenarios(t *testing.T) {
// try to inject a malicious orthogonal out-of-band cursor in policies applicable to scope mode (effect is to
// ignore root, but process leaf normally).
rsp, err = cache.ListScopedRoleAssignments(t.Context(), scopedaccessv1.ListScopedRoleAssignmentsRequest_builder{
ResourceScope: scopespb.Filter_builder{
Mode: scopespb.Mode_MODE_POLICIES_APPLICABLE_TO_SCOPE,
ScopeFilter: scopespb.Filter_builder{
Mode: scopespb.Mode_MODE_ANCESTORS,
Scope: "/aa",
}.Build(),
PageToken: "v1:carol-01/dynamic@/bb",
@@ -288,8 +288,8 @@ func TestListScopedRoleAssignmentsScenarios(t *testing.T) {
// verify rejection of unknown cursor version
_, err = cache.ListScopedRoleAssignments(t.Context(), scopedaccessv1.ListScopedRoleAssignmentsRequest_builder{
ResourceScope: scopespb.Filter_builder{
Mode: scopespb.Mode_MODE_RESOURCES_SUBJECT_TO_SCOPE,
ScopeFilter: scopespb.Filter_builder{
Mode: scopespb.Mode_MODE_DESCENDANTS,
Scope: "/aa",
}.Build(),
PageToken: "v2:bob-02/dynamic@/aa",
@@ -545,8 +545,8 @@ func TestListScopedRoleAssignmentsBasics(t *testing.T) {
{
name: "resource scope root",
req: scopedaccessv1.ListScopedRoleAssignmentsRequest_builder{
ResourceScope: scopespb.Filter_builder{
Mode: scopespb.Mode_MODE_RESOURCES_SUBJECT_TO_SCOPE,
ScopeFilter: scopespb.Filter_builder{
Mode: scopespb.Mode_MODE_DESCENDANTS,
Scope: "/",
}.Build(),
}.Build(),
@@ -563,8 +563,8 @@ func TestListScopedRoleAssignmentsBasics(t *testing.T) {
{
name: "resource scope non-root",
req: scopedaccessv1.ListScopedRoleAssignmentsRequest_builder{
ResourceScope: scopespb.Filter_builder{
Mode: scopespb.Mode_MODE_RESOURCES_SUBJECT_TO_SCOPE,
ScopeFilter: scopespb.Filter_builder{
Mode: scopespb.Mode_MODE_DESCENDANTS,
Scope: "/aa",
}.Build(),
}.Build(),
@@ -578,8 +578,8 @@ func TestListScopedRoleAssignmentsBasics(t *testing.T) {
{
name: "policy scope root",
req: scopedaccessv1.ListScopedRoleAssignmentsRequest_builder{
ResourceScope: scopespb.Filter_builder{
Mode: scopespb.Mode_MODE_POLICIES_APPLICABLE_TO_SCOPE,
ScopeFilter: scopespb.Filter_builder{
Mode: scopespb.Mode_MODE_ANCESTORS,
Scope: "/",
}.Build(),
}.Build(),
@@ -593,8 +593,8 @@ func TestListScopedRoleAssignmentsBasics(t *testing.T) {
{
name: "policy scope non-root",
req: scopedaccessv1.ListScopedRoleAssignmentsRequest_builder{
ResourceScope: scopespb.Filter_builder{
Mode: scopespb.Mode_MODE_POLICIES_APPLICABLE_TO_SCOPE,
ScopeFilter: scopespb.Filter_builder{
Mode: scopespb.Mode_MODE_ANCESTORS,
Scope: "/aa",
}.Build(),
}.Build(),
+2 -2
View File
@@ -60,7 +60,7 @@ func (c *AssignmentCache) PopulatePinnedAssignmentsForUser(ctx context.Context,
var lastErr error
// all non-orthogonal assignments for this user *may* assign roles relevant to this pin
assignments := c.cache.AllNonOrthogonalResources(pin.GetScope(), c.cache.WithFilter(func(assignment *scopedaccessv1.ScopedRoleAssignment) bool {
assignments := c.cache.ScopeAndRelatives(pin.GetScope(), c.cache.WithFilter(func(assignment *scopedaccessv1.ScopedRoleAssignment) bool {
return assignment.GetSpec().GetUser() == user
}))
@@ -200,7 +200,7 @@ func (c *AssignmentCache) PopulatePinnedAssignmentsForBot(
// all non-orthogonal assignments for this bot *may* assign roles relevant to this pin
wantBot := scopes.QualifiedName{Scope: botScope, Name: botName}.String()
assignments := c.cache.AllNonOrthogonalResources(pin.GetScope(), c.cache.WithFilter(func(assignment *scopedaccessv1.ScopedRoleAssignment) bool {
assignments := c.cache.ScopeAndRelatives(pin.GetScope(), c.cache.WithFilter(func(assignment *scopedaccessv1.ScopedRoleAssignment) bool {
// match on the full scope-qualified name. comparing the scope component
// as well as the name mitigates name reuse attacks across scopes.
return assignment.GetSpec().GetBot() == wantBot
+99 -10
View File
@@ -25,6 +25,7 @@ import (
"github.com/gravitational/trace"
scopesv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/v1"
"github.com/gravitational/teleport/lib/scopes"
"github.com/gravitational/teleport/lib/utils/sortmap"
)
@@ -161,9 +162,96 @@ func (c *Cache[T, K]) Get(key K) (T, bool) {
return c.cfg.Clone(value), true
}
// PoliciesApplicableToResourceScope iterates over the cached items using policy-application rules (i.e.
// a descending iteration from root, through the leaf of the specified scope).
func (c *Cache[T, K]) PoliciesApplicableToResourceScope(scope string, opts ...Option[T, K]) iter.Seq[ScopedItems[T]] {
// ResourcesMatchingScopeFilter returns an iterator over the cached items whose scope matches the given
// primary scope filter, selecting the appropriate traversal strategy for the filter's mode. This is the
// cache-side counterpart to [scopes.MatchScope]: it must select exactly the set of items that MatchScope
// would accept for the same filter, so that the indexed traversal and a naive per-item match agree.
//
// An empty/unspecified filter and MODE_ALL are both treated as "match everything" (the scoped equivalent
// of a get-all), consistent with MatchScope's wildcard handling. The filter is validated via [scopes.ValidateFilter]
// before use, so callers do not need to pre-validate.
//
// NOTE: currently MODE_UNSCOPED is treated as a valid input that happens to match nothing, which has been
// judged to be the most consistent behavior. Especially if we decide to start supporting unscoped resources
// in this cache in the future.
func (c *Cache[T, K]) ResourcesMatchingScopeFilter(filter *scopesv1.Filter, opts ...Option[T, K]) (iter.Seq[ScopedItems[T]], error) {
if err := scopes.ValidateFilter(filter); err != nil {
return nil, trace.Wrap(err)
}
scope := filter.GetScope()
switch filter.GetMode() {
case scopesv1.Mode_MODE_UNSPECIFIED, scopesv1.Mode_MODE_ALL:
// wildcard: yield everything (an exhaustive iteration from root).
return c.ScopeAndDescendants(scopes.Root, opts...), nil
case scopesv1.Mode_MODE_EXACT:
return c.ExactScope(scope, opts...), nil
case scopesv1.Mode_MODE_DESCENDANTS:
return c.ScopeAndDescendants(scope, opts...), nil
case scopesv1.Mode_MODE_ANCESTORS, scopesv1.Mode_MODE_POLICIES_APPLICABLE_TO_SCOPE: //nolint:staticcheck // SA1019. Deprecated mode retained as equivalent for backwards compatibility.
return c.ScopeAndAncestors(scope, opts...), nil
case scopesv1.Mode_MODE_RELATIVES:
return c.ScopeAndRelatives(scope, opts...), nil
case scopesv1.Mode_MODE_UNSCOPED:
// scoped caches only contain scoped resources, so an unscoped filter matches nothing.
return func(yield func(ScopedItems[T]) bool) {}, nil
default:
return nil, trace.BadParameter("unsupported scope filter mode %v (this is a bug)", filter.GetMode())
}
}
// ExactScope iterates over the cached items at exactly the given scope, with no ancestors or descendants.
// This corresponds to scopesv1.Mode_MODE_EXACT.
func (c *Cache[T, K]) ExactScope(scope string, opts ...Option[T, K]) iter.Seq[ScopedItems[T]] {
return func(yield func(ScopedItems[T]) bool) {
c.rw.RLock()
defer c.rw.RUnlock()
var options options[T, K]
for _, opt := range opts {
opt(&options)
}
if c.root == nil {
return
}
// keep track of the segments visited
var visited []string
// search for start position, beginning at the root
current := c.root
descender := newDescender(options.cursor)
defer descender.Stop()
for segment := range scopes.DescendingSegments(scope) {
// get next scope if it exists
var ok bool
current, ok = current.children.Get(segment)
if !ok {
// the target scope does not exist, nothing to yield.
return
}
// advance the descender to the next scope
descender.Descend(segment)
// update visited segments
visited = append(visited, segment)
}
// yield only the members at exactly the target scope (no recursion into descendants).
if !maybeYieldScopedItems(yield, visited, &descender, current.members, c.items, c.cfg.Clone, options.filter) {
return
}
}
}
// ScopeAndAncestors iterates over the cached items at the given scope and all of its ancestors (i.e.
// a descending iteration from root, through the leaf of the specified scope). This corresponds to
// scopesv1.Mode_MODE_ANCESTORS.
func (c *Cache[T, K]) ScopeAndAncestors(scope string, opts ...Option[T, K]) iter.Seq[ScopedItems[T]] {
return func(yield func(ScopedItems[T]) bool) {
c.rw.RLock()
defer c.rw.RUnlock()
@@ -212,9 +300,10 @@ func (c *Cache[T, K]) PoliciesApplicableToResourceScope(scope string, opts ...Op
}
}
// ResourcesSubjectToPolicyScope iterates over the cached items using resources-subjugation rules (i.e.
// an exhaustive descending iteration of the specified scope and all of its descendants).
func (c *Cache[T, K]) ResourcesSubjectToPolicyScope(scope string, opts ...Option[T, K]) iter.Seq[ScopedItems[T]] {
// ScopeAndDescendants iterates over the cached items at the given scope and all of its descendants (i.e.
// an exhaustive descending iteration of the specified scope and all of its descendants). This corresponds
// to scopesv1.Mode_MODE_DESCENDANTS.
func (c *Cache[T, K]) ScopeAndDescendants(scope string, opts ...Option[T, K]) iter.Seq[ScopedItems[T]] {
return func(yield func(ScopedItems[T]) bool) {
c.rw.RLock()
defer c.rw.RUnlock()
@@ -261,13 +350,13 @@ func (c *Cache[T, K]) ResourcesSubjectToPolicyScope(scope string, opts ...Option
}
}
// AllNonOrthogonalResources returns all resources at scopes non-orhthogonal to the given scope. For example, given the
// ScopeAndRelatives returns all resources at scopes non-orhthogonal to the given scope. For example, given the
// scope '/foo', this will return resources at '/', '/foo', '/foo/bar', etc, but not '/bar'. This operation is less typical
// than either PoliciesApplicableToResourceScope or ResourcesSubjectToPolicyScope. Currently, the only operation that wants
// than either ScopeAndAncestors or ScopeAndDescendants. Currently, the only operation that wants
// this kind of query is scope pin generation, which must discover all policies that might apply to access at the pinned scope
// *or* any of its sub-scopes. This can reasonably be thought of as producing the union of the result of both the
// PoliciesApplicableToResourceScope and ResourcesSubjectToPolicyScope methods.
func (c *Cache[T, K]) AllNonOrthogonalResources(scope string, opts ...Option[T, K]) iter.Seq[ScopedItems[T]] {
// ScopeAndAncestors and ScopeAndDescendants methods, and corresponds to scopesv1.Mode_MODE_RELATIVES.
func (c *Cache[T, K]) ScopeAndRelatives(scope string, opts ...Option[T, K]) iter.Seq[ScopedItems[T]] {
return func(yield func(ScopedItems[T]) bool) {
c.rw.RLock()
defer c.rw.RUnlock()
+45 -45
View File
@@ -222,7 +222,7 @@ func TestCacheFiltering(t *testing.T) {
}
var policiesApplicableTo []item[int]
for scope := range cache.PoliciesApplicableToResourceScope(tt.scope, cache.WithFilter(tt.filter)) {
for scope := range cache.ScopeAndAncestors(tt.scope, cache.WithFilter(tt.filter)) {
var seen int
for item := range scope.Items() {
seen++
@@ -238,7 +238,7 @@ func TestCacheFiltering(t *testing.T) {
cloned = 0
var resourcesSubjectTo []item[int]
for scope := range cache.ResourcesSubjectToPolicyScope(tt.scope, cache.WithFilter(tt.filter)) {
for scope := range cache.ScopeAndDescendants(tt.scope, cache.WithFilter(tt.filter)) {
var seen int
for item := range scope.Items() {
seen++
@@ -254,7 +254,7 @@ func TestCacheFiltering(t *testing.T) {
cloned = 0
var nonOrthogonalResources []item[int]
for scope := range cache.AllNonOrthogonalResources(tt.scope, cache.WithFilter(tt.filter)) {
for scope := range cache.ScopeAndRelatives(tt.scope, cache.WithFilter(tt.filter)) {
var seen int
for item := range scope.Items() {
seen++
@@ -275,10 +275,10 @@ func TestCacheFiltering(t *testing.T) {
}
}
// TestCacheAllNonOrthogonalResources verifies the expected behavior of the AllNonOrthogonalResources method separately
// TestCacheScopeAndRelatives verifies the expected behavior of the ScopeAndRelatives method separately
// because most coverage is provided by asserting that it produces the union of the output of the other two
// methods. This test primarily servers as a sanity-check to ensure that the other checks aren't wildly off-base.
func TestCacheAllNonOrthogonalResources(t *testing.T) {
func TestCacheScopeAndRelatives(t *testing.T) {
t.Parallel()
items := []item[int]{
@@ -355,7 +355,7 @@ func TestCacheAllNonOrthogonalResources(t *testing.T) {
}
var nonOrthogonalResources []item[int]
for scope := range cache.AllNonOrthogonalResources(tt.scope) {
for scope := range cache.ScopeAndRelatives(tt.scope) {
for item := range scope.Items() {
nonOrthogonalResources = append(nonOrthogonalResources, item)
}
@@ -366,8 +366,8 @@ func TestCacheAllNonOrthogonalResources(t *testing.T) {
}
// joinAndDeduplicateItems joins multiple sets of items and deduplicates them based on their keys. Used in testing
// the AllNonOrthogonalResources method since its output should always be the union of the output of
// PoliciesApplicableToResourceScope and ResourcesSubjectToPolicyScope.
// the ScopeAndRelatives method since its output should always be the union of the output of
// ScopeAndAncestors and ScopeAndDescendants.
func joinAndDeduplicateItems(sets ...[]item[int]) []item[int] {
// join all sets together and deduplicate
seen := make(map[int]struct{})
@@ -419,7 +419,7 @@ func TestCacheConcurrency(t *testing.T) {
go func() {
// perform a policy application query that will match all items
for _ = range cache.PoliciesApplicableToResourceScope("/aa/bb/cc") {
for _ = range cache.ScopeAndAncestors("/aa/bb/cc") {
lockstepC <- struct{}{} // block until the second query has caught up
<-proceedC // wait for the main test routine to unblock us
}
@@ -427,7 +427,7 @@ func TestCacheConcurrency(t *testing.T) {
go func() {
// perform a resource subjugation query that will match all items
for _ = range cache.ResourcesSubjectToPolicyScope("/") {
for _ = range cache.ScopeAndDescendants("/") {
<-lockstepC // wait until we get the signal from the first query
}
}()
@@ -784,7 +784,7 @@ func TestCursorScenarios(t *testing.T) {
// verify policies-applicable-to-resource iteration
var policiesApplicableTo []item[int]
for scope := range cache.PoliciesApplicableToResourceScope(tt.scope, cache.WithCursor(tt.cursor)) {
for scope := range cache.ScopeAndAncestors(tt.scope, cache.WithCursor(tt.cursor)) {
for item := range scope.Items() {
policiesApplicableTo = append(policiesApplicableTo, item)
}
@@ -794,7 +794,7 @@ func TestCursorScenarios(t *testing.T) {
// verify resources-subject-to-policy iteration
var resourcesSubjectTo []item[int]
for scope := range cache.ResourcesSubjectToPolicyScope(tt.scope, cache.WithCursor(tt.cursor)) {
for scope := range cache.ScopeAndDescendants(tt.scope, cache.WithCursor(tt.cursor)) {
for item := range scope.Items() {
resourcesSubjectTo = append(resourcesSubjectTo, item)
}
@@ -804,7 +804,7 @@ func TestCursorScenarios(t *testing.T) {
// verify non-orhthogonal iteration
var nonOrthogonal []item[int]
for scope := range cache.AllNonOrthogonalResources(tt.scope, cache.WithCursor(tt.cursor)) {
for scope := range cache.ScopeAndRelatives(tt.scope, cache.WithCursor(tt.cursor)) {
for item := range scope.Items() {
nonOrthogonal = append(nonOrthogonal, item)
}
@@ -973,7 +973,7 @@ func TestCursorPagination(t *testing.T) {
var remaining []item[int]
var cursor Cursor[int]
PolicyApplicationOuter:
for scope := range cache.PoliciesApplicableToResourceScope(tt.scope) {
for scope := range cache.ScopeAndAncestors(tt.scope) {
for item := range scope.Items() {
if len(firstPage) == tt.limit {
cursor = Cursor[int]{
@@ -989,7 +989,7 @@ func TestCursorPagination(t *testing.T) {
require.Equal(t, tt.policiesApplicableTo.firstPage, firstPage)
if !cursor.IsZero() {
for scope := range cache.PoliciesApplicableToResourceScope(tt.scope, cache.WithCursor(cursor)) {
for scope := range cache.ScopeAndAncestors(tt.scope, cache.WithCursor(cursor)) {
for item := range scope.Items() {
remaining = append(remaining, item)
}
@@ -1005,7 +1005,7 @@ func TestCursorPagination(t *testing.T) {
cursor = Cursor[int]{}
ResourceSubjugationOuter:
for scope := range cache.ResourcesSubjectToPolicyScope(tt.scope) {
for scope := range cache.ScopeAndDescendants(tt.scope) {
for item := range scope.Items() {
if len(firstPage) == tt.limit {
cursor = Cursor[int]{
@@ -1021,7 +1021,7 @@ func TestCursorPagination(t *testing.T) {
require.Equal(t, tt.resourcesSubjectTo.firstPage, firstPage)
if !cursor.IsZero() {
for scope := range cache.ResourcesSubjectToPolicyScope(tt.scope, cache.WithCursor(cursor)) {
for scope := range cache.ScopeAndDescendants(tt.scope, cache.WithCursor(cursor)) {
for item := range scope.Items() {
remaining = append(remaining, item)
}
@@ -1073,12 +1073,12 @@ func TestCacheOperations(t *testing.T) {
require.Equal(t, 0, cache.Len())
// verify empty reads of root
require.Equal(t, map[string][]string{}, collectScopedItemKeys(cache.PoliciesApplicableToResourceScope("/")))
require.Equal(t, map[string][]string{}, collectScopedItemKeys(cache.ResourcesSubjectToPolicyScope("/")))
require.Equal(t, map[string][]string{}, collectScopedItemKeys(cache.ScopeAndAncestors("/")))
require.Equal(t, map[string][]string{}, collectScopedItemKeys(cache.ScopeAndDescendants("/")))
// verify empty sub-scope reads
require.Equal(t, map[string][]string{}, collectScopedItemKeys(cache.PoliciesApplicableToResourceScope("/child")))
require.Equal(t, map[string][]string{}, collectScopedItemKeys(cache.ResourcesSubjectToPolicyScope("/child")))
require.Equal(t, map[string][]string{}, collectScopedItemKeys(cache.ScopeAndAncestors("/child")))
require.Equal(t, map[string][]string{}, collectScopedItemKeys(cache.ScopeAndDescendants("/child")))
for _, item := range items {
cache.Put(item)
@@ -1090,49 +1090,49 @@ func TestCacheOperations(t *testing.T) {
require.Equal(t, map[string][]string{
"/": {"root-scoped", "root-scoped-other"},
}, collectScopedItemKeys(cache.PoliciesApplicableToResourceScope("/")))
}, collectScopedItemKeys(cache.ScopeAndAncestors("/")))
require.Equal(t, map[string][]string{
"/": {"root-scoped", "root-scoped-other"},
"/child": {"child-scoped", "child-scoped-other"},
}, collectScopedItemKeys(cache.PoliciesApplicableToResourceScope("/child")))
}, collectScopedItemKeys(cache.ScopeAndAncestors("/child")))
require.Equal(t, map[string][]string{
"/": {"root-scoped", "root-scoped-other"},
"/child": {"child-scoped", "child-scoped-other"},
"/child/subchild": {"child-sub-scoped"},
}, collectScopedItemKeys(cache.PoliciesApplicableToResourceScope("/child/subchild")))
}, collectScopedItemKeys(cache.ScopeAndAncestors("/child/subchild")))
// verify basic resources-subject-to-policy iteration
require.Equal(t, map[string][]string{},
collectScopedItemKeys(cache.ResourcesSubjectToPolicyScope("/nonexistent")))
collectScopedItemKeys(cache.ScopeAndDescendants("/nonexistent")))
require.Equal(t, map[string][]string{
"/child/subchild": {"child-sub-scoped"},
}, collectScopedItemKeys(cache.ResourcesSubjectToPolicyScope("/child/subchild")))
}, collectScopedItemKeys(cache.ScopeAndDescendants("/child/subchild")))
require.Equal(t, map[string][]string{
"/child": {"child-scoped", "child-scoped-other"},
"/child/subchild": {"child-sub-scoped"},
}, collectScopedItemKeys(cache.ResourcesSubjectToPolicyScope("/child")))
}, collectScopedItemKeys(cache.ScopeAndDescendants("/child")))
require.Equal(t, map[string][]string{
"/": {"root-scoped", "root-scoped-other"},
"/child": {"child-scoped", "child-scoped-other"},
"/child/subchild": {"child-sub-scoped"},
"/orthogonal": {"child-orthogonal"},
}, collectScopedItemKeys(cache.ResourcesSubjectToPolicyScope("/")))
}, collectScopedItemKeys(cache.ScopeAndDescendants("/")))
// verify that the concept of policies-applicable-to-resource used by the cache
// matches up with the definition expected by the scopes package.
for scope := range cache.PoliciesApplicableToResourceScope("/child/subchild") {
for scope := range cache.ScopeAndAncestors("/child/subchild") {
require.True(t, scopes.ResourceScope("/child/subchild").IsSubjectToScopeOfEffect(scope.Scope()), "scope=%s", scope.Scope())
}
// verify that the concept of resources-subject-to-policy used by the cache
// matches up with the definition expected by the scopes package.
for scope := range cache.ResourcesSubjectToPolicyScope("/child") {
for scope := range cache.ScopeAndDescendants("/child") {
require.True(t, scopes.ScopeOfEffect("/child").AppliesToResourceScope(scope.Scope()), "scope=%s", scope.Scope())
}
@@ -1144,14 +1144,14 @@ func TestCacheOperations(t *testing.T) {
"/": {"root-scoped", "root-scoped-other"},
"/child": {"child-scoped-other"},
"/child/subchild": {"child-sub-scoped"},
}, collectScopedItemKeys(cache.PoliciesApplicableToResourceScope("/child/subchild")))
}, collectScopedItemKeys(cache.ScopeAndAncestors("/child/subchild")))
require.Equal(t, map[string][]string{
"/": {"root-scoped", "root-scoped-other"},
"/child": {"child-scoped-other"},
"/child/subchild": {"child-sub-scoped"},
"/orthogonal": {"child-orthogonal"},
}, collectScopedItemKeys(cache.ResourcesSubjectToPolicyScope("/")))
}, collectScopedItemKeys(cache.ScopeAndDescendants("/")))
// verify deletion of a single root-scoped item
cache.Del("root-scoped")
@@ -1161,14 +1161,14 @@ func TestCacheOperations(t *testing.T) {
"/": {"root-scoped-other"},
"/child": {"child-scoped-other"},
"/child/subchild": {"child-sub-scoped"},
}, collectScopedItemKeys(cache.PoliciesApplicableToResourceScope("/child/subchild")))
}, collectScopedItemKeys(cache.ScopeAndAncestors("/child/subchild")))
require.Equal(t, map[string][]string{
"/": {"root-scoped-other"},
"/child": {"child-scoped-other"},
"/child/subchild": {"child-sub-scoped"},
"/orthogonal": {"child-orthogonal"},
}, collectScopedItemKeys(cache.ResourcesSubjectToPolicyScope("/")))
}, collectScopedItemKeys(cache.ScopeAndDescendants("/")))
// verify full deletion of all contents of an intermediate scope
cache.Del("child-scoped-other")
@@ -1177,13 +1177,13 @@ func TestCacheOperations(t *testing.T) {
require.Equal(t, map[string][]string{
"/": {"root-scoped-other"},
"/child/subchild": {"child-sub-scoped"},
}, collectScopedItemKeys(cache.PoliciesApplicableToResourceScope("/child/subchild")))
}, collectScopedItemKeys(cache.ScopeAndAncestors("/child/subchild")))
require.Equal(t, map[string][]string{
"/": {"root-scoped-other"},
"/child/subchild": {"child-sub-scoped"},
"/orthogonal": {"child-orthogonal"},
}, collectScopedItemKeys(cache.ResourcesSubjectToPolicyScope("/")))
}, collectScopedItemKeys(cache.ScopeAndDescendants("/")))
// verfiy full deletion of all contents of a root scope
cache.Del("root-scoped-other")
@@ -1191,23 +1191,23 @@ func TestCacheOperations(t *testing.T) {
require.Equal(t, map[string][]string{
"/child/subchild": {"child-sub-scoped"},
}, collectScopedItemKeys(cache.PoliciesApplicableToResourceScope("/child/subchild")))
}, collectScopedItemKeys(cache.ScopeAndAncestors("/child/subchild")))
require.Equal(t, map[string][]string{
"/child/subchild": {"child-sub-scoped"},
"/orthogonal": {"child-orthogonal"},
}, collectScopedItemKeys(cache.ResourcesSubjectToPolicyScope("/")))
}, collectScopedItemKeys(cache.ScopeAndDescendants("/")))
// verify deletion of leaf scope
cache.Del("child-sub-scoped")
require.Equal(t, len(items)-5, cache.Len())
require.Equal(t, map[string][]string{},
collectScopedItemKeys(cache.PoliciesApplicableToResourceScope("/child/subchild")))
collectScopedItemKeys(cache.ScopeAndAncestors("/child/subchild")))
require.Equal(t, map[string][]string{
"/orthogonal": {"child-orthogonal"},
}, collectScopedItemKeys(cache.ResourcesSubjectToPolicyScope("/")))
}, collectScopedItemKeys(cache.ScopeAndDescendants("/")))
// verify basic re-add
cache.Put(item[string]{
@@ -1218,12 +1218,12 @@ func TestCacheOperations(t *testing.T) {
require.Equal(t, map[string][]string{
"/child": {"child-scoped"},
}, collectScopedItemKeys(cache.PoliciesApplicableToResourceScope("/child")))
}, collectScopedItemKeys(cache.ScopeAndAncestors("/child")))
require.Equal(t, map[string][]string{
"/child": {"child-scoped"},
"/orthogonal": {"child-orthogonal"},
}, collectScopedItemKeys(cache.ResourcesSubjectToPolicyScope("/")))
}, collectScopedItemKeys(cache.ScopeAndDescendants("/")))
// verify overwrite of existing item by primary key
cache.Put(item[string]{
@@ -1233,16 +1233,16 @@ func TestCacheOperations(t *testing.T) {
require.Equal(t, len(items)-4, cache.Len())
require.Equal(t, map[string][]string{},
collectScopedItemKeys(cache.PoliciesApplicableToResourceScope("/child/subchild")))
collectScopedItemKeys(cache.ScopeAndAncestors("/child/subchild")))
require.Equal(t, map[string][]string{
"/child/other": {"child-scoped"},
}, collectScopedItemKeys(cache.PoliciesApplicableToResourceScope("/child/other")))
}, collectScopedItemKeys(cache.ScopeAndAncestors("/child/other")))
require.Equal(t, map[string][]string{
"/child/other": {"child-scoped"},
"/orthogonal": {"child-orthogonal"},
}, collectScopedItemKeys(cache.ResourcesSubjectToPolicyScope("/")))
}, collectScopedItemKeys(cache.ScopeAndDescendants("/")))
}
// collectScopedItemKeys aggregates a scoped iterator from one of the cache iteration
+152
View File
@@ -0,0 +1,152 @@
/*
* Teleport
* Copyright (C) 2026 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package cache
import (
"testing"
"github.com/stretchr/testify/require"
scopesv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/v1"
)
// newFilterMatchCache returns a cache pre-populated with items that exercise the filter-matching logic.
func newFilterMatchCache(t *testing.T) *Cache[item[int], int] {
t.Helper()
c, err := New(Config[item[int], int]{
Scope: (item[int]).Scope,
Key: (item[int]).Key,
})
require.NoError(t, err)
filterMatchItems := []item[int]{
{1, "/"},
{2, "/aa"},
{3, "/aa/bb"},
{4, "/aa/bb/cc"},
{5, "/aa/cc"},
}
for _, it := range filterMatchItems {
c.Put(it)
}
return c
}
// TestCacheExactScope verifies that ExactScope yields only the members at exactly the given scope.
func TestCacheExactScope(t *testing.T) {
t.Parallel()
c := newFilterMatchCache(t)
tests := []struct {
scope string
want map[string][]int
}{
{scope: "/aa/bb", want: map[string][]int{"/aa/bb": {3}}},
{scope: "/", want: map[string][]int{"/": {1}}},
{scope: "/aa/bb/cc", want: map[string][]int{"/aa/bb/cc": {4}}},
{scope: "/nonexistent", want: map[string][]int{}},
}
for _, tt := range tests {
t.Run(tt.scope, func(t *testing.T) {
require.Equal(t, tt.want, collectScopedItemKeys(c.ExactScope(tt.scope)))
})
}
}
// TestResourcesMatchingScopeFilter verifies that the primary-filter mode is mapped to the correct cache
// traversal, including the wildcard (nil/unspecified/all) and unscoped (matches-nothing) cases.
func TestResourcesMatchingScopeFilter(t *testing.T) {
t.Parallel()
c := newFilterMatchCache(t)
const filterScope = "/aa/bb"
all := map[string][]int{"/": {1}, "/aa": {2}, "/aa/bb": {3}, "/aa/bb/cc": {4}, "/aa/cc": {5}}
tests := []struct {
name string
filter *scopesv1.Filter
want map[string][]int
}{
{
name: "nil is wildcard",
filter: nil,
want: all,
},
{
name: "unspecified is wildcard",
filter: scopesv1.Filter_builder{Mode: scopesv1.Mode_MODE_UNSPECIFIED}.Build(),
want: all,
},
{
name: "all is wildcard",
filter: scopesv1.Filter_builder{Mode: scopesv1.Mode_MODE_ALL}.Build(),
want: all,
},
{
name: "exact",
filter: scopesv1.Filter_builder{Mode: scopesv1.Mode_MODE_EXACT, Scope: filterScope}.Build(),
want: map[string][]int{"/aa/bb": {3}},
},
{
name: "descendants",
filter: scopesv1.Filter_builder{Mode: scopesv1.Mode_MODE_DESCENDANTS, Scope: filterScope}.Build(),
want: map[string][]int{"/aa/bb": {3}, "/aa/bb/cc": {4}},
},
{
name: "ancestors",
filter: scopesv1.Filter_builder{Mode: scopesv1.Mode_MODE_ANCESTORS, Scope: filterScope}.Build(),
want: map[string][]int{"/": {1}, "/aa": {2}, "/aa/bb": {3}},
},
{
name: "relatives",
filter: scopesv1.Filter_builder{Mode: scopesv1.Mode_MODE_RELATIVES, Scope: filterScope}.Build(),
want: map[string][]int{"/": {1}, "/aa": {2}, "/aa/bb": {3}, "/aa/bb/cc": {4}},
},
{
name: "unscoped matches nothing",
filter: scopesv1.Filter_builder{Mode: scopesv1.Mode_MODE_UNSCOPED}.Build(),
want: map[string][]int{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
getter, err := c.ResourcesMatchingScopeFilter(tt.filter)
require.NoError(t, err)
require.Equal(t, tt.want, collectScopedItemKeys(getter))
})
}
}
// TestResourcesMatchingScopeFilterInvalid verifies that a malformed filter is rejected rather than silently
// mishandled.
func TestResourcesMatchingScopeFilterInvalid(t *testing.T) {
t.Parallel()
c := newFilterMatchCache(t)
// EXACT requires a non-empty scope.
_, err := c.ResourcesMatchingScopeFilter(scopesv1.Filter_builder{Mode: scopesv1.Mode_MODE_EXACT}.Build())
require.Error(t, err)
// UNSCOPED requires an empty scope.
_, err = c.ResourcesMatchingScopeFilter(scopesv1.Filter_builder{Mode: scopesv1.Mode_MODE_UNSCOPED, Scope: "/aa"}.Build())
require.Error(t, err)
}
+7 -35
View File
@@ -26,8 +26,6 @@ import (
"google.golang.org/protobuf/proto"
scopedaccessv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/access/v1"
scopespb "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/v1"
"github.com/gravitational/teleport/lib/scopes"
scopedaccess "github.com/gravitational/teleport/lib/scopes/access"
"github.com/gravitational/teleport/lib/scopes/cache"
)
@@ -89,43 +87,11 @@ func (c *RoleCache) ListScopedRolesWithFilter(ctx context.Context, req *scopedac
pageSize = maxPageSize
}
if req.GetResourceScope() != nil && req.GetAssignableScope() != nil {
return nil, trace.BadParameter("cannot filter by both resource scope and assignable scope simultaneously")
}
cursor, err := cache.DecodeStringCursor(req.GetPageToken())
if err != nil {
return nil, trace.Wrap(err)
}
// resources subject to policy scope root is basically the scoped resource equivalent
// of a "get all". this is a reasonable default for most queries.
getter := c.cache.ResourcesSubjectToPolicyScope
scope := scopes.Root
// if a resource scope filter has been provided, the user has specified a custom scope/mode to
// query by.
if req.GetResourceScope() != nil {
// a resource-scope based filter has been provided
switch req.GetResourceScope().GetMode() {
case scopespb.Mode_MODE_RESOURCES_SUBJECT_TO_SCOPE:
getter = c.cache.ResourcesSubjectToPolicyScope
case scopespb.Mode_MODE_POLICIES_APPLICABLE_TO_SCOPE:
getter = c.cache.PoliciesApplicableToResourceScope
default:
return nil, trace.BadParameter("unsupported or unspecified scoping mode %q in scoped role resource scope filter", req.GetResourceScope().GetMode())
}
scope = req.GetResourceScope().GetScope()
}
if req.GetAssignableScope() != nil {
// NOTE: we eventually want to be able to query roles by the scopes at which they are assignable,
// instead of just resource scope. this is important for introspection/auditing but not necessary for
// the core funcationality of teleport, so implementation has been deferred. (will require implementation
// of a more complex caching structure where each resource will be associable with multiple scopes)
return nil, trace.NotImplemented("assignable scope filtering for scoped role roles is not yet supported")
}
finalFilter := filter
if nameFilter := req.GetNameFilter(); nameFilter != "" {
finalFilter = func(role *scopedaccessv1.ScopedRole) bool {
@@ -133,10 +99,16 @@ func (c *RoleCache) ListScopedRolesWithFilter(ctx context.Context, req *scopedac
}
}
// the primary scope filter selects the cache traversal strategy.
resources, err := c.cache.ResourcesMatchingScopeFilter(req.GetScopeFilter(), c.cache.WithCursor(cursor), c.cache.WithFilter(finalFilter))
if err != nil {
return nil, trace.Wrap(err)
}
var out []*scopedaccessv1.ScopedRole
var nextCursor cache.Cursor[string]
Outer:
for scope := range getter(scope, c.cache.WithCursor(cursor), c.cache.WithFilter(finalFilter)) {
for scope := range resources {
for role := range scope.Items() {
if len(out) == pageSize {
nextCursor = cache.Cursor[string]{
+24 -24
View File
@@ -121,8 +121,8 @@ func TestListScopedRolesScenarios(t *testing.T) {
// verify expected behavior for standard cursors in resources subject to scope mode
rsp, err := cache.ListScopedRoles(t.Context(), scopedaccessv1.ListScopedRolesRequest_builder{
ResourceScope: scopespb.Filter_builder{
Mode: scopespb.Mode_MODE_RESOURCES_SUBJECT_TO_SCOPE,
ScopeFilter: scopespb.Filter_builder{
Mode: scopespb.Mode_MODE_DESCENDANTS,
Scope: "/aa",
}.Build(),
PageToken: "v1:user-02@/aa",
@@ -133,8 +133,8 @@ func TestListScopedRolesScenarios(t *testing.T) {
// try to inject a malicious root out-of-band cursor in resources subject to scope mode (no effect)
rsp, err = cache.ListScopedRoles(t.Context(), scopedaccessv1.ListScopedRolesRequest_builder{
ResourceScope: scopespb.Filter_builder{
Mode: scopespb.Mode_MODE_RESOURCES_SUBJECT_TO_SCOPE,
ScopeFilter: scopespb.Filter_builder{
Mode: scopespb.Mode_MODE_DESCENDANTS,
Scope: "/aa",
}.Build(),
PageToken: "v1:user-01@/",
@@ -145,8 +145,8 @@ func TestListScopedRolesScenarios(t *testing.T) {
// try to inject a malicious orthogonal out-of-band cursor in resources subject to scope mode (no effect)
rsp, err = cache.ListScopedRoles(t.Context(), scopedaccessv1.ListScopedRolesRequest_builder{
ResourceScope: scopespb.Filter_builder{
Mode: scopespb.Mode_MODE_RESOURCES_SUBJECT_TO_SCOPE,
ScopeFilter: scopespb.Filter_builder{
Mode: scopespb.Mode_MODE_DESCENDANTS,
Scope: "/aa",
}.Build(),
PageToken: "v1:intern-01@/bb",
@@ -157,8 +157,8 @@ func TestListScopedRolesScenarios(t *testing.T) {
// verify expected behavior for standard cursors in policies applicable to scope mode
rsp, err = cache.ListScopedRoles(t.Context(), scopedaccessv1.ListScopedRolesRequest_builder{
ResourceScope: scopespb.Filter_builder{
Mode: scopespb.Mode_MODE_POLICIES_APPLICABLE_TO_SCOPE,
ScopeFilter: scopespb.Filter_builder{
Mode: scopespb.Mode_MODE_ANCESTORS,
Scope: "/aa",
}.Build(),
PageToken: "v1:user-01@/",
@@ -170,8 +170,8 @@ func TestListScopedRolesScenarios(t *testing.T) {
// try to inject a malicious child out-of-band cursor in policies applicable to scope mode (effect is
// to ignore all items in valid query path).
rsp, err = cache.ListScopedRoles(t.Context(), scopedaccessv1.ListScopedRolesRequest_builder{
ResourceScope: scopespb.Filter_builder{
Mode: scopespb.Mode_MODE_POLICIES_APPLICABLE_TO_SCOPE,
ScopeFilter: scopespb.Filter_builder{
Mode: scopespb.Mode_MODE_ANCESTORS,
Scope: "/aa",
}.Build(),
PageToken: "v1:user-03@/aa/bb",
@@ -183,8 +183,8 @@ func TestListScopedRolesScenarios(t *testing.T) {
// try to inject a malicious orthogonal out-of-band cursor in policies applicable to scope mode (effect is to
// ignore root, but process leaf normally).
rsp, err = cache.ListScopedRoles(t.Context(), scopedaccessv1.ListScopedRolesRequest_builder{
ResourceScope: scopespb.Filter_builder{
Mode: scopespb.Mode_MODE_POLICIES_APPLICABLE_TO_SCOPE,
ScopeFilter: scopespb.Filter_builder{
Mode: scopespb.Mode_MODE_ANCESTORS,
Scope: "/aa",
}.Build(),
PageToken: "v1:intern-01@/bb",
@@ -195,8 +195,8 @@ func TestListScopedRolesScenarios(t *testing.T) {
// verify rejection of unknown cursor version
_, err = cache.ListScopedRoles(t.Context(), scopedaccessv1.ListScopedRolesRequest_builder{
ResourceScope: scopespb.Filter_builder{
Mode: scopespb.Mode_MODE_RESOURCES_SUBJECT_TO_SCOPE,
ScopeFilter: scopespb.Filter_builder{
Mode: scopespb.Mode_MODE_DESCENDANTS,
Scope: "/aa",
}.Build(),
PageToken: "v2:user-02@/aa",
@@ -327,8 +327,8 @@ func TestListScopedRolesBasics(t *testing.T) {
{
name: "resource scope root",
req: scopedaccessv1.ListScopedRolesRequest_builder{
ResourceScope: scopespb.Filter_builder{
Mode: scopespb.Mode_MODE_RESOURCES_SUBJECT_TO_SCOPE,
ScopeFilter: scopespb.Filter_builder{
Mode: scopespb.Mode_MODE_DESCENDANTS,
Scope: "/",
}.Build(),
}.Build(),
@@ -345,8 +345,8 @@ func TestListScopedRolesBasics(t *testing.T) {
{
name: "resource scope non-root",
req: scopedaccessv1.ListScopedRolesRequest_builder{
ResourceScope: scopespb.Filter_builder{
Mode: scopespb.Mode_MODE_RESOURCES_SUBJECT_TO_SCOPE,
ScopeFilter: scopespb.Filter_builder{
Mode: scopespb.Mode_MODE_DESCENDANTS,
Scope: "/aa",
}.Build(),
}.Build(),
@@ -360,8 +360,8 @@ func TestListScopedRolesBasics(t *testing.T) {
{
name: "policy scope root",
req: scopedaccessv1.ListScopedRolesRequest_builder{
ResourceScope: scopespb.Filter_builder{
Mode: scopespb.Mode_MODE_POLICIES_APPLICABLE_TO_SCOPE,
ScopeFilter: scopespb.Filter_builder{
Mode: scopespb.Mode_MODE_ANCESTORS,
Scope: "/",
}.Build(),
}.Build(),
@@ -375,8 +375,8 @@ func TestListScopedRolesBasics(t *testing.T) {
{
name: "policy scope non-root",
req: scopedaccessv1.ListScopedRolesRequest_builder{
ResourceScope: scopespb.Filter_builder{
Mode: scopespb.Mode_MODE_POLICIES_APPLICABLE_TO_SCOPE,
ScopeFilter: scopespb.Filter_builder{
Mode: scopespb.Mode_MODE_ANCESTORS,
Scope: "/aa",
}.Build(),
}.Build(),
@@ -428,8 +428,8 @@ func TestListScopedRolesBasics(t *testing.T) {
name: "name filter policy scope root",
req: scopedaccessv1.ListScopedRolesRequest_builder{
NameFilter: "01",
ResourceScope: scopespb.Filter_builder{
Mode: scopespb.Mode_MODE_POLICIES_APPLICABLE_TO_SCOPE,
ScopeFilter: scopespb.Filter_builder{
Mode: scopespb.Mode_MODE_ANCESTORS,
Scope: "/",
}.Build(),
}.Build(),
+109
View File
@@ -0,0 +1,109 @@
/*
* Teleport
* Copyright (C) 2026 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package scopes
import (
"github.com/gravitational/trace"
scopesv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/v1"
)
// IsMatchAll reports whether the given filter is a wildcard match that selects all resources.
func IsMatchAll(filter *scopesv1.Filter) bool {
// an unspecified filter and MODE_ALL are both treated as a wildcard match at the matching layer. see
// [MatchScope] for discussion of why these are equivalent here despite being authorized differently
// at the API/authz layer.
mode := filter.GetMode()
return mode == scopesv1.Mode_MODE_UNSPECIFIED || mode == scopesv1.Mode_MODE_ALL
}
// MatchScope reports whether a resource at the given scope matches the supplied filter. This is the
// authoritative scope-matching logic used by caches/backends to decide which resources a filter selects.
//
// The empty string ("") is used to represent an unscoped resource and is treated as orthogonal to all
// scoped values (it is *not* equivalent to the root scope). As a result, the relationship-based modes
// never match unscoped resources, and MODE_UNSCOPED matches only unscoped resources.
//
// Matching logic here treats empty/unspecified filters and MODE_ALL as equivalent wildcard matchers. However,
// the API/authz defaults an empty/unspecified filter to being one of UNSCOPED or EXACT depending on the
// caller's identity. This helps ensure that outdated or un-explicit calls result in safe/conservative defaults.
//
// This function does not validate its inputs. Use [ValidateFilter] to verify that a filter is well-formed
// (e.g. that the scope and mode are mutually consistent) before relying on it.
func MatchScope(filter *scopesv1.Filter, resourceScope string) bool {
if IsMatchAll(filter) {
return true
}
mode := filter.GetMode()
if mode == scopesv1.Mode_MODE_UNSCOPED {
// unscoped resources only.
return resourceScope == ""
}
// all remaining modes select resources by the relationship of the resource scope to the filter scope.
rel := Compare(filter.GetScope(), resourceScope)
switch mode {
case scopesv1.Mode_MODE_EXACT:
return rel == Equivalent
case scopesv1.Mode_MODE_DESCENDANTS:
return rel == Equivalent || rel == Descendant
case scopesv1.Mode_MODE_ANCESTORS, scopesv1.Mode_MODE_POLICIES_APPLICABLE_TO_SCOPE: //nolint:staticcheck // SA1019. Deprecated mode retained as equivalent for backwards compatibility.
return rel == Equivalent || rel == Ancestor
case scopesv1.Mode_MODE_RELATIVES:
return rel != Orthogonal
default:
// unknown modes match nothing.
return false
}
}
// ValidateFilter checks that a filter is well-formed: that its mode is recognized and that its scope is
// consistent with its mode. Relational modes (EXACT/DESCENDANTS/ANCESTORS/RELATIVES) require a
// non-empty/valid scope, while the non-relational modes (UNSCOPED/ALL) and the unspecified mode
// require an empty scope. A nil or unspecified filter is considered valid.
func ValidateFilter(filter *scopesv1.Filter) error {
switch filter.GetMode() {
case scopesv1.Mode_MODE_UNSPECIFIED:
if filter.GetScope() != "" {
return trace.BadParameter("scope filter specifies a scope %q without a mode", filter.GetScope())
}
return nil
case scopesv1.Mode_MODE_EXACT,
scopesv1.Mode_MODE_DESCENDANTS,
scopesv1.Mode_MODE_ANCESTORS,
scopesv1.Mode_MODE_RELATIVES,
scopesv1.Mode_MODE_POLICIES_APPLICABLE_TO_SCOPE: //nolint:staticcheck // SA1019. Deprecated mode retained as equivalent for backwards compatibility.
if filter.GetScope() == "" {
return trace.BadParameter("scope filter mode %v requires a non-empty scope", filter.GetMode())
}
if err := WeakValidate(filter.GetScope()); err != nil {
return trace.Wrap(err, "invalid scope in scope filter")
}
return nil
case scopesv1.Mode_MODE_UNSCOPED, scopesv1.Mode_MODE_ALL:
if filter.GetScope() != "" {
return trace.BadParameter("scope filter mode %v requires an empty scope, got %q", filter.GetMode(), filter.GetScope())
}
return nil
default:
return trace.BadParameter("unknown scope filter mode %v", filter.GetMode())
}
}
+243
View File
@@ -0,0 +1,243 @@
/*
* Teleport
* Copyright (C) 2026 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package scopes
import (
"testing"
"github.com/stretchr/testify/require"
scopesv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/v1"
)
// TestMatchScope verifies the core scope-matching logic across all modes and scope relationships,
// including the unscoped ("") resource scope and the wildcard (nil/unspecified/all) cases.
func TestMatchScope(t *testing.T) {
t.Parallel()
// resourceScopes exercises the full set of relationships relative to a filter scope of "/aa/bb":
// the exact scope, an ancestor, the root ancestor, a descendant, an orthogonal scope, and unscoped.
const filterScope = "/aa/bb"
tests := []struct {
name string
mode scopesv1.Mode
// want maps a resource scope to whether it should match.
want map[string]bool
}{
{
name: "exact",
mode: scopesv1.Mode_MODE_EXACT,
want: map[string]bool{
"/aa/bb": true,
"/aa": false,
"/": false,
"/aa/bb/cc": false,
"/aa/cc": false,
"/xx": false,
"": false,
},
},
{
name: "descendants",
mode: scopesv1.Mode_MODE_DESCENDANTS,
want: map[string]bool{
"/aa/bb": true,
"/aa": false,
"/": false,
"/aa/bb/cc": true,
"/aa/cc": false,
"/xx": false,
"": false,
},
},
{
name: "ancestors",
mode: scopesv1.Mode_MODE_ANCESTORS,
want: map[string]bool{
"/aa/bb": true,
"/aa": true,
"/": true,
"/aa/bb/cc": false,
"/aa/cc": false,
"/xx": false,
"": false,
},
},
{
name: "relatives",
mode: scopesv1.Mode_MODE_RELATIVES,
want: map[string]bool{
"/aa/bb": true,
"/aa": true,
"/": true,
"/aa/bb/cc": true,
"/aa/cc": false,
"/xx": false,
"": false,
},
},
{
name: "unscoped",
mode: scopesv1.Mode_MODE_UNSCOPED,
want: map[string]bool{
"/aa/bb": false,
"/aa": false,
"/": false,
"/aa/bb/cc": false,
"/aa/cc": false,
"/xx": false,
"": true,
},
},
{
name: "all is a wildcard",
mode: scopesv1.Mode_MODE_ALL,
want: map[string]bool{
"/aa/bb": true,
"/aa": true,
"/": true,
"/aa/bb/cc": true,
"/aa/cc": true,
"/xx": true,
"": true,
},
},
{
name: "unspecified is a wildcard",
mode: scopesv1.Mode_MODE_UNSPECIFIED,
want: map[string]bool{
"/aa/bb": true,
"/aa": true,
"/": true,
"/aa/bb/cc": true,
"/aa/cc": true,
"/xx": true,
"": true,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
filter := scopesv1.Filter_builder{
Scope: filterScope,
Mode: tt.mode,
}.Build()
for resourceScope, want := range tt.want {
require.Equal(t, want, MatchScope(filter, resourceScope), "resourceScope=%q", resourceScope)
}
})
}
}
// TestMatchScopeNilFilter verifies that a nil filter is treated as a wildcard match.
func TestMatchScopeNilFilter(t *testing.T) {
t.Parallel()
for _, resourceScope := range []string{"", "/", "/aa", "/aa/bb"} {
require.True(t, MatchScope(nil, resourceScope), "resourceScope=%q", resourceScope)
}
}
// TestValidateFilter verifies basic filter validation.
func TestValidateFilter(t *testing.T) {
t.Parallel()
tests := []struct {
name string
filter *scopesv1.Filter
wantErr bool
}{
{
name: "nil filter is valid",
filter: nil,
},
{
name: "unspecified with empty scope is valid",
filter: scopesv1.Filter_builder{Mode: scopesv1.Mode_MODE_UNSPECIFIED}.Build(),
},
{
name: "unspecified with scope is invalid",
filter: scopesv1.Filter_builder{Mode: scopesv1.Mode_MODE_UNSPECIFIED, Scope: "/aa"}.Build(),
wantErr: true,
},
{
name: "exact with scope is valid",
filter: scopesv1.Filter_builder{Mode: scopesv1.Mode_MODE_EXACT, Scope: "/aa/bb"}.Build(),
},
{
name: "exact without scope is invalid",
filter: scopesv1.Filter_builder{Mode: scopesv1.Mode_MODE_EXACT}.Build(),
wantErr: true,
},
{
name: "descendants with scope is valid",
filter: scopesv1.Filter_builder{Mode: scopesv1.Mode_MODE_DESCENDANTS, Scope: "/aa"}.Build(),
},
{
name: "ancestors with scope is valid",
filter: scopesv1.Filter_builder{Mode: scopesv1.Mode_MODE_ANCESTORS, Scope: "/aa"}.Build(),
},
{
name: "relatives with scope is valid",
filter: scopesv1.Filter_builder{Mode: scopesv1.Mode_MODE_RELATIVES, Scope: "/aa"}.Build(),
},
{
name: "relationship mode with invalid scope is invalid",
filter: scopesv1.Filter_builder{Mode: scopesv1.Mode_MODE_EXACT, Scope: "/aa/b@b"}.Build(),
wantErr: true,
},
{
name: "unscoped with empty scope is valid",
filter: scopesv1.Filter_builder{Mode: scopesv1.Mode_MODE_UNSCOPED}.Build(),
},
{
name: "unscoped with scope is invalid",
filter: scopesv1.Filter_builder{Mode: scopesv1.Mode_MODE_UNSCOPED, Scope: "/aa"}.Build(),
wantErr: true,
},
{
name: "all with empty scope is valid",
filter: scopesv1.Filter_builder{Mode: scopesv1.Mode_MODE_ALL}.Build(),
},
{
name: "all with scope is invalid",
filter: scopesv1.Filter_builder{Mode: scopesv1.Mode_MODE_ALL, Scope: "/aa"}.Build(),
wantErr: true,
},
{
name: "unknown mode is invalid",
filter: scopesv1.Filter_builder{Mode: scopesv1.Mode(999)}.Build(),
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateFilter(tt.filter)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}
+19 -11
View File
@@ -105,12 +105,8 @@ func (s *ScopedAccessService) GetScopedRole(ctx context.Context, req *scopedacce
// ListScopedRoles returns a paginated list of scoped roles.
// NOTE: this method is only used by local auth caches, and doesn't implement sorting, filtering, or pagination.
func (s *ScopedAccessService) ListScopedRoles(ctx context.Context, req *scopedaccessv1.ListScopedRolesRequest) (*scopedaccessv1.ListScopedRolesResponse, error) {
if req.GetResourceScope() != nil {
return nil, trace.NotImplemented("filtering by resource scope is not implemented for direct backend scoped role reads")
}
if req.GetAssignableScope() != nil {
return nil, trace.NotImplemented("filtering by assignable scope is not implemented for direct backend scoped role reads")
if err := scopes.ValidateFilter(req.GetScopeFilter()); err != nil {
return nil, trace.Wrap(err)
}
if req.GetNameFilter() != "" {
@@ -127,6 +123,10 @@ func (s *ScopedAccessService) ListScopedRoles(ctx context.Context, req *scopedac
return nil, trace.Wrap(err)
}
if !scopes.MatchScope(req.GetScopeFilter(), role.GetScope()) {
continue
}
out = append(out, role)
}
@@ -375,12 +375,11 @@ func (s *ScopedAccessService) GetScopedRoleAssignment(ctx context.Context, req *
// ListScopedRoleAssignments returns a paginated list of scoped role assignments.
// NOTE: this method is only used by local auth caches, and doesn't implement sorting, filtering, or pagination.
func (s *ScopedAccessService) ListScopedRoleAssignments(ctx context.Context, req *scopedaccessv1.ListScopedRoleAssignmentsRequest) (*scopedaccessv1.ListScopedRoleAssignmentsResponse, error) {
if req.GetResourceScope() != nil {
return nil, trace.NotImplemented("filtering by resource scope is not implemented for direct backend scoped role assignment reads")
if err := scopes.ValidateFilter(req.GetScopeFilter()); err != nil {
return nil, trace.Wrap(err)
}
if req.GetAssignedScope() != nil {
return nil, trace.NotImplemented("filtering by assigned scope is not implemented for direct backend scoped role assignment reads")
if err := scopes.ValidateFilter(req.GetAssignedScopeFilter()); err != nil {
return nil, trace.Wrap(err)
}
if req.GetPageToken() != "" {
@@ -392,6 +391,15 @@ func (s *ScopedAccessService) ListScopedRoleAssignments(ctx context.Context, req
if err != nil {
return nil, trace.Wrap(err)
}
if !scopes.MatchScope(req.GetScopeFilter(), assignment.GetScope()) {
continue
}
if !scopedaccess.MatchSecondaryAssignmentFilters(req, assignment) {
continue
}
out = append(out, assignment)
}
+8 -29
View File
@@ -29,7 +29,6 @@ import (
"google.golang.org/protobuf/types/known/timestamppb"
joiningv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/joining/v1"
scopesv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/v1"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/utils/retryutils"
"github.com/gravitational/teleport/lib/backend"
@@ -216,21 +215,6 @@ func (s *ScopedTokenService) UseScopedToken(ctx context.Context, token *joiningv
return token, nil
}
func evalScopeFilter(filter *scopesv1.Filter, scope string) bool {
if filter == nil {
return true
}
switch filter.GetMode() {
case scopesv1.Mode_MODE_RESOURCES_SUBJECT_TO_SCOPE:
return scopes.ResourceScope(scope).IsSubjectToScopeOfEffect(filter.GetScope())
case scopesv1.Mode_MODE_POLICIES_APPLICABLE_TO_SCOPE:
return scopes.ScopeOfEffect(scope).AppliesToResourceScope(filter.GetScope())
}
return true
}
// ListScopedTokens retrieves a paginated list of scoped join tokens.
func (s *ScopedTokenService) ListScopedTokens(ctx context.Context, req *joiningv1.ListScopedTokensRequest) (*joiningv1.ListScopedTokensResponse, error) {
// we only want to return filters if at least one of the filters
@@ -238,8 +222,8 @@ func (s *ScopedTokenService) ListScopedTokens(ctx context.Context, req *joiningv
// backend can choose to perform a simple list operation instead
// of a list with filter
switch {
case req.HasResourceScope():
case req.HasAssignedScope():
case req.HasScopeFilter():
case req.HasAssignedScopeFilter():
case len(req.GetRoles()) > 0:
case len(req.GetLabels()) > 0:
default:
@@ -257,17 +241,12 @@ func (s *ScopedTokenService) ListScopedTokens(ctx context.Context, req *joiningv
}.Build(), nil
}
if req.GetAssignedScope().GetScope() != "" {
if err := scopes.WeakValidate(req.GetAssignedScope().GetScope()); err != nil {
return nil, trace.BadParameter("invalid scope for assigned filter: %s", req.GetAssignedScope().GetScope())
}
if err := scopes.ValidateFilter(req.GetAssignedScopeFilter()); err != nil {
return nil, trace.Wrap(err, "invalid assigned scope filter")
}
if req.GetResourceScope().GetScope() != "" {
if err := scopes.WeakValidate(req.GetResourceScope().GetScope()); err != nil {
return nil, trace.BadParameter("invalid scope for resource filter: %s", req.GetResourceScope().GetScope())
}
if err := scopes.ValidateFilter(req.GetScopeFilter()); err != nil {
return nil, trace.Wrap(err, "invalid scope filter")
}
filterRoles, err := types.NewTeleportRoles(req.GetRoles())
@@ -287,11 +266,11 @@ func (s *ScopedTokenService) ListScopedTokens(ctx context.Context, req *joiningv
}
}
if !evalScopeFilter(req.GetAssignedScope(), token.GetSpec().GetAssignedScope()) {
if !scopes.MatchScope(req.GetAssignedScopeFilter(), token.GetSpec().GetAssignedScope()) {
return false
}
if !evalScopeFilter(req.GetResourceScope(), token.GetScope()) {
if !scopes.MatchScope(req.GetScopeFilter(), token.GetScope()) {
return false
}
+24 -24
View File
@@ -248,8 +248,8 @@ func TestScopedTokenList(t *testing.T) {
{
name: "tokens assigning scope descendant of /test",
req: joiningv1.ListScopedTokensRequest_builder{
AssignedScope: scopesv1.Filter_builder{
Mode: scopesv1.Mode_MODE_RESOURCES_SUBJECT_TO_SCOPE,
AssignedScopeFilter: scopesv1.Filter_builder{
Mode: scopesv1.Mode_MODE_DESCENDANTS,
Scope: "/test",
}.Build(),
WithSecrets: true,
@@ -259,8 +259,8 @@ func TestScopedTokenList(t *testing.T) {
{
name: "tokens assigning scope descendant of /test/aa",
req: joiningv1.ListScopedTokensRequest_builder{
AssignedScope: scopesv1.Filter_builder{
Mode: scopesv1.Mode_MODE_RESOURCES_SUBJECT_TO_SCOPE,
AssignedScopeFilter: scopesv1.Filter_builder{
Mode: scopesv1.Mode_MODE_DESCENDANTS,
Scope: "/test/aa",
}.Build(),
WithSecrets: true,
@@ -270,8 +270,8 @@ func TestScopedTokenList(t *testing.T) {
{
name: "tokens assigning scope ancestor to /test/bb",
req: joiningv1.ListScopedTokensRequest_builder{
AssignedScope: scopesv1.Filter_builder{
Mode: scopesv1.Mode_MODE_POLICIES_APPLICABLE_TO_SCOPE,
AssignedScopeFilter: scopesv1.Filter_builder{
Mode: scopesv1.Mode_MODE_ANCESTORS,
Scope: "/test/bb",
}.Build(),
WithSecrets: true,
@@ -281,8 +281,8 @@ func TestScopedTokenList(t *testing.T) {
{
name: "tokens descendants of /test",
req: joiningv1.ListScopedTokensRequest_builder{
ResourceScope: scopesv1.Filter_builder{
Mode: scopesv1.Mode_MODE_RESOURCES_SUBJECT_TO_SCOPE,
ScopeFilter: scopesv1.Filter_builder{
Mode: scopesv1.Mode_MODE_DESCENDANTS,
Scope: "/test",
}.Build(),
WithSecrets: true,
@@ -292,8 +292,8 @@ func TestScopedTokenList(t *testing.T) {
{
name: "tokens descendants of /test/aa",
req: joiningv1.ListScopedTokensRequest_builder{
ResourceScope: scopesv1.Filter_builder{
Mode: scopesv1.Mode_MODE_RESOURCES_SUBJECT_TO_SCOPE,
ScopeFilter: scopesv1.Filter_builder{
Mode: scopesv1.Mode_MODE_DESCENDANTS,
Scope: "/test/aa",
}.Build(),
WithSecrets: true,
@@ -303,8 +303,8 @@ func TestScopedTokenList(t *testing.T) {
{
name: "tokens ancestor to /test/bb",
req: joiningv1.ListScopedTokensRequest_builder{
ResourceScope: scopesv1.Filter_builder{
Mode: scopesv1.Mode_MODE_POLICIES_APPLICABLE_TO_SCOPE,
ScopeFilter: scopesv1.Filter_builder{
Mode: scopesv1.Mode_MODE_ANCESTORS,
Scope: "/test/bb",
}.Build(),
WithSecrets: true,
@@ -314,12 +314,12 @@ func TestScopedTokenList(t *testing.T) {
{
name: "tokens descendant of /stage assigning /stage/aa",
req: joiningv1.ListScopedTokensRequest_builder{
ResourceScope: scopesv1.Filter_builder{
Mode: scopesv1.Mode_MODE_RESOURCES_SUBJECT_TO_SCOPE,
ScopeFilter: scopesv1.Filter_builder{
Mode: scopesv1.Mode_MODE_DESCENDANTS,
Scope: "/stage",
}.Build(),
AssignedScope: scopesv1.Filter_builder{
Mode: scopesv1.Mode_MODE_RESOURCES_SUBJECT_TO_SCOPE,
AssignedScopeFilter: scopesv1.Filter_builder{
Mode: scopesv1.Mode_MODE_DESCENDANTS,
Scope: "/stage/aa",
}.Build(),
WithSecrets: true,
@@ -329,12 +329,12 @@ func TestScopedTokenList(t *testing.T) {
{
name: "tokens descendant of /stage/aa assigning /stage/aa",
req: joiningv1.ListScopedTokensRequest_builder{
ResourceScope: scopesv1.Filter_builder{
Mode: scopesv1.Mode_MODE_RESOURCES_SUBJECT_TO_SCOPE,
ScopeFilter: scopesv1.Filter_builder{
Mode: scopesv1.Mode_MODE_DESCENDANTS,
Scope: "/stage/aa",
}.Build(),
AssignedScope: scopesv1.Filter_builder{
Mode: scopesv1.Mode_MODE_RESOURCES_SUBJECT_TO_SCOPE,
AssignedScopeFilter: scopesv1.Filter_builder{
Mode: scopesv1.Mode_MODE_DESCENDANTS,
Scope: "/stage/aa",
}.Build(),
WithSecrets: true,
@@ -344,8 +344,8 @@ func TestScopedTokenList(t *testing.T) {
{
name: "tokens in /test scope applying auth role",
req: joiningv1.ListScopedTokensRequest_builder{
ResourceScope: scopesv1.Filter_builder{
Mode: scopesv1.Mode_MODE_RESOURCES_SUBJECT_TO_SCOPE,
ScopeFilter: scopesv1.Filter_builder{
Mode: scopesv1.Mode_MODE_DESCENDANTS,
Scope: "/test",
}.Build(),
Roles: []string{types.RoleAuth.String()},
@@ -356,8 +356,8 @@ func TestScopedTokenList(t *testing.T) {
{
name: "tokens in /test scope filtered by label",
req: joiningv1.ListScopedTokensRequest_builder{
ResourceScope: scopesv1.Filter_builder{
Mode: scopesv1.Mode_MODE_RESOURCES_SUBJECT_TO_SCOPE,
ScopeFilter: scopesv1.Filter_builder{
Mode: scopesv1.Mode_MODE_DESCENDANTS,
Scope: "/test",
}.Build(),
Roles: []string{types.RoleNode.String()},
@@ -305,6 +305,36 @@ func (c *ScopedAccessCheckerContext) riskyEnumerateScopedCheckers(ctx context.Co
return c.enumerateAll(ctx)
}
// ResolveScopeFilter returns the scope filter that should be used for a list/read/watch request, applying
// identity-derived defaulting when the caller did not specify an explicit filter. An empty (nil or
// MODE_UNSPECIFIED) filter is replaced with the safe default for this identity: MODE_EXACT at the pinned
// scope for scoped identities, or MODE_UNSCOPED for unscoped identities. A filter with an explicit mode is
// returned unchanged.
//
// This is the single source of truth for scope-filter defaulting. API handlers should pass caller-provided
// filters through this method so that an omitted filter is consistently and safely defaulted, minimizing
// per-API defaulting logic. Note that this method only handles defaulting; callers are still responsible for
// validating (see [scopes.ValidateFilter]) and authorizing the resulting filter.
func (c *ScopedAccessCheckerContext) ResolveScopeFilter(filter *scopesv1.Filter) *scopesv1.Filter {
if filter.GetMode() != scopesv1.Mode_MODE_UNSPECIFIED {
// the caller specified an explicit filter; return it unchanged.
return filter
}
if !c.isScoped() {
// unscoped callers default to matching unscoped resources only.
return scopesv1.Filter_builder{
Mode: scopesv1.Mode_MODE_UNSCOPED,
}.Build()
}
// scoped callers default to the minimal/safe MODE_EXACT at their pinned scope.
return scopesv1.Filter_builder{
Scope: c.pin.GetScope(),
Mode: scopesv1.Mode_MODE_EXACT,
}.Build()
}
// CheckMaybeHasAccessToRules returns an error if the context definitely does not have access to the provided
// rules. For scoped identities, always returns nil — the scoped access model evaluates permissions per-resource.
func (c *ScopedAccessCheckerContext) CheckMaybeHasAccessToRules(ctx RuleContext, resource string, verbs ...string) error {
+5 -1
View File
@@ -23,6 +23,7 @@ import (
"github.com/gravitational/trace"
scopedaccessv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/access/v1"
scopesv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/v1"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/asciitable"
"github.com/gravitational/teleport/lib/auth/authclient"
@@ -150,7 +151,10 @@ func getScopedRole(ctx context.Context, client *authclient.Client, subKind strin
return &scopedRoleCollection{roles: []*scopedaccessv1.ScopedRole{role}}, nil
}
items, err := stream.Collect(scopedutils.RangeScopedRoles(ctx, client.ScopedAccessServiceClient(), &scopedaccessv1.ListScopedRolesRequest{}))
items, err := stream.Collect(scopedutils.RangeScopedRoles(ctx, client.ScopedAccessServiceClient(), scopedaccessv1.ListScopedRolesRequest_builder{
// exhaustive user-facing views use MODE_ALL per RFD 0229i
ScopeFilter: scopesv1.Filter_builder{Mode: scopesv1.Mode_MODE_ALL}.Build(),
}.Build()))
if err != nil {
return nil, trace.Wrap(err)
}
@@ -25,6 +25,7 @@ import (
"github.com/gravitational/trace"
scopedaccessv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/access/v1"
scopesv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/v1"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/asciitable"
"github.com/gravitational/teleport/lib/auth/authclient"
@@ -171,7 +172,10 @@ func getScopedRoleAssignment(ctx context.Context, client *authclient.Client, sub
return &ScopedRoleAssignmentCollection{roleAssignments: []*scopedaccessv1.ScopedRoleAssignment{assignment}}, nil
}
items, err := stream.Collect(scopedutils.RangeScopedRoleAssignments(ctx, client.ScopedAccessServiceClient(), &scopedaccessv1.ListScopedRoleAssignmentsRequest{}))
items, err := stream.Collect(scopedutils.RangeScopedRoleAssignments(ctx, client.ScopedAccessServiceClient(), scopedaccessv1.ListScopedRoleAssignmentsRequest_builder{
// exhaustive user-facing views use MODE_ALL per RFD 0229i
ScopeFilter: scopesv1.Filter_builder{Mode: scopesv1.Mode_MODE_ALL}.Build(),
}.Build()))
if err != nil {
return nil, trace.Wrap(err)
}
@@ -26,6 +26,7 @@ import (
"github.com/gravitational/trace"
joiningv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/joining/v1"
scopesv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/v1"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/utils/clientutils"
"github.com/gravitational/teleport/lib/asciitable"
@@ -139,6 +140,8 @@ func getScopedToken(ctx context.Context, client *authclient.Client, subKind stri
Limit: uint32(pageSize),
Cursor: pageKey,
WithSecrets: opts.WithSecrets,
// exhaustive user-facing views use MODE_ALL per RFD 0229i
ScopeFilter: scopesv1.Filter_builder{Mode: scopesv1.Mode_MODE_ALL}.Build(),
}.Build())
if err != nil {
return nil, "", trace.Wrap(err)
@@ -25,6 +25,7 @@ import (
"github.com/gravitational/teleport"
scopedaccessv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/access/v1"
scopesv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/v1"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/itertools/stream"
scopedutils "github.com/gravitational/teleport/lib/scopes/utils"
@@ -104,6 +105,8 @@ func (c *scopedAssignmentsListCommand) list(ctx context.Context, client services
items, err := stream.Collect(scopedutils.RangeScopedRoleAssignments(ctx, client, scopedaccessv1.ListScopedRoleAssignmentsRequest_builder{
User: c.user,
Role: c.role,
// exhaustive user-facing views use MODE_ALL per RFD 0229i
ScopeFilter: scopesv1.Filter_builder{Mode: scopesv1.Mode_MODE_ALL}.Build(),
}.Build()))
if err != nil {
return trace.Wrap(err, "collecting filtered scoped role assignments")
@@ -28,6 +28,7 @@ import (
headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1"
scopedaccessv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/access/v1"
scopesv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/v1"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/integration/helpers"
"github.com/gravitational/teleport/lib/config"
@@ -152,7 +153,10 @@ func TestScopedAssignmentListCommand(t *testing.T) {
ctx := t.Context()
require.EventuallyWithT(t, func(t *assert.CollectT) {
resp, err := scopedClt.ListScopedRoleAssignments(ctx, &scopedaccessv1.ListScopedRoleAssignmentsRequest{})
resp, err := scopedClt.ListScopedRoleAssignments(ctx, scopedaccessv1.ListScopedRoleAssignmentsRequest_builder{
// exhaustive view: opt out of identity-based filter defaulting.
ScopeFilter: scopesv1.Filter_builder{Mode: scopesv1.Mode_MODE_ALL}.Build(),
}.Build())
require.NoError(t, err)
require.Len(t, resp.GetAssignments(), len(assignments))
}, 10*time.Second, 50*time.Millisecond, "waiting for scoped role assignments to be present in cache")
+9 -2
View File
@@ -30,6 +30,7 @@ import (
"github.com/gravitational/trace"
scopedaccessv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/access/v1"
scopesv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/v1"
"github.com/gravitational/teleport/lib/asciitable"
"github.com/gravitational/teleport/lib/auth/authclient"
"github.com/gravitational/teleport/lib/scopes"
@@ -118,7 +119,10 @@ func (c *scopedStatusCommand) status(ctx context.Context, client *authclient.Cli
// aggregate per scope counts
rows := make(map[string]scopeMetricRow)
for role, err := range scopedutils.RangeScopedRoles(ctx, client.ScopedAccessServiceClient(), &scopedaccessv1.ListScopedRolesRequest{}) {
for role, err := range scopedutils.RangeScopedRoles(ctx, client.ScopedAccessServiceClient(), scopedaccessv1.ListScopedRolesRequest_builder{
// exhaustive user-facing views use MODE_ALL per RFD 0229i
ScopeFilter: scopesv1.Filter_builder{Mode: scopesv1.Mode_MODE_ALL}.Build(),
}.Build()) {
if err != nil {
return trace.Wrap(err)
}
@@ -130,7 +134,10 @@ func (c *scopedStatusCommand) status(ctx context.Context, client *authclient.Cli
rows[scope] = row
}
for assignment, err := range scopedutils.RangeScopedRoleAssignments(ctx, client.ScopedAccessServiceClient(), &scopedaccessv1.ListScopedRoleAssignmentsRequest{}) {
for assignment, err := range scopedutils.RangeScopedRoleAssignments(ctx, client.ScopedAccessServiceClient(), scopedaccessv1.ListScopedRoleAssignmentsRequest_builder{
// exhaustive user-facing views use MODE_ALL per RFD 0229i
ScopeFilter: scopesv1.Filter_builder{Mode: scopesv1.Mode_MODE_ALL}.Build(),
}.Build()) {
if err != nil {
return trace.Wrap(err)
}
+3
View File
@@ -36,6 +36,7 @@ import (
"github.com/gravitational/teleport"
headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1"
joiningv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/joining/v1"
scopesv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/v1"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/utils/clientutils"
"github.com/gravitational/teleport/lib/auth/authclient"
@@ -286,6 +287,8 @@ func (c *ScopedTokensCommand) List(ctx context.Context, client *authclient.Clien
Limit: uint32(pageSize),
Cursor: pageKey,
WithSecrets: c.withSecrets,
// exhaustive user-facing views use MODE_ALL per RFD 0229i
ScopeFilter: scopesv1.Filter_builder{Mode: scopesv1.Mode_MODE_ALL}.Build(),
}.Build())
if err != nil {
return nil, "", trace.Wrap(err)
+8 -5
View File
@@ -28,6 +28,7 @@ import (
"github.com/gravitational/trace"
scopedaccessv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/access/v1"
scopesv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/v1"
"github.com/gravitational/teleport/lib/asciitable"
"github.com/gravitational/teleport/lib/auth/authclient"
"github.com/gravitational/teleport/lib/itertools/stream"
@@ -84,12 +85,14 @@ func (c *scopesLSCommand) run(cf *CLIConf) error {
if err := tc.WithRootClusterClient(ctx, func(clt authclient.ClientI) error {
assignments, err = stream.Collect(scopedutils.RangeScopedRoleAssignments(ctx, clt.ScopedAccessServiceClient(), scopedaccessv1.ListScopedRoleAssignmentsRequest_builder{
// note that we are using the AllCallerAssignments flag here rather than just looking for
// our assignments by username. This flag suppresses standard scope-pinning, which allows
// us to see assignments in parent/orthogonal scopes. Generally, scoped commands only show
// the subset of state subject to the currently pinned scope, but the purpose of 'tsh scopes ls'
// is specifically to discover potential 'tsh login --scope=...' targets, so we want to see
// everything regardless of current scope.
// our assignments by username. This flag bypasses standard access checks in favor of allowing
// all assignments belonging to the callert to be readable. Combined with filter mode ALL, this
// allows us to always see all assignments for our identity, regardless of our particular permissions
// or pinning state.
AllCallerAssignments: true,
ScopeFilter: scopesv1.Filter_builder{
Mode: scopesv1.Mode_MODE_ALL,
}.Build(),
}.Build()))
return trace.Wrap(err)
}); err != nil {