diff --git a/docs/pages/reference/infrastructure-as-code/operator-resources/resources-teleport-dev-scopedroleassignmentsv1.mdx b/docs/pages/reference/infrastructure-as-code/operator-resources/resources-teleport-dev-scopedroleassignmentsv1.mdx new file mode 100644 index 00000000000..38dcdbb7d29 --- /dev/null +++ b/docs/pages/reference/infrastructure-as-code/operator-resources/resources-teleport-dev-scopedroleassignmentsv1.mdx @@ -0,0 +1,44 @@ +--- +title: TeleportScopedRoleAssignmentV1 +description: Provides a comprehensive list of fields in the TeleportScopedRoleAssignmentV1 resource available through the Teleport Kubernetes operator +--- + +{/*Auto-generated file. Do not edit.*/} +{/*To regenerate, navigate to integrations/operator and run "make crd-docs".*/} + +{/* Disable the outdated name check since custom resource fields occasionally +need to refer to these. */} +{/* vale 3rd-party-products.former-names = NO */} + +This guide is a comprehensive reference to the fields in the `TeleportScopedRoleAssignmentV1` +resource, which you can apply after installing the Teleport Kubernetes operator. + + +## resources.teleport.dev/v1 + +**apiVersion:** resources.teleport.dev/v1 + +|Field|Type|Description| +|---|---|---| +|apiVersion|string|APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources| +|kind|string|Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds| +|metadata|object|| +|scope|string|Scope is the scope of the role assignment resource.| +|spec|[object](#spec)|ScopedRoleAssignment resource definition v1 from Teleport| + +### spec + +|Field|Type|Description| +|---|---|---| +|assignments|[][object](#specassignments-items)|Assignments is a list of individual role @ scope assignments.| +|bot_name|string|Name of the Bot to whom all contained assignments apply. Mutually exclusive with `user`.| +|bot_scope|string|Scope of the Bot to whom all contained assignments apply. Required if `bot_name` is set. If specified, assignment scopes must be equal or descendent of this scope.| +|user|string|User is the user to whom all contained assignments apply. Mutually exclusive with `bot_name`.| + +### spec.assignments items + +|Field|Type|Description| +|---|---|---| +|role|string|Roles is the name of the role that is assigned by this assignment.| +|scope|string|Scope is the scope to which the role is assigned. This must be a member/child of the scope of the [ScopedRoleAssignment] in which this assignment is contained.| + diff --git a/examples/chart/teleport-cluster/charts/teleport-operator/operator-crds/resources.teleport.dev_scopedroleassignmentsv1.yaml b/examples/chart/teleport-cluster/charts/teleport-operator/operator-crds/resources.teleport.dev_scopedroleassignmentsv1.yaml new file mode 100644 index 00000000000..c6fb072893c --- /dev/null +++ b/examples/chart/teleport-cluster/charts/teleport-operator/operator-crds/resources.teleport.dev_scopedroleassignmentsv1.yaml @@ -0,0 +1,146 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: teleportscopedroleassignmentsv1.resources.teleport.dev +spec: + group: resources.teleport.dev + names: + kind: TeleportScopedRoleAssignmentV1 + listKind: TeleportScopedRoleAssignmentV1List + plural: teleportscopedroleassignmentsv1 + shortNames: + - scopedroleassignmentv1 + - scopedroleassignmentsv1 + singular: teleportscopedroleassignmentv1 + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + description: ScopedRoleAssignmentV1 is the Schema for the scopedroleassignmentsv1 + API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + scope: + description: Scope is the scope of the role assignment resource. + type: string + spec: + description: ScopedRoleAssignment resource definition v1 from Teleport + properties: + assignments: + description: Assignments is a list of individual role @ scope assignments. + items: + properties: + role: + description: Roles is the name of the role that is assigned + by this assignment. + type: string + scope: + description: Scope is the scope to which the role is assigned. + This must be a member/child of the scope of the [ScopedRoleAssignment] + in which this assignment is contained. + type: string + type: object + nullable: true + type: array + bot_name: + description: Name of the Bot to whom all contained assignments apply. + Mutually exclusive with `user`. + type: string + bot_scope: + description: Scope of the Bot to whom all contained assignments apply. + Required if `bot_name` is set. If specified, assignment scopes must + be equal or descendent of this scope. + type: string + user: + description: User is the user to whom all contained assignments apply. + Mutually exclusive with `bot_name`. + type: string + type: object + status: + description: Status defines the observed state of the Teleport resource + properties: + conditions: + description: Conditions represent the latest available observations + of an object's state + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + teleportResourceID: + format: int64 + type: integer + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: null + storedVersions: null diff --git a/examples/chart/teleport-cluster/charts/teleport-operator/templates/role.yaml b/examples/chart/teleport-cluster/charts/teleport-operator/templates/role.yaml index fb5bc7bbb29..08e250dfda7 100644 --- a/examples/chart/teleport-cluster/charts/teleport-operator/templates/role.yaml +++ b/examples/chart/teleport-cluster/charts/teleport-operator/templates/role.yaml @@ -68,6 +68,8 @@ rules: - teleportscopedtokensv1/status - teleportscopedrolesv1 - teleportscopedrolesv1/status + - teleportscopedroleassignmentsv1 + - teleportscopedroleassignmentsv1/status verbs: - get - list diff --git a/examples/chart/teleport-cluster/templates/auth/config.yaml b/examples/chart/teleport-cluster/templates/auth/config.yaml index bedd59ecc12..2ca7bf928aa 100644 --- a/examples/chart/teleport-cluster/templates/auth/config.yaml +++ b/examples/chart/teleport-cluster/templates/auth/config.yaml @@ -260,6 +260,14 @@ data: - readnosecrets - update - delete + - resources: + - scoped_role_assignment + verbs: + - list + - create + - read + - update + - delete deny: {} version: v7 --- diff --git a/integrations/operator/Makefile b/integrations/operator/Makefile index 57dceb14841..2725ceeb944 100644 --- a/integrations/operator/Makefile +++ b/integrations/operator/Makefile @@ -96,7 +96,8 @@ PROTOS = \ teleport/summarizer/v1/summarizer.proto \ teleport/accessmonitoringrules/v1/access_monitoring_rules.proto \ teleport/scopes/joining/v1/token.proto \ - teleport/scopes/access/v1/role.proto + teleport/scopes/access/v1/role.proto \ + teleport/scopes/access/v1/assignment.proto .PHONY: crd-manifests crd-manifests: crdgen ## Generates CRD manifests using crdgen. diff --git a/integrations/operator/apis/resources/v1/scopedroleassignment_types.go b/integrations/operator/apis/resources/v1/scopedroleassignment_types.go new file mode 100644 index 00000000000..9cd3786c2a8 --- /dev/null +++ b/integrations/operator/apis/resources/v1/scopedroleassignment_types.go @@ -0,0 +1,107 @@ +// 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 . +package v1 + +import ( + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1" + accessv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/access/v1" + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/integrations/operator/apis/resources/teleportcr" + "github.com/gravitational/teleport/lib/scopes/access" +) + +func init() { + SchemeBuilder.Register(&TeleportScopedRoleAssignmentV1{}) +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// TeleportScopedRoleAssignmentV1 represents a Kubernetes custom resource for +// Scoped Role Assignments. +type TeleportScopedRoleAssignmentV1 struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata"` + + Scope string `json:"scope"` + Spec *TeleportScopedRoleAssignmentV1Spec `json:"spec,omitempty"` + Status teleportcr.Status `json:"status"` +} + +// TeleportScopedRoleAssignmentV1Spec defines the desired state of the Scoped Role Assignment. +type TeleportScopedRoleAssignmentV1Spec accessv1.ScopedRoleAssignmentSpec + +//+kubebuilder:object:root = true + +// TeleportScopedRoleAssignmentV1List contains a list of [TeleportScopedRoleAssignmentV1] +// objects. +type TeleportScopedRoleAssignmentV1List struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata"` + Items []TeleportScopedRoleAssignmentV1 `json:"items"` +} + +// ToTeleport returns a Teleport representation of this Kubernetes resource. +func (m *TeleportScopedRoleAssignmentV1) ToTeleport() *accessv1.ScopedRoleAssignment { + resource := &accessv1.ScopedRoleAssignment{ + Kind: access.KindScopedRoleAssignment, + SubKind: access.SubKindDynamic, + Version: types.V1, + Metadata: &headerv1.Metadata{ + Name: m.Name, + Description: m.Annotations[teleportcr.DescriptionKey], + Labels: m.Labels, + }, + Scope: m.Scope, + Spec: (*accessv1.ScopedRoleAssignmentSpec)(m.Spec), + } + return resource +} + +// StatusConditions returns a pointer to Status.Conditions slice. This is used +// by the Teleport resource controller to report conditions back to resource. +func (m *TeleportScopedRoleAssignmentV1) StatusConditions() *[]metav1.Condition { + return &m.Status.Conditions +} + +// UnmarshalJSON delegates unmarshaling of the TeleportScopedRoleAssignmentV1Spec to +// protojson, which is necessary for Proto RFD153 resources to be unmarshaled +// correctly from the unstructured object. +func (spec *TeleportScopedRoleAssignmentV1Spec) UnmarshalJSON(data []byte) error { + return protojson.UnmarshalOptions{ + DiscardUnknown: true, + }.Unmarshal(data, (*accessv1.ScopedRoleAssignmentSpec)(spec)) +} + +// MarshalJSON delegates marshaling of the TeleportScopedRoleAssignmentV1Spec to +// protojson, which is necessary for Proto RFD153 resources to be marshaled +// correctly into an unstructured object. +func (spec *TeleportScopedRoleAssignmentV1Spec) MarshalJSON() ([]byte, error) { + return protojson.MarshalOptions{ + UseProtoNames: true, + }.Marshal((*accessv1.ScopedRoleAssignmentSpec)(spec)) +} + +// DeepCopyInto deep-copies one TeleportScopedRoleAssignmentV1Spec into another. +// Required to satisfy runtime.Object interface. +func (spec *TeleportScopedRoleAssignmentV1Spec) DeepCopyInto(out *TeleportScopedRoleAssignmentV1Spec) { + proto.Reset((*accessv1.ScopedRoleAssignmentSpec)(out)) + proto.Merge((*accessv1.ScopedRoleAssignmentSpec)(out), (*accessv1.ScopedRoleAssignmentSpec)(spec)) +} diff --git a/integrations/operator/apis/resources/v1/zz_generated.deepcopy.go b/integrations/operator/apis/resources/v1/zz_generated.deepcopy.go index 86590709e60..2e6e447fcbc 100644 --- a/integrations/operator/apis/resources/v1/zz_generated.deepcopy.go +++ b/integrations/operator/apis/resources/v1/zz_generated.deepcopy.go @@ -1431,6 +1431,70 @@ func (in *TeleportSAMLIdPServiceProviderV1Spec) DeepCopy() *TeleportSAMLIdPServi return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TeleportScopedRoleAssignmentV1) DeepCopyInto(out *TeleportScopedRoleAssignmentV1) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + if in.Spec != nil { + in, out := &in.Spec, &out.Spec + *out = (*in).DeepCopy() + } + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TeleportScopedRoleAssignmentV1. +func (in *TeleportScopedRoleAssignmentV1) DeepCopy() *TeleportScopedRoleAssignmentV1 { + if in == nil { + return nil + } + out := new(TeleportScopedRoleAssignmentV1) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *TeleportScopedRoleAssignmentV1) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TeleportScopedRoleAssignmentV1List) DeepCopyInto(out *TeleportScopedRoleAssignmentV1List) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]TeleportScopedRoleAssignmentV1, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TeleportScopedRoleAssignmentV1List. +func (in *TeleportScopedRoleAssignmentV1List) DeepCopy() *TeleportScopedRoleAssignmentV1List { + if in == nil { + return nil + } + out := new(TeleportScopedRoleAssignmentV1List) + in.DeepCopyInto(out) + return out +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TeleportScopedRoleAssignmentV1Spec. +func (in *TeleportScopedRoleAssignmentV1Spec) DeepCopy() *TeleportScopedRoleAssignmentV1Spec { + if in == nil { + return nil + } + out := new(TeleportScopedRoleAssignmentV1Spec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TeleportScopedRoleV1) DeepCopyInto(out *TeleportScopedRoleV1) { *out = *in diff --git a/integrations/operator/config/crd/bases/resources.teleport.dev_scopedroleassignmentsv1.yaml b/integrations/operator/config/crd/bases/resources.teleport.dev_scopedroleassignmentsv1.yaml new file mode 100644 index 00000000000..c6fb072893c --- /dev/null +++ b/integrations/operator/config/crd/bases/resources.teleport.dev_scopedroleassignmentsv1.yaml @@ -0,0 +1,146 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: teleportscopedroleassignmentsv1.resources.teleport.dev +spec: + group: resources.teleport.dev + names: + kind: TeleportScopedRoleAssignmentV1 + listKind: TeleportScopedRoleAssignmentV1List + plural: teleportscopedroleassignmentsv1 + shortNames: + - scopedroleassignmentv1 + - scopedroleassignmentsv1 + singular: teleportscopedroleassignmentv1 + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + description: ScopedRoleAssignmentV1 is the Schema for the scopedroleassignmentsv1 + API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + scope: + description: Scope is the scope of the role assignment resource. + type: string + spec: + description: ScopedRoleAssignment resource definition v1 from Teleport + properties: + assignments: + description: Assignments is a list of individual role @ scope assignments. + items: + properties: + role: + description: Roles is the name of the role that is assigned + by this assignment. + type: string + scope: + description: Scope is the scope to which the role is assigned. + This must be a member/child of the scope of the [ScopedRoleAssignment] + in which this assignment is contained. + type: string + type: object + nullable: true + type: array + bot_name: + description: Name of the Bot to whom all contained assignments apply. + Mutually exclusive with `user`. + type: string + bot_scope: + description: Scope of the Bot to whom all contained assignments apply. + Required if `bot_name` is set. If specified, assignment scopes must + be equal or descendent of this scope. + type: string + user: + description: User is the user to whom all contained assignments apply. + Mutually exclusive with `bot_name`. + type: string + type: object + status: + description: Status defines the observed state of the Teleport resource + properties: + conditions: + description: Conditions represent the latest available observations + of an object's state + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + teleportResourceID: + format: int64 + type: integer + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: null + storedVersions: null diff --git a/integrations/operator/controllers/resources/scopedroleassignment_controller.go b/integrations/operator/controllers/resources/scopedroleassignment_controller.go new file mode 100644 index 00000000000..f1630bf9b17 --- /dev/null +++ b/integrations/operator/controllers/resources/scopedroleassignment_controller.go @@ -0,0 +1,80 @@ +// 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 . + +package resources + +import ( + "context" + + "github.com/gravitational/trace" + kclient "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/gravitational/teleport/api/client" + accessv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/access/v1" + resourcesv1 "github.com/gravitational/teleport/integrations/operator/apis/resources/v1" + "github.com/gravitational/teleport/integrations/operator/controllers" + "github.com/gravitational/teleport/integrations/operator/controllers/reconcilers" + scopedaccess "github.com/gravitational/teleport/lib/scopes/access" +) + +type scopedRoleAssignmentClient struct { + teleportClient *client.Client +} + +func (s *scopedRoleAssignmentClient) Create(ctx context.Context, assignment *accessv1.ScopedRoleAssignment) error { + _, err := s.teleportClient.ScopedAccessServiceClient().CreateScopedRoleAssignment(ctx, &accessv1.CreateScopedRoleAssignmentRequest{ + Assignment: assignment, + }) + return trace.Wrap(err) +} + +func (s *scopedRoleAssignmentClient) Delete(ctx context.Context, name string) error { + _, err := s.teleportClient.ScopedAccessServiceClient().DeleteScopedRoleAssignment(ctx, &accessv1.DeleteScopedRoleAssignmentRequest{ + Name: name, + SubKind: scopedaccess.SubKindDynamic, + }) + if err != nil && trace.IsCompareFailed(err) { + return trace.NotFound("scoped role assignment %q not found: %v", name, err) + } + return trace.Wrap(err) +} + +func (s *scopedRoleAssignmentClient) Get(ctx context.Context, name string) (*accessv1.ScopedRoleAssignment, error) { + resp, err := s.teleportClient.ScopedAccessServiceClient().GetScopedRoleAssignment(ctx, &accessv1.GetScopedRoleAssignmentRequest{ + Name: name, + SubKind: scopedaccess.SubKindDynamic, + }) + if err != nil { + return nil, trace.Wrap(err) + } + return resp.GetAssignment(), nil +} + +func (s *scopedRoleAssignmentClient) Update(ctx context.Context, assignment *accessv1.ScopedRoleAssignment) error { + _, err := s.teleportClient.ScopedAccessServiceClient().UpdateScopedRoleAssignment(ctx, &accessv1.UpdateScopedRoleAssignmentRequest{ + Assignment: assignment, + }) + return trace.Wrap(err) +} + +func NewScopedRoleAssignmentV1Reconciler(client kclient.Client, tClient *client.Client) (controllers.Reconciler, error) { + return reconcilers.NewTeleportResource153Reconciler[*accessv1.ScopedRoleAssignment, *resourcesv1.TeleportScopedRoleAssignmentV1]( + client, + &scopedRoleAssignmentClient{ + teleportClient: tClient, + }, + ) +} diff --git a/integrations/operator/controllers/resources/scopedroleassignment_controller_test.go b/integrations/operator/controllers/resources/scopedroleassignment_controller_test.go new file mode 100644 index 00000000000..8c9119ce4bd --- /dev/null +++ b/integrations/operator/controllers/resources/scopedroleassignment_controller_test.go @@ -0,0 +1,175 @@ +/* + * 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 . + */ + +package resources_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/uuid" + "github.com/gravitational/trace" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + kclient "sigs.k8s.io/controller-runtime/pkg/client" + + headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1" + accessv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/access/v1" + "github.com/gravitational/teleport/api/types" + resourcesv1 "github.com/gravitational/teleport/integrations/operator/apis/resources/v1" + "github.com/gravitational/teleport/integrations/operator/controllers/reconcilers" + "github.com/gravitational/teleport/integrations/operator/controllers/resources" + "github.com/gravitational/teleport/integrations/operator/controllers/resources/testlib" + "github.com/gravitational/teleport/lib/scopes/access" +) + +var scopedRoleAssignmentSpec = &accessv1.ScopedRoleAssignmentSpec{ + User: "test-user", + Assignments: []*accessv1.Assignment{ + { + Role: "test-role", + Scope: "/staging", + }, + }, +} + +type scopedRoleAssignmentTestingPrimitives struct { + setup *testSetup + reconcilers.Resource153Adapter[*accessv1.ScopedRoleAssignment] +} + +func (g *scopedRoleAssignmentTestingPrimitives) Init(setup *testSetup) { + g.setup = setup +} + +func (g *scopedRoleAssignmentTestingPrimitives) SetupTeleportFixtures(ctx context.Context) error { + return nil +} + +func (g *scopedRoleAssignmentTestingPrimitives) CreateTeleportResource(ctx context.Context, name string) error { + assignment := &accessv1.ScopedRoleAssignment{ + Kind: access.KindScopedRoleAssignment, + Version: types.V1, + SubKind: access.SubKindDynamic, + Metadata: &headerv1.Metadata{ + Name: name, + Labels: map[string]string{ + types.OriginLabel: types.OriginKubernetes, + }, + }, + Scope: "/staging", + Spec: scopedRoleAssignmentSpec, + } + _, err := g.setup.TeleportClient.ScopedAccessServiceClient().CreateScopedRoleAssignment(ctx, &accessv1.CreateScopedRoleAssignmentRequest{ + Assignment: assignment, + }) + return trace.Wrap(err) +} + +func (g *scopedRoleAssignmentTestingPrimitives) GetTeleportResource(ctx context.Context, name string) (*accessv1.ScopedRoleAssignment, error) { + resp, err := g.setup.TeleportClient.ScopedAccessServiceClient().GetScopedRoleAssignment(ctx, &accessv1.GetScopedRoleAssignmentRequest{ + Name: name, + SubKind: access.SubKindDynamic, + }) + if err != nil { + return nil, trace.Wrap(err) + } + return resp.GetAssignment(), nil +} + +func (g *scopedRoleAssignmentTestingPrimitives) DeleteTeleportResource(ctx context.Context, name string) error { + _, err := g.setup.TeleportClient.ScopedAccessServiceClient().DeleteScopedRoleAssignment(ctx, &accessv1.DeleteScopedRoleAssignmentRequest{ + Name: name, + SubKind: access.SubKindDynamic, + }) + return trace.Wrap(err) +} + +func (g *scopedRoleAssignmentTestingPrimitives) CreateKubernetesResource(ctx context.Context, name string) error { + assignment := &resourcesv1.TeleportScopedRoleAssignmentV1{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: g.setup.Namespace.Name, + }, + Scope: "/staging", + Spec: (*resourcesv1.TeleportScopedRoleAssignmentV1Spec)(scopedRoleAssignmentSpec), + } + return trace.Wrap(g.setup.K8sClient.Create(ctx, assignment)) +} + +func (g *scopedRoleAssignmentTestingPrimitives) DeleteKubernetesResource(ctx context.Context, name string) error { + assignment := &resourcesv1.TeleportScopedRoleAssignmentV1{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: g.setup.Namespace.Name, + }, + } + return trace.Wrap(g.setup.K8sClient.Delete(ctx, assignment)) +} + +func (g *scopedRoleAssignmentTestingPrimitives) GetKubernetesResource(ctx context.Context, name string) (*resourcesv1.TeleportScopedRoleAssignmentV1, error) { + assignment := &resourcesv1.TeleportScopedRoleAssignmentV1{} + obj := kclient.ObjectKey{ + Name: name, + Namespace: g.setup.Namespace.Name, + } + err := g.setup.K8sClient.Get(ctx, obj, assignment) + return assignment, trace.Wrap(err) +} + +func (g *scopedRoleAssignmentTestingPrimitives) ModifyKubernetesResource(ctx context.Context, name string) error { + assignment, err := g.GetKubernetesResource(ctx, name) + if err != nil { + return trace.Wrap(err) + } + assignment.Spec.Assignments = []*accessv1.Assignment{ + { + Role: "test-role", + Scope: "/staging/aa", + }, + } + return trace.Wrap(g.setup.K8sClient.Update(ctx, assignment)) +} + +func (g *scopedRoleAssignmentTestingPrimitives) CompareTeleportAndKubernetesResource( + tResource *accessv1.ScopedRoleAssignment, kubeResource *resourcesv1.TeleportScopedRoleAssignmentV1) (bool, string) { + diff := cmp.Diff( + tResource, + kubeResource.ToTeleport(), + testlib.ProtoCompareOptions()..., + ) + return diff == "", diff +} + +func TestScopedRoleAssignmentCreation(t *testing.T) { + t.Setenv("TELEPORT_UNSTABLE_SCOPES", "yes") + test := &scopedRoleAssignmentTestingPrimitives{} + testlib.ResourceCreationSynchronousTest(t, resources.NewScopedRoleAssignmentV1Reconciler, test, testlib.WithResourceName(uuid.New().String())) +} + +func TestScopedRoleAssignmentDeletionDrift(t *testing.T) { + t.Setenv("TELEPORT_UNSTABLE_SCOPES", "yes") + test := &scopedRoleAssignmentTestingPrimitives{} + testlib.ResourceDeletionDriftSynchronousTest(t, resources.NewScopedRoleAssignmentV1Reconciler, test, testlib.WithResourceName(uuid.New().String())) +} + +func TestScopedRoleAssignmentUpdate(t *testing.T) { + t.Setenv("TELEPORT_UNSTABLE_SCOPES", "yes") + test := &scopedRoleAssignmentTestingPrimitives{} + testlib.ResourceUpdateTestSynchronous(t, resources.NewScopedRoleAssignmentV1Reconciler, test, testlib.WithResourceName(uuid.New().String())) +} diff --git a/integrations/operator/controllers/resources/setup.go b/integrations/operator/controllers/resources/setup.go index 43cb80b0688..8e3a2ae0e0b 100644 --- a/integrations/operator/controllers/resources/setup.go +++ b/integrations/operator/controllers/resources/setup.go @@ -67,6 +67,7 @@ func SetupAllControllers(log logr.Logger, mgr manager.Manager, teleportClient *c {"TeleportSAMLIdPServiceProviderV1", NewSAMLIdPServiceProviderV1Reconciler}, {"TeleportScopedTokenV1", NewScopedTokenV1Reconciler}, {"TeleportScopedRoleV1", NewScopedRoleV1Reconciler}, + {"TeleportScopedRoleAssignmentV1", NewScopedRoleAssignmentV1Reconciler}, } oidc := modules.GetProtoEntitlement(features, entitlements.OIDC) diff --git a/integrations/operator/controllers/resources/testlib/env.go b/integrations/operator/controllers/resources/testlib/env.go index 4e94f0c9bc4..118f1aaead8 100644 --- a/integrations/operator/controllers/resources/testlib/env.go +++ b/integrations/operator/controllers/resources/testlib/env.go @@ -142,6 +142,7 @@ func defaultTeleportServiceConfig(t *testing.T, insecureMode bool) (*helpers.Tel types.NewRule(types.KindSAMLIdPServiceProvider, unrestricted), types.NewRule(types.KindScopedToken, unrestricted), types.NewRule(access.KindScopedRole, unrestricted), + types.NewRule(access.KindScopedRoleAssignment, unrestricted), }, }, }) diff --git a/integrations/operator/crdgen/handlerequest.go b/integrations/operator/crdgen/handlerequest.go index 14dc7fed2c7..f3e06af922a 100644 --- a/integrations/operator/crdgen/handlerequest.go +++ b/integrations/operator/crdgen/handlerequest.go @@ -281,6 +281,13 @@ func generateSchema(file *File, groupName string, format crdFormatFunc, resp *go withAdditionalRootFields([]string{"scope"}), }, }, + { + name: "ScopedRoleAssignment", + opts: []resourceSchemaOption{ + withVersionOverride(types.V1), + withAdditionalRootFields([]string{"scope"}), + }, + }, } for _, resource := range resources { diff --git a/integrations/operator/crdgen/test/fixtures/scoped_role_assignment.yaml b/integrations/operator/crdgen/test/fixtures/scoped_role_assignment.yaml new file mode 100644 index 00000000000..40308894ad8 --- /dev/null +++ b/integrations/operator/crdgen/test/fixtures/scoped_role_assignment.yaml @@ -0,0 +1,10 @@ +apiVersion: resources.teleport.dev/v1 +kind: TeleportScopedRoleAssignmentV1 +scope: /foo +sub_kind: dynamic +spec: + user: "test" + assignments: + - role: some-role + scope: /foo/bar +version: v1 \ No newline at end of file diff --git a/integrations/operator/hack/fixture-operator-role.yaml b/integrations/operator/hack/fixture-operator-role.yaml index 2f7058d0df0..0155330ddf9 100644 --- a/integrations/operator/hack/fixture-operator-role.yaml +++ b/integrations/operator/hack/fixture-operator-role.yaml @@ -197,6 +197,14 @@ spec: - readnosecrets - update - delete + - resources: + - scoped_role_assignment + verbs: + - list + - create + - read + - update + - delete deny: {} version: v7