add scoped role assignments to terraform (#65632)

fix template

regenerate docs

Update provider.go

address linter issue

lint

delete uuid requirement - fix tests

Update resource.tf

fix tests

fix test

adjust to use envelope types

fix

Update presets.go

Update assignment_terraform.go

make docs
This commit is contained in:
williamong-tel
2026-04-27 18:13:23 +00:00
committed by GitHub
parent 247ecf84a8
commit 1493bb13f6
18 changed files with 2116 additions and 0 deletions
@@ -40,6 +40,7 @@ The Teleport Terraform provider supports the following data-sources:
- [`teleport_saml_connector`](./saml_connector.mdx)
- [`teleport_saml_idp_service_provider`](./saml_idp_service_provider.mdx)
- [`teleport_scoped_role`](./scoped_role.mdx)
- [`teleport_scoped_role_assignment`](./scoped_role_assignment.mdx)
- [`teleport_scoped_token`](./scoped_token.mdx)
- [`teleport_session_recording_config`](./session_recording_config.mdx)
- [`teleport_static_host_user`](./static_host_user.mdx)
@@ -0,0 +1,63 @@
---
title: Reference for the teleport_scoped_role_assignment Terraform data-source
sidebar_label: scoped_role_assignment
description: This page describes the supported values of the teleport_scoped_role_assignment data-source of the Teleport Terraform provider.
---
{/*Auto-generated file. Do not edit.*/}
{/*To regenerate, navigate to integrations/terraform and run `make docs`.*/}
{/* Disable the outdated name check since data source fields occasionally need
to refer to these. */}
{/* vale 3rd-party-products.former-names = NO */}
This page describes the supported values of the `teleport_scoped_role_assignment` data source of the
Teleport Terraform provider.
{/* schema generated by tfplugindocs */}
## Schema
### Required
- `metadata` (Attributes) Metadata contains the resource metadata. (see [below for nested schema](#nested-schema-for-metadata))
- `scope` (String) Scope is the scope of the role assignment resource.
- `spec` (Attributes) Spec is the role assignment specification. (see [below for nested schema](#nested-schema-for-spec))
- `sub_kind` (String) SubKind is the resource sub-kind.
- `version` (String) Version is the resource version.
### Nested Schema for `metadata`
Required:
- `name` (String) name is an object name.
Optional:
- `description` (String) description is object description.
- `expires` (String) expires is a global expiry time header can be set on any resource in the system.
- `labels` (Map of String) labels is a set of labels.
### Nested Schema for `spec`
Required:
- `assignments` (Attributes List) Assignments is a list of individual role @ scope assignments. (see [below for nested schema](#nested-schema-for-specassignments))
Optional:
- `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`.
### Nested Schema for `spec.assignments`
Optional:
- `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.
@@ -45,6 +45,7 @@ The Teleport Terraform provider supports the following resources:
- [`teleport_saml_connector`](./saml_connector.mdx)
- [`teleport_saml_idp_service_provider`](./saml_idp_service_provider.mdx)
- [`teleport_scoped_role`](./scoped_role.mdx)
- [`teleport_scoped_role_assignment`](./scoped_role_assignment.mdx)
- [`teleport_scoped_token`](./scoped_token.mdx)
- [`teleport_server`](./server.mdx)
- [`teleport_session_recording_config`](./session_recording_config.mdx)
@@ -0,0 +1,105 @@
---
title: Reference for the teleport_scoped_role_assignment Terraform resource
sidebar_label: scoped_role_assignment
description: This page describes the supported values of the teleport_scoped_role_assignment resource of the Teleport Terraform provider.
---
{/*Auto-generated file. Do not edit.*/}
{/*To regenerate, navigate to integrations/terraform and run `make docs`.*/}
{/* Disable the outdated name check since resource fields occasionally need
to refer to these. */}
{/* vale 3rd-party-products.former-names = NO */}
This page describes the supported values of the teleport_scoped_role_assignment resource of the Teleport Terraform provider.
## Example Usage
```hcl
# Teleport Scoped Role Assignment resource
#
# Assigns an existing scoped role to a user at a specific scope.
# The referenced scoped role must already exist.
# resource "teleport_scoped_role" "example" {
# version = "v1"
# metadata = {
# name = "example-scoped-role"
# }
# scope = "/staging"
# spec = {
# assignable_scopes = ["/staging/aa"]
# rules = [{
# resources = ["scoped_token"]
# verbs = ["read", "list"]
# }]
# }
# }
resource "teleport_scoped_role_assignment" "example" {
version = "v1"
# sub_kind must be dynamic when creating scoped role assignments.
sub_kind = "dynamic"
metadata = {
name = "test-scoped-role-assignment"
}
scope = "/staging"
spec = {
user = "will"
assignments = [{
role = "example-scoped-role"
scope = "/staging/aa"
}]
}
}
```
{/* schema generated by tfplugindocs */}
## Schema
### Required
- `metadata` (Attributes) Metadata contains the resource metadata. (see [below for nested schema](#nested-schema-for-metadata))
- `scope` (String) Scope is the scope of the role assignment resource.
- `spec` (Attributes) Spec is the role assignment specification. (see [below for nested schema](#nested-schema-for-spec))
- `sub_kind` (String) SubKind is the resource sub-kind.
- `version` (String) Version is the resource version.
### Nested Schema for `metadata`
Required:
- `name` (String) name is an object name.
Optional:
- `description` (String) description is object description.
- `expires` (String) expires is a global expiry time header can be set on any resource in the system.
- `labels` (Map of String) labels is a set of labels.
### Nested Schema for `spec`
Required:
- `assignments` (Attributes List) Assignments is a list of individual role @ scope assignments. (see [below for nested schema](#nested-schema-for-specassignments))
Optional:
- `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`.
### Nested Schema for `spec.assignments`
Optional:
- `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.
+8
View File
@@ -192,6 +192,13 @@ endif
--terraform_out=config=protoc-gen-terraform-scopedrole.yaml:./tfschema \
teleport/scopes/access/v1/role.proto
@protoc \
-I=../../api/proto \
-I=$(PROTOBUF_MOD_PATH) \
--plugin=$(PROTOC_GEN_TERRAFORM) \
--terraform_out=config=protoc-gen-terraform-scopedroleassignment.yaml:./tfschema \
teleport/scopes/access/v1/assignment.proto
mv ./tfschema/github.com/gravitational/teleport/api/gen/proto/go/teleport/loginrule/v1/loginrule_terraform.go ./tfschema/loginrule/v1/
mv ./tfschema/github.com/gravitational/teleport/api/gen/proto/go/teleport/accesslist/v1/accesslist_terraform.go ./tfschema/accesslist/v1/
mv ./tfschema/github.com/gravitational/teleport/api/gen/proto/go/teleport/accessmonitoringrules/v1/access_monitoring_rules_terraform.go ./tfschema/accessmonitoringrules/v1/
@@ -206,6 +213,7 @@ endif
mv ./tfschema/github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/joining/v1/token_terraform.go ./tfschema/scopes/joining/v1/
mv ./tfschema/github.com/gravitational/teleport/api/gen/proto/go/teleport/workloadcluster/v1/workloadcluster_terraform.go ./tfschema/workloadcluster/v1/
mv ./tfschema/github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/access/v1/role_terraform.go ./tfschema/scopes/access/v1/
mv ./tfschema/github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/access/v1/assignment_terraform.go ./tfschema/scopes/access/assignment/v1/
mv ./tfschema/github.com/gravitational/teleport/api/types/device_terraform.go ./tfschema/devicetrust/v1/
rm -r ./tfschema/github.com/
@go run ./gen/main.go
@@ -0,0 +1,40 @@
# Teleport Scoped Role Assignment resource
#
# Assigns an existing scoped role to a user at a specific scope.
# The referenced scoped role must already exist.
# resource "teleport_scoped_role" "example" {
# version = "v1"
# metadata = {
# name = "example-scoped-role"
# }
# scope = "/staging"
# spec = {
# assignable_scopes = ["/staging/aa"]
# rules = [{
# resources = ["scoped_token"]
# verbs = ["read", "list"]
# }]
# }
# }
resource "teleport_scoped_role_assignment" "example" {
version = "v1"
# sub_kind must be dynamic when creating scoped role assignments.
sub_kind = "dynamic"
metadata = {
name = "test-scoped-role-assignment"
}
scope = "/staging"
spec = {
user = "will"
assignments = [{
role = "example-scoped-role"
scope = "/staging/aa"
}]
}
}
+33
View File
@@ -1068,6 +1068,37 @@ var (
DeleteRequest: "DeleteScopedRoleRequest",
},
}
scopedRoleAssignment = payload{
Name: "ScopedRoleAssignment",
TypeName: "ScopedRoleAssignment",
VarName: "scopedRoleAssignment",
GetMethod: "ScopedAccessServiceClient().GetScopedRoleAssignment",
CreateMethod: "ScopedAccessServiceClient().CreateScopedRoleAssignment",
UpdateMethod: "ScopedAccessServiceClient().UpsertScopedRoleAssignment",
UpsertMethodArity: 2,
DeleteMethod: "ScopedAccessServiceClient().DeleteScopedRoleAssignment",
ID: "scopedRoleAssignment.Metadata.Name",
Kind: "scoped_role_assignment",
HasStaticID: false,
ProtoPackage: "accessv1",
ProtoPackagePath: "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/access/v1",
SchemaPackage: "assignmentschemav1",
SchemaPackagePath: "github.com/gravitational/teleport/integrations/terraform/tfschema/scopes/access/assignment/v1",
TerraformResourceType: "teleport_scoped_role_assignment",
IsPlainStruct: true,
ExtraImports: []string{"apitypes \"github.com/gravitational/teleport/lib/scopes/access\""},
ForceSetKind: "apitypes.KindScopedRoleAssignment",
RequestWrapper: &RequestWrapper{
RequestResourceField: "Assignment",
GetRequest: "GetScopedRoleAssignmentRequest",
CreateRequest: "CreateScopedRoleAssignmentRequest",
UpdateRequest: "UpsertScopedRoleAssignmentRequest",
DeleteRequest: "DeleteScopedRoleAssignmentRequest",
},
DefaultSubKind: "\"dynamic\"",
}
scopedToken = payload{
Name: "ScopedToken",
TypeName: "ScopedToken",
@@ -1245,6 +1276,8 @@ func genTFSchema() {
generateDataSource(retrievalModel, singularDataSource)
generateResource(scopedRole, pluralResource)
generateDataSource(scopedRole, pluralDataSource)
generateResource(scopedRoleAssignment, pluralResource)
generateDataSource(scopedRoleAssignment, pluralDataSource)
generateResource(scopedToken, pluralResource)
generateDataSource(scopedToken, pluralDataSource)
generateResource(workloadCluster, pluralResource)
@@ -0,0 +1,71 @@
---
target_package_name: "v1"
default_package_name: "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/access/v1"
use_state_for_unknown_by_default: true
# Top-level type names to export
types:
- "ScopedRoleAssignment"
# These import paths were not being automatically picked up by
# protoc-gen-terraform without these overrides
import_path_overrides:
"types": "github.com/gravitational/teleport/api/types"
"wrappers": "github.com/gravitational/teleport/api/types/wrappers"
"timestamppb": "google.golang.org/protobuf/types/known/timestamppb"
"v1": "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1"
"github_com_gravitational_teleport_integrations_terraform_tfschema": "github.com/gravitational/teleport/integrations/terraform/tfschema"
# id field is required for integration tests. It is not used by provider.
# We have to add it manually (might be removed in the future versions).
injected_fields:
ScopedRoleAssignment:
- name: id
type: github.com/hashicorp/terraform-plugin-framework/types.StringType
computed: true
plan_modifiers:
- "github.com/hashicorp/terraform-plugin-framework/tfsdk.UseStateForUnknown()"
# These fields will be excluded
exclude_fields:
- "ScopedRoleAssignment.metadata.id"
# Usage tracking is server-side only
- "ScopedRoleAssignment.status"
# These fields will be marked as Computed: true
computed_fields:
- "ScopedRoleAssignment.metadata.expires"
- "ScopedRoleAssignment.metadata.namespace"
- "ScopedRoleAssignment.metadata.revision"
- "ScopedRoleAssignment.kind"
# These fields will be marked as Required: true
required_fields:
- "ScopedRoleAssignment.metadata"
- "ScopedRoleAssignment.metadata.name"
- "ScopedRoleAssignment.version"
- "ScopedRoleAssignment.sub_kind"
- "ScopedRoleAssignment.scope"
- "ScopedRoleAssignment.spec"
- "ScopedRoleAssignment.spec.assignments"
sensitive_fields:
plan_modifiers:
# Force to recreate resource if its name changes
Metadata.name:
- "github.com/hashicorp/terraform-plugin-framework/tfsdk.RequiresReplace()"
# This must be defined for the generator to be happy, but in reality all time
# fields are overridden (because the protobuf timestamps contain locks and the
# linter gets mad if we use raw structs instead of pointers).
time_type:
type: "PlaceholderType"
validators:
# Expires must be in the future
Metadata.expires:
- github_com_gravitational_teleport_integrations_terraform_tfschema.MustTimeBeInFuture()
custom_types:
"ScopedRoleAssignment.metadata.expires": Timestamp
@@ -0,0 +1,109 @@
// Code generated by _gen/main.go DO NOT EDIT
/*
Copyright 2015-2024 Gravitational, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package provider
import (
"context"
accessv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/access/v1"
"github.com/gravitational/trace"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
"github.com/hashicorp/terraform-plugin-framework/types"
assignmentschemav1 "github.com/gravitational/teleport/integrations/terraform/tfschema/scopes/access/assignment/v1"
)
// dataSourceTeleportScopedRoleAssignmentType is the data source metadata type
type dataSourceTeleportScopedRoleAssignmentType struct{}
// dataSourceTeleportScopedRoleAssignment is the resource
type dataSourceTeleportScopedRoleAssignment struct {
p Provider
}
// GetSchema returns the data source schema
func (r dataSourceTeleportScopedRoleAssignmentType) GetSchema(ctx context.Context) (tfsdk.Schema, diag.Diagnostics) {
return assignmentschemav1.GenSchemaScopedRoleAssignment(ctx)
}
// NewDataSource creates the empty data source
func (r dataSourceTeleportScopedRoleAssignmentType) NewDataSource(_ context.Context, p tfsdk.Provider) (tfsdk.DataSource, diag.Diagnostics) {
return dataSourceTeleportScopedRoleAssignment{
p: *(p.(*Provider)),
}, nil
}
// Read reads teleport ScopedRoleAssignment
func (r dataSourceTeleportScopedRoleAssignment) Read(ctx context.Context, req tfsdk.ReadDataSourceRequest, resp *tfsdk.ReadDataSourceResponse) {
var id types.String
diags := req.Config.GetAttribute(ctx, path.Root("metadata").AtName("name"), &id)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
var subKind types.String
diags = req.Config.GetAttribute(ctx, path.Root("sub_kind"), &subKind)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
if subKind.Value == "" {
subKind.Value = "dynamic"
}
scopedRoleAssignmentGetResp, err := r.p.Client.ScopedAccessServiceClient().GetScopedRoleAssignment(ctx, &accessv1.GetScopedRoleAssignmentRequest{
Name: id.Value,
SubKind: subKind.Value,
})
if err != nil {
resp.Diagnostics.Append(diagFromWrappedErr("Error reading ScopedRoleAssignment", trace.Wrap(err), "scoped_role_assignment"))
return
}
scopedRoleAssignmentI := scopedRoleAssignmentGetResp.GetAssignment()
var state types.Object
resp.Diagnostics.Append(req.Config.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
// Todo: Remove after updating terraform-plugin to >=v1.5.0.
// terraform-plugin-testing version <1.5.0 requires data resources to
// implement the 'id' attribute.
// https://developer.hashicorp.com/terraform/plugin/framework/acctests#no-id-found-in-attributes
v, ok := state.Attrs["id"]
if !ok || v.IsNull() {
state.Attrs["id"] = id
}
scopedRoleAssignment := scopedRoleAssignmentI
diags = assignmentschemav1.CopyScopedRoleAssignmentToTerraform(ctx, scopedRoleAssignment, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
diags = resp.State.Set(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}
@@ -586,6 +586,7 @@ func (p *Provider) GetResources(_ context.Context) (map[string]tfsdk.ResourceTyp
"teleport_scoped_token": resourceTeleportScopedTokenType{},
"teleport_workload_cluster": resourceTeleportWorkloadClusterType{},
"teleport_scoped_role": resourceTeleportScopedRoleType{},
"teleport_scoped_role_assignment": resourceTeleportScopedRoleAssignmentType{},
}, nil
}
@@ -628,6 +629,7 @@ func (p *Provider) GetDataSources(_ context.Context) (map[string]tfsdk.DataSourc
"teleport_integration": dataSourceTeleportIntegrationType{},
"teleport_scoped_token": dataSourceTeleportScopedTokenType{},
"teleport_scoped_role": dataSourceTeleportScopedRoleType{},
"teleport_scoped_role_assignment": dataSourceTeleportScopedRoleAssignmentType{},
// TODO(bl-nero): Add teleport_inference_* data sources after data sources
// are fixed. The current problems with data sources include:
// - Data sources only perform a "shallow fill", which means only setting
@@ -0,0 +1,411 @@
// Code generated by _gen/main.go DO NOT EDIT
/*
Copyright 2015-2024 Gravitational, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package provider
import (
"context"
"fmt"
"strings"
apitypes "github.com/gravitational/teleport/lib/scopes/access"
accessv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/access/v1"
"github.com/gravitational/teleport/api/utils/retryutils"
"github.com/gravitational/trace"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
"github.com/hashicorp/terraform-plugin-framework/types"
assignmentschemav1 "github.com/gravitational/teleport/integrations/terraform/tfschema/scopes/access/assignment/v1"
)
// resourceTeleportScopedRoleAssignmentType is the resource metadata type
type resourceTeleportScopedRoleAssignmentType struct{}
// resourceTeleportScopedRoleAssignment is the resource
type resourceTeleportScopedRoleAssignment struct {
p Provider
}
// GetSchema returns the resource schema
func (r resourceTeleportScopedRoleAssignmentType) GetSchema(ctx context.Context) (tfsdk.Schema, diag.Diagnostics) {
return assignmentschemav1.GenSchemaScopedRoleAssignment(ctx)
}
// NewResource creates the empty resource
func (r resourceTeleportScopedRoleAssignmentType) NewResource(_ context.Context, p tfsdk.Provider) (tfsdk.Resource, diag.Diagnostics) {
return resourceTeleportScopedRoleAssignment{
p: *(p.(*Provider)),
}, nil
}
// Create creates the ScopedRoleAssignment
func (r resourceTeleportScopedRoleAssignment) Create(ctx context.Context, req tfsdk.CreateResourceRequest, resp *tfsdk.CreateResourceResponse) {
var err error
if !r.p.IsConfigured(resp.Diagnostics) {
return
}
var plan types.Object
diags := req.Plan.Get(ctx, &plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
scopedRoleAssignment := &accessv1.ScopedRoleAssignment{}
diags = assignmentschemav1.CopyScopedRoleAssignmentFromTerraform(ctx, plan, scopedRoleAssignment)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
scopedRoleAssignmentResource := scopedRoleAssignment
scopedRoleAssignmentResource.Kind = apitypes.KindScopedRoleAssignment
if scopedRoleAssignmentResource.SubKind == "" {
scopedRoleAssignmentResource.SubKind = "dynamic"
}
id := scopedRoleAssignmentResource.Metadata.Name
_, err = r.p.Client.ScopedAccessServiceClient().GetScopedRoleAssignment(ctx, &accessv1.GetScopedRoleAssignmentRequest{
Name: id,
SubKind: scopedRoleAssignmentResource.SubKind,
})
if !trace.IsNotFound(err) {
if err == nil {
existErr := fmt.Sprintf("ScopedRoleAssignment exists in Teleport. Either remove it (tctl rm scoped_role_assignment/%v)"+
" or import it to the existing state (terraform import teleport_scoped_role_assignment.%v %v)", id, id, id)
resp.Diagnostics.Append(diagFromErr("ScopedRoleAssignment exists in Teleport", trace.Errorf(existErr)))
return
}
resp.Diagnostics.Append(diagFromWrappedErr("Error reading ScopedRoleAssignment", trace.Wrap(err), "scoped_role_assignment"))
return
}
_, err = r.p.Client.ScopedAccessServiceClient().CreateScopedRoleAssignment(ctx, &accessv1.CreateScopedRoleAssignmentRequest{
Assignment: scopedRoleAssignmentResource,
})
if err != nil {
resp.Diagnostics.Append(diagFromWrappedErr("Error creating ScopedRoleAssignment", trace.Wrap(err), "scoped_role_assignment"))
return
}
var scopedRoleAssignmentI *accessv1.ScopedRoleAssignment
// Try getting the resource until it exists.
tries := 0
retry, err := retryutils.NewRetryV2(retryutils.RetryV2Config{
Driver: retryutils.NewExponentialDriver(r.p.RetryConfig.Base),
First: r.p.RetryConfig.Base,
Max: r.p.RetryConfig.Cap,
Jitter: retryutils.HalfJitter,
})
if err != nil {
return
}
for {
tries = tries + 1
scopedRoleAssignmentGetResp, getErr := r.p.Client.ScopedAccessServiceClient().GetScopedRoleAssignment(ctx, &accessv1.GetScopedRoleAssignmentRequest{
Name: id,
SubKind: scopedRoleAssignmentResource.SubKind,
})
err = getErr
if err == nil {
scopedRoleAssignmentI = scopedRoleAssignmentGetResp.GetAssignment()
}
if trace.IsNotFound(err) {
select {
case <-ctx.Done():
resp.Diagnostics.Append(diagFromWrappedErr("Error reading ScopedRoleAssignment", trace.Wrap(ctx.Err()), "scoped_role_assignment"))
return
case <-retry.After():
}
if tries >= r.p.RetryConfig.MaxTries {
diagMessage := fmt.Sprintf("Error reading ScopedRoleAssignment (tried %d times) - state outdated, please import resource", tries)
resp.Diagnostics.AddError(diagMessage, "scoped_role_assignment")
return
}
continue
}
break
}
if err != nil {
resp.Diagnostics.Append(diagFromWrappedErr("Error reading ScopedRoleAssignment", trace.Wrap(err), "scoped_role_assignment"))
return
}
scopedRoleAssignmentResource = scopedRoleAssignmentI
scopedRoleAssignment = scopedRoleAssignmentResource
diags = assignmentschemav1.CopyScopedRoleAssignmentToTerraform(ctx, scopedRoleAssignment, &plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
plan.Attrs["id"] = types.String{Value: scopedRoleAssignment.Metadata.Name}
diags = resp.State.Set(ctx, &plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}
// Read reads teleport ScopedRoleAssignment
func (r resourceTeleportScopedRoleAssignment) Read(ctx context.Context, req tfsdk.ReadResourceRequest, resp *tfsdk.ReadResourceResponse) {
var state types.Object
diags := req.State.Get(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
var id types.String
diags = req.State.GetAttribute(ctx, path.Root("metadata").AtName("name"), &id)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
var subKind types.String
diags = req.State.GetAttribute(ctx, path.Root("sub_kind"), &subKind)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
if subKind.Value == "" {
subKind.Value = "dynamic"
}
scopedRoleAssignmentGetResp, err := r.p.Client.ScopedAccessServiceClient().GetScopedRoleAssignment(ctx, &accessv1.GetScopedRoleAssignmentRequest{
Name: id.Value,
SubKind: subKind.Value,
})
if trace.IsNotFound(err) {
resp.State.RemoveResource(ctx)
return
}
if err != nil {
resp.Diagnostics.Append(diagFromWrappedErr("Error reading ScopedRoleAssignment", trace.Wrap(err), "scoped_role_assignment"))
return
}
scopedRoleAssignmentI := scopedRoleAssignmentGetResp.GetAssignment()
scopedRoleAssignment := scopedRoleAssignmentI
diags = assignmentschemav1.CopyScopedRoleAssignmentToTerraform(ctx, scopedRoleAssignment, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
diags = resp.State.Set(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}
// Update updates teleport ScopedRoleAssignment
func (r resourceTeleportScopedRoleAssignment) Update(ctx context.Context, req tfsdk.UpdateResourceRequest, resp *tfsdk.UpdateResourceResponse) {
if !r.p.IsConfigured(resp.Diagnostics) {
return
}
var plan types.Object
diags := req.Plan.Get(ctx, &plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
scopedRoleAssignment := &accessv1.ScopedRoleAssignment{}
diags = assignmentschemav1.CopyScopedRoleAssignmentFromTerraform(ctx, plan, scopedRoleAssignment)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
scopedRoleAssignmentResource := scopedRoleAssignment
scopedRoleAssignmentResource.Kind = apitypes.KindScopedRoleAssignment
if scopedRoleAssignmentResource.SubKind == "" {
scopedRoleAssignmentResource.SubKind = "dynamic"
}
name := scopedRoleAssignmentResource.Metadata.Name
scopedRoleAssignmentBeforeResp, err := r.p.Client.ScopedAccessServiceClient().GetScopedRoleAssignment(ctx, &accessv1.GetScopedRoleAssignmentRequest{
Name: name,
SubKind: scopedRoleAssignmentResource.SubKind,
})
if err != nil {
resp.Diagnostics.Append(diagFromWrappedErr("Error reading ScopedRoleAssignment", err, "scoped_role_assignment"))
return
}
scopedRoleAssignmentBefore := scopedRoleAssignmentBeforeResp.GetAssignment()
_, err = r.p.Client.ScopedAccessServiceClient().UpsertScopedRoleAssignment(ctx, &accessv1.UpsertScopedRoleAssignmentRequest{
Assignment: scopedRoleAssignmentResource,
})
if err != nil {
resp.Diagnostics.Append(diagFromWrappedErr("Error updating ScopedRoleAssignment", err, "scoped_role_assignment"))
return
}
var scopedRoleAssignmentI *accessv1.ScopedRoleAssignment
tries := 0
retry, err := retryutils.NewRetryV2(retryutils.RetryV2Config{
Driver: retryutils.NewExponentialDriver(r.p.RetryConfig.Base),
First: r.p.RetryConfig.Base,
Max: r.p.RetryConfig.Cap,
Jitter: retryutils.HalfJitter,
})
if err != nil {
return
}
for {
tries = tries + 1
scopedRoleAssignmentGetResp, getErr := r.p.Client.ScopedAccessServiceClient().GetScopedRoleAssignment(ctx, &accessv1.GetScopedRoleAssignmentRequest{
Name: name,
SubKind: scopedRoleAssignmentResource.SubKind,
})
err = getErr
if err == nil {
scopedRoleAssignmentI = scopedRoleAssignmentGetResp.GetAssignment()
}
if err != nil {
resp.Diagnostics.Append(diagFromWrappedErr("Error reading ScopedRoleAssignment", err, "scoped_role_assignment"))
return
}
if scopedRoleAssignmentBefore.GetMetadata().Revision != scopedRoleAssignmentI.GetMetadata().Revision || false {
break
}
select {
case <-ctx.Done():
resp.Diagnostics.Append(diagFromWrappedErr("Error reading ScopedRoleAssignment", trace.Wrap(ctx.Err()), "scoped_role_assignment"))
return
case <-retry.After():
}
if tries >= r.p.RetryConfig.MaxTries {
diagMessage := fmt.Sprintf("Error reading ScopedRoleAssignment (tried %d times) - state outdated, please import resource", tries)
resp.Diagnostics.AddError(diagMessage, "scoped_role_assignment")
return
}
}
scopedRoleAssignmentResource = scopedRoleAssignmentI
diags = assignmentschemav1.CopyScopedRoleAssignmentToTerraform(ctx, scopedRoleAssignment, &plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
diags = resp.State.Set(ctx, plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}
// Delete deletes Teleport ScopedRoleAssignment
func (r resourceTeleportScopedRoleAssignment) Delete(ctx context.Context, req tfsdk.DeleteResourceRequest, resp *tfsdk.DeleteResourceResponse) {
var id types.String
diags := req.State.GetAttribute(ctx, path.Root("metadata").AtName("name"), &id)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
var subKind types.String
diags = req.State.GetAttribute(ctx, path.Root("sub_kind"), &subKind)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
if subKind.Value == "" {
subKind.Value = "dynamic"
}
_, err := r.p.Client.ScopedAccessServiceClient().DeleteScopedRoleAssignment(ctx, &accessv1.DeleteScopedRoleAssignmentRequest{
Name: id.Value,
SubKind: subKind.Value,
})
if err != nil {
resp.Diagnostics.Append(diagFromWrappedErr("Error deleting ScopedRoleAssignment", trace.Wrap(err), "scoped_role_assignment"))
return
}
resp.State.RemoveResource(ctx)
}
// ImportState imports ScopedRoleAssignment state
func (r resourceTeleportScopedRoleAssignment) ImportState(ctx context.Context, req tfsdk.ImportResourceStateRequest, resp *tfsdk.ImportResourceStateResponse) {
subKind := "dynamic"
name := req.ID
if before, after, ok := strings.Cut(req.ID, "/"); ok {
subKind = before
name = after
}
scopedRoleAssignmentGetResp, err := r.p.Client.ScopedAccessServiceClient().GetScopedRoleAssignment(ctx, &accessv1.GetScopedRoleAssignmentRequest{
Name: name,
SubKind: subKind,
})
if err != nil {
resp.Diagnostics.Append(diagFromWrappedErr("Error reading ScopedRoleAssignment", trace.Wrap(err), "scoped_role_assignment"))
return
}
scopedRoleAssignment := scopedRoleAssignmentGetResp.GetAssignment()
if err != nil {
resp.Diagnostics.Append(diagFromWrappedErr("Error reading ScopedRoleAssignment", trace.Wrap(err), "scoped_role_assignment"))
return
}
scopedRoleAssignmentResource := scopedRoleAssignment
var state types.Object
diags := resp.State.Get(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
diags = assignmentschemav1.CopyScopedRoleAssignmentToTerraform(ctx, scopedRoleAssignmentResource, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
id := scopedRoleAssignment.Metadata.Name
state.Attrs["id"] = types.String{Value: id}
diags = resp.State.Set(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}
@@ -0,0 +1,15 @@
resource "teleport_scoped_role_assignment" "test" {
version = "v1"
metadata = {
name = "test-scoped-role-assignment"
}
scope = "/staging"
sub_kind = "dynamic"
spec = {
user = "testuser"
assignments = [{
role = "test-scoped-role"
scope = "/staging/aa"
}]
}
}
@@ -0,0 +1,15 @@
resource "teleport_scoped_role_assignment" "test" {
version = "v1"
metadata = {
name = "test-scoped-role-assignment"
}
scope = "/staging"
sub_kind = "dynamic"
spec = {
user = "testuser"
assignments = [{
role = "test-scoped-role"
scope = "/staging/aaaa"
}]
}
}
@@ -0,0 +1,147 @@
// 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 testlib
import (
"fmt"
"time"
"github.com/gravitational/trace"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
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/lib/scopes/access"
)
func (s *TerraformSuiteOSS) TestScopedRoleAssignment() {
t := s.T()
ctx := t.Context()
t.Setenv("TELEPORT_UNSTABLE_SCOPES", "yes")
checkDestroyed := func(state *terraform.State) error {
accessClient := s.client.ScopedAccessServiceClient()
_, err := accessClient.GetScopedRoleAssignment(ctx, &accessv1.GetScopedRoleAssignmentRequest{
Name: "test-scoped-role-assignment",
SubKind: access.SubKindDynamic,
})
if !trace.IsNotFound(err) {
return trace.Errorf("expected not found, actual: %v", err)
}
return nil
}
name := "teleport_scoped_role_assignment.test"
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: s.terraformProviders,
CheckDestroy: checkDestroyed,
IsUnitTest: true,
Steps: []resource.TestStep{
{
Config: s.getFixture("scoped_role_assignment_0_create.tf"),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(name, "kind", access.KindScopedRoleAssignment),
resource.TestCheckResourceAttr(name, "sub_kind", access.SubKindDynamic),
resource.TestCheckResourceAttr(name, "scope", "/staging"),
resource.TestCheckResourceAttr(name, "spec.user", "testuser"),
resource.TestCheckResourceAttr(name, "spec.assignments.0.role", "test-scoped-role"),
resource.TestCheckResourceAttr(name, "spec.assignments.0.scope", "/staging/aa"),
),
},
{
Config: s.getFixture("scoped_role_assignment_1_update.tf"),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(name, "scope", "/staging"),
resource.TestCheckResourceAttr(name, "spec.assignments.0.role", "test-scoped-role"),
resource.TestCheckResourceAttr(name, "spec.assignments.0.scope", "/staging/aaaa"),
),
},
{
Config: s.getFixture("scoped_role_assignment_1_update.tf"),
PlanOnly: true,
},
},
})
}
func (s *TerraformSuiteOSS) TestImportScopedRoleAssignment() {
t := s.T()
ctx := t.Context()
t.Setenv("TELEPORT_UNSTABLE_SCOPES", "yes")
accessClient := s.client.ScopedAccessServiceClient()
r := "teleport_scoped_role_assignment"
id := "test-import-sra"
name := r + "." + id
assignment := &accessv1.ScopedRoleAssignment{
Kind: access.KindScopedRoleAssignment,
SubKind: access.SubKindDynamic,
Version: types.V1,
Metadata: &headerv1.Metadata{
Name: id,
},
Scope: "/staging",
Spec: &accessv1.ScopedRoleAssignmentSpec{
User: "testuser",
Assignments: []*accessv1.Assignment{
{
Role: "test-scoped-role",
Scope: "/staging/aa",
},
},
},
}
_, err := accessClient.CreateScopedRoleAssignment(ctx, &accessv1.CreateScopedRoleAssignmentRequest{
Assignment: assignment,
})
require.NoError(t, err)
require.EventuallyWithT(t, func(t *assert.CollectT) {
_, err := accessClient.GetScopedRoleAssignment(ctx, &accessv1.GetScopedRoleAssignmentRequest{
Name: id,
SubKind: access.SubKindDynamic,
})
require.NoError(t, err)
}, 5*time.Second, time.Second)
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: s.terraformProviders,
IsUnitTest: true,
Steps: []resource.TestStep{
{
Config: fmt.Sprintf("%s\nresource %q %q { }", s.terraformConfig, r, id),
ResourceName: name,
ImportState: true,
ImportStateId: id,
ImportStateCheck: func(state []*terraform.InstanceState) error {
require.Equal(t, access.KindScopedRoleAssignment, state[0].Attributes["kind"])
require.Equal(t, "/staging", state[0].Attributes["scope"])
require.Equal(t, "testuser", state[0].Attributes["spec.user"])
return nil
},
},
},
})
}
@@ -0,0 +1,25 @@
/*
Copyright 2026 Gravitational, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package v1
import "github.com/gravitational/teleport/integrations/terraform/tfschema/resource153"
var (
GenSchemaTimestamp = resource153.GenSchemaTimestamp
CopyToTimestamp = resource153.CopyToTimestamp
CopyFromTimestamp = resource153.CopyFromTimestamp
)
+1
View File
@@ -845,6 +845,7 @@ func NewPresetTerraformProviderRole() types.Role {
types.NewRule(types.KindSAMLIdPServiceProvider, RW()),
types.NewRule(types.KindScopedToken, RW()),
types.NewRule(access.KindScopedRole, RW()),
types.NewRule(access.KindScopedRoleAssignment, RW()),
},
},
},
+1
View File
@@ -780,6 +780,7 @@ func TestAddRoleDefaults(t *testing.T) {
types.NewRule(types.KindRetrievalModel, RW()),
types.NewRule(types.KindScopedToken, RW()),
types.NewRule(access.KindScopedRole, RW()),
types.NewRule(access.KindScopedRoleAssignment, RW()),
},
},
},