From e07c5300d05e38da7b7061e99b2ab8032293169e Mon Sep 17 00:00:00 2001 From: Kit Foster Date: Fri, 11 Apr 2025 18:01:01 +0200 Subject: [PATCH 01/10] Fix saml access groups --- vercel/data_source_team_config.go | 19 ++++--- vercel/resource_team_config.go | 87 ++++++++++++++----------------- 2 files changed, 52 insertions(+), 54 deletions(-) diff --git a/vercel/data_source_team_config.go b/vercel/data_source_team_config.go index dc82e19a..1bc1fa34 100644 --- a/vercel/data_source_team_config.go +++ b/vercel/data_source_team_config.go @@ -112,14 +112,19 @@ func (d *teamConfigDataSource) Schema(_ context.Context, _ datasource.SchemaRequ Description: "Indicates if SAML is enforced for the team.", Computed: true, }, - "roles": schema.MapAttribute{ + "roles": schema.SingleNestedAttribute{ Description: "Directory groups to role or access group mappings.", - Computed: true, - ElementType: types.StringType, - }, - "access_group_id": schema.StringAttribute{ - Description: "The ID of the access group to use for the team.", - Computed: true, + Optional: true, + Attributes: map[string]schema.Attribute{ + "role": schema.StringAttribute{ + Description: "The role that the user should have in the project. One of 'MEMBER', 'OWNER', 'VIEWER', 'DEVELOPER', 'BILLING' or 'CONTRIBUTOR'. Depending on your Team's plan, some of these roles may be unavailable.", + Computed: true, + }, + "access_group_id": schema.StringAttribute{ + Description: "The ID of the access group to use for the team.", + Computed: true, + }, + }, }, }, Computed: true, diff --git a/vercel/resource_team_config.go b/vercel/resource_team_config.go index d1bb9bc0..4b5b3246 100644 --- a/vercel/resource_team_config.go +++ b/vercel/resource_team_config.go @@ -5,14 +5,12 @@ import ( "encoding/json" "fmt" "os" - "regexp" "strings" "github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" @@ -124,28 +122,18 @@ func (r *teamConfigResource) Schema(_ context.Context, req resource.SchemaReques Description: "Indicates if SAML is enforced for the team.", Required: true, }, - "roles": schema.MapAttribute{ + "roles": schema.SingleNestedAttribute{ Description: "Directory groups to role or access group mappings.", Optional: true, - ElementType: types.StringType, - Validators: []validator.Map{ - // Validate only this attribute or roles is configured. - mapvalidator.ExactlyOneOf( - path.MatchRoot("saml.roles"), - path.MatchRoot("saml.access_group_id"), - ), - }, - }, - "access_group_id": schema.StringAttribute{ - Description: "The ID of the access group to use for the team.", - Optional: true, - Validators: []validator.String{ - stringvalidator.RegexMatches(regexp.MustCompile("^ag_[A-z0-9_ -]+$"), "Access group ID must be a valid access group"), - // Validate only this attribute or roles is configured. - stringvalidator.ExactlyOneOf( - path.MatchRoot("saml.roles"), - path.MatchRoot("saml.access_group_id"), - ), + Attributes: map[string]schema.Attribute{ + "role": schema.StringAttribute{ + Description: "The role that the user should have in the project. One of 'MEMBER', 'OWNER', 'VIEWER', 'DEVELOPER', 'BILLING' or 'CONTRIBUTOR'. Depending on your Team's plan, some of these roles may be unavailable.", + Required: true, + }, + "access_group_id": schema.StringAttribute{ + Description: "The ID of the access group to use for the team.", + Optional: true, + }, }, }, }, @@ -222,18 +210,29 @@ type SamlDirectory struct { State types.String `tfsdk:"state"` } +// only one of these is non-nil +type SamlRoles struct { + Role *types.String `tfsdk:"role"` + AccessGroupID *types.String `tfsdk:"access_group_id"` +} + type Saml struct { - Enforced types.Bool `tfsdk:"enforced"` - Roles types.Map `tfsdk:"roles"` - AccessGroupId types.String `tfsdk:"access_group_id"` + Enforced types.Bool `tfsdk:"enforced"` + Roles types.Object `tfsdk:"roles"` } -var samlAttrTypes = map[string]attr.Type{ - "enforced": types.BoolType, - "roles": types.MapType{ElemType: types.StringType}, +var samlRoleType = map[string]attr.Type{ + "role": types.StringType, "access_group_id": types.StringType, } +var samlAttrTypes = map[string]attr.Type{ + "enforced": types.BoolType, + "roles": types.ObjectType{ + AttrTypes: samlRoleType, + }, +} + func (s *Saml) toUpdateSamlConfig(ctx context.Context) *client.UpdateSamlConfig { if s == nil { return nil @@ -242,13 +241,8 @@ func (s *Saml) toUpdateSamlConfig(ctx context.Context) *client.UpdateSamlConfig config := &client.UpdateSamlConfig{ Enforced: s.Enforced.ValueBool(), } - roles := map[string]string{} - if !s.AccessGroupId.IsNull() { - roles["accessGroupId"] = s.AccessGroupId.ValueString() - } else { - s.Roles.ElementsAs(ctx, &roles, false) - } - config.Roles = roles + + // TODO convert return config } @@ -359,20 +353,19 @@ func convertResponseToTeamConfig(ctx context.Context, response client.Team, avat saml := types.ObjectNull(samlAttrTypes) if response.Saml != nil && response.Saml.Roles != nil { samlValue := map[string]attr.Value{ - "enforced": types.BoolValue(response.Saml.Enforced), - "roles": types.MapNull(types.StringType), - "access_group_id": types.StringNull(), + "enforced": types.BoolValue(response.Saml.Enforced), + "roles": types.ObjectNull(samlRoleType), } - if response.Saml.Roles["accessGroupId"] != "" { - samlValue["access_group_id"] = types.StringValue(response.Saml.Roles["accessGroupId"]) - } else { - roles, diags := types.MapValueFrom(ctx, types.StringType, response.Saml.Roles) - if diags.HasError() { - return TeamConfig{}, diags - } - samlValue["roles"] = roles + + roles, diags := types.ObjectValue(samlRoleType, map[string]attr.Value{ + "role": types.StringValue(response.Saml.Roles["role"]), + "access_group_id": types.StringValue(response.Saml.Roles["accessGroupId"]), + }) + if diags.HasError() { + return TeamConfig{}, diags } - var diags diag.Diagnostics + samlValue["roles"] = roles + saml, diags = types.ObjectValue(samlAttrTypes, samlValue) if diags.HasError() { return TeamConfig{}, diags From aa631ef37d857a20a463768a802383fb5e6e9205 Mon Sep 17 00:00:00 2001 From: Kit Foster Date: Mon, 14 Apr 2025 15:45:07 +0200 Subject: [PATCH 02/10] =?UTF-8?q?=F0=9F=93=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/team.go | 30 ++++++++---- docs/data-sources/team_config.md | 11 ++++- docs/resources/team_config.md | 11 ++++- vercel/data_source_team_config.go | 19 ++++---- vercel/resource_team_config.go | 78 ++++++++++++++++++------------- 5 files changed, 93 insertions(+), 56 deletions(-) diff --git a/client/team.go b/client/team.go index 624cc983..8d2c94a9 100644 --- a/client/team.go +++ b/client/team.go @@ -19,12 +19,21 @@ type SamlRoleAccessGroupID struct { AccessGroupID string `json:"accessGroupId"` } -type SamlRole struct { +type SamlRoleAPI struct { Role *string AccessGroupID *SamlRoleAccessGroupID } -func (f *SamlRole) UnmarshalJSON(data []byte) error { +type SamlRolesAPI map[string]SamlRoleAPI + +type SamlRole struct { + Role *string `json:"role"` + AccessGroupID *string `json:"access_group_id"` +} + +type SamlRoles map[string]SamlRole + +func (f *SamlRoleAPI) UnmarshalJSON(data []byte) error { var role string if err := json.Unmarshal(data, &role); err == nil { f.Role = &role @@ -38,10 +47,8 @@ func (f *SamlRole) UnmarshalJSON(data []byte) error { return fmt.Errorf("received json is neither Role string nor AccessGroupID map") } -type SamlRoles map[string]string - func (f *SamlRoles) UnmarshalJSON(data []byte) error { - var result map[string]SamlRole + var result SamlRolesAPI if err := json.Unmarshal(data, &result); err != nil { return err } @@ -50,7 +57,14 @@ func (f *SamlRoles) UnmarshalJSON(data []byte) error { k := k v := v if v.Role != nil { - tmp[k] = *(v.Role) + tmp[k] = SamlRole{ + Role: v.Role, + } + } + if v.AccessGroupID != nil { + tmp[k] = SamlRole{ + AccessGroupID: &v.AccessGroupID.AccessGroupID, + } } } *f = tmp @@ -120,8 +134,8 @@ func (c *Client) GetTeam(ctx context.Context, idOrSlug string) (t Team, err erro } type UpdateSamlConfig struct { - Enforced bool `json:"enforced"` - Roles map[string]string `json:"roles"` + Enforced bool `json:"enforced"` + Roles map[string]any `json:"roles"` } type UpdateTeamRequest struct { diff --git a/docs/data-sources/team_config.md b/docs/data-sources/team_config.md index 856565e0..3dc1ed06 100644 --- a/docs/data-sources/team_config.md +++ b/docs/data-sources/team_config.md @@ -54,6 +54,13 @@ Read-Only: Read-Only: -- `access_group_id` (String) The ID of the access group to use for the team. - `enforced` (Boolean) Indicates if SAML is enforced for the team. -- `roles` (Map of String) Directory groups to role or access group mappings. +- `roles` (Map of Object) Directory groups to role or access group mappings. For each directory key, either a role or access group id is specified. The role is one of 'MEMBER', 'OWNER', 'VIEWER', 'DEVELOPER', 'BILLING' or 'CONTRIBUTOR'. The access group id is the id of an access group. (see [below for nested schema](#nestedatt--saml--roles)) + + +### Nested Schema for `saml.roles` + +Read-Only: + +- `access_group_id` (String) +- `role` (String) diff --git a/docs/resources/team_config.md b/docs/resources/team_config.md index 3e811834..5c3b71fb 100644 --- a/docs/resources/team_config.md +++ b/docs/resources/team_config.md @@ -78,5 +78,12 @@ Required: Optional: -- `access_group_id` (String) The ID of the access group to use for the team. -- `roles` (Map of String) Directory groups to role or access group mappings. +- `roles` (Map of Object) Directory groups to role or access group mappings. For each directory key, specify either a role or access group id. The role should be one of 'MEMBER', 'OWNER', 'VIEWER', 'DEVELOPER', 'BILLING' or 'CONTRIBUTOR'. The access group id should be the id of an access group. (see [below for nested schema](#nestedatt--saml--roles)) + + +### Nested Schema for `saml.roles` + +Optional: + +- `access_group_id` (String) +- `role` (String) diff --git a/vercel/data_source_team_config.go b/vercel/data_source_team_config.go index 1bc1fa34..9bf38408 100644 --- a/vercel/data_source_team_config.go +++ b/vercel/data_source_team_config.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/types" @@ -112,17 +113,13 @@ func (d *teamConfigDataSource) Schema(_ context.Context, _ datasource.SchemaRequ Description: "Indicates if SAML is enforced for the team.", Computed: true, }, - "roles": schema.SingleNestedAttribute{ - Description: "Directory groups to role or access group mappings.", - Optional: true, - Attributes: map[string]schema.Attribute{ - "role": schema.StringAttribute{ - Description: "The role that the user should have in the project. One of 'MEMBER', 'OWNER', 'VIEWER', 'DEVELOPER', 'BILLING' or 'CONTRIBUTOR'. Depending on your Team's plan, some of these roles may be unavailable.", - Computed: true, - }, - "access_group_id": schema.StringAttribute{ - Description: "The ID of the access group to use for the team.", - Computed: true, + "roles": schema.MapAttribute{ + Description: "Directory groups to role or access group mappings. For each directory key, either a role or access group id is specified. The role is one of 'MEMBER', 'OWNER', 'VIEWER', 'DEVELOPER', 'BILLING' or 'CONTRIBUTOR'. The access group id is the id of an access group.", + Computed: true, + ElementType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "role": types.StringType, + "access_group_id": types.StringType, }, }, }, diff --git a/vercel/resource_team_config.go b/vercel/resource_team_config.go index 4b5b3246..49069039 100644 --- a/vercel/resource_team_config.go +++ b/vercel/resource_team_config.go @@ -122,19 +122,16 @@ func (r *teamConfigResource) Schema(_ context.Context, req resource.SchemaReques Description: "Indicates if SAML is enforced for the team.", Required: true, }, - "roles": schema.SingleNestedAttribute{ - Description: "Directory groups to role or access group mappings.", + "roles": schema.MapAttribute{ + Description: "Directory groups to role or access group mappings. For each directory key, specify either a role or access group id. The role should be one of 'MEMBER', 'OWNER', 'VIEWER', 'DEVELOPER', 'BILLING' or 'CONTRIBUTOR'. The access group id should be the id of an access group.", Optional: true, - Attributes: map[string]schema.Attribute{ - "role": schema.StringAttribute{ - Description: "The role that the user should have in the project. One of 'MEMBER', 'OWNER', 'VIEWER', 'DEVELOPER', 'BILLING' or 'CONTRIBUTOR'. Depending on your Team's plan, some of these roles may be unavailable.", - Required: true, - }, - "access_group_id": schema.StringAttribute{ - Description: "The ID of the access group to use for the team.", - Optional: true, + ElementType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "role": types.StringType, + "access_group_id": types.StringType, }, }, + PlanModifiers: []planmodifier.Map{mapplanmodifier.UseStateForUnknown()}, }, }, Optional: true, @@ -212,28 +209,34 @@ type SamlDirectory struct { // only one of these is non-nil type SamlRoles struct { - Role *types.String `tfsdk:"role"` - AccessGroupID *types.String `tfsdk:"access_group_id"` + Role types.String `tfsdk:"role"` + AccessGroupID types.String `tfsdk:"access_group_id"` } type Saml struct { - Enforced types.Bool `tfsdk:"enforced"` - Roles types.Object `tfsdk:"roles"` + Enforced types.Bool `tfsdk:"enforced"` + Roles map[string]SamlRoles `tfsdk:"roles"` } -var samlRoleType = map[string]attr.Type{ +var samlRoleAttrType = map[string]attr.Type{ "role": types.StringType, "access_group_id": types.StringType, } +var samlRoleType = types.ObjectType{ + AttrTypes: samlRoleAttrType, +} + +var samlRolesType = types.MapType{ + ElemType: samlRoleType, +} + var samlAttrTypes = map[string]attr.Type{ "enforced": types.BoolType, - "roles": types.ObjectType{ - AttrTypes: samlRoleType, - }, + "roles": samlRolesType, } -func (s *Saml) toUpdateSamlConfig(ctx context.Context) *client.UpdateSamlConfig { +func (s *Saml) toUpdateSamlConfig() *client.UpdateSamlConfig { if s == nil { return nil } @@ -241,8 +244,17 @@ func (s *Saml) toUpdateSamlConfig(ctx context.Context) *client.UpdateSamlConfig config := &client.UpdateSamlConfig{ Enforced: s.Enforced.ValueBool(), } - - // TODO convert + roles := map[string]any{} + for k, v := range s.Roles { + if !v.Role.IsNull() { + roles[k] = v.Role.ValueString() + } else { + roles[k] = map[string]string{ + "accessGroupId": v.AccessGroupID.ValueString(), + } + } + } + config.Roles = roles return config } @@ -334,7 +346,7 @@ func (t *TeamConfig) toUpdateTeamRequest(ctx context.Context, avatar string, sta RemoteCaching: rc.toUpdateTeamRequest(), HideIPAddresses: hideIPAddressses, HideIPAddressesInLogDrains: hideIPAddresssesInLogDrains, - Saml: saml.toUpdateSamlConfig(ctx), + Saml: saml.toUpdateSamlConfig(), }, nil } @@ -352,21 +364,21 @@ func convertResponseToTeamConfig(ctx context.Context, response client.Team, avat saml := types.ObjectNull(samlAttrTypes) if response.Saml != nil && response.Saml.Roles != nil { - samlValue := map[string]attr.Value{ - "enforced": types.BoolValue(response.Saml.Enforced), - "roles": types.ObjectNull(samlRoleType), + roles := map[string]SamlRoles{} + for k, v := range response.Saml.Roles { + role := SamlRoles{ + Role: types.StringPointerValue(v.Role), + AccessGroupID: types.StringPointerValue(v.AccessGroupID), + } + roles[k] = role } - roles, diags := types.ObjectValue(samlRoleType, map[string]attr.Value{ - "role": types.StringValue(response.Saml.Roles["role"]), - "access_group_id": types.StringValue(response.Saml.Roles["accessGroupId"]), + var diags diag.Diagnostics + saml, diags = types.ObjectValueFrom(ctx, samlAttrTypes, &Saml{ + Enforced: types.BoolValue(response.Saml.Enforced), + Roles: roles, }) - if diags.HasError() { - return TeamConfig{}, diags - } - samlValue["roles"] = roles - saml, diags = types.ObjectValue(samlAttrTypes, samlValue) if diags.HasError() { return TeamConfig{}, diags } From d1f286199758bdbe9a243522d830ae5becfd9362 Mon Sep 17 00:00:00 2001 From: Kit Foster Date: Mon, 14 Apr 2025 17:54:04 +0200 Subject: [PATCH 03/10] =?UTF-8?q?=F0=9F=A7=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/team.go | 2 +- vercel/resource_team_config.go | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/client/team.go b/client/team.go index 8d2c94a9..54b965b1 100644 --- a/client/team.go +++ b/client/team.go @@ -28,7 +28,7 @@ type SamlRolesAPI map[string]SamlRoleAPI type SamlRole struct { Role *string `json:"role"` - AccessGroupID *string `json:"access_group_id"` + AccessGroupID *string `json:"accessGroupId"` } type SamlRoles map[string]SamlRole diff --git a/vercel/resource_team_config.go b/vercel/resource_team_config.go index 49069039..7e340c64 100644 --- a/vercel/resource_team_config.go +++ b/vercel/resource_team_config.go @@ -14,6 +14,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/mapdefault" "github.com/hashicorp/terraform-plugin-framework/resource/schema/mapplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" @@ -125,12 +126,19 @@ func (r *teamConfigResource) Schema(_ context.Context, req resource.SchemaReques "roles": schema.MapAttribute{ Description: "Directory groups to role or access group mappings. For each directory key, specify either a role or access group id. The role should be one of 'MEMBER', 'OWNER', 'VIEWER', 'DEVELOPER', 'BILLING' or 'CONTRIBUTOR'. The access group id should be the id of an access group.", Optional: true, + Computed: true, ElementType: types.ObjectType{ AttrTypes: map[string]attr.Type{ "role": types.StringType, "access_group_id": types.StringType, }, }, + Default: mapdefault.StaticValue(types.MapValueMust(types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "role": types.StringType, + "access_group_id": types.StringType, + }, + }, map[string]attr.Value{})), PlanModifiers: []planmodifier.Map{mapplanmodifier.UseStateForUnknown()}, }, }, @@ -246,6 +254,12 @@ func (s *Saml) toUpdateSamlConfig() *client.UpdateSamlConfig { } roles := map[string]any{} for k, v := range s.Roles { + if v.Role.IsNull() && v.AccessGroupID.IsNull() { + panic("SAML roles must specify either a role or access group id: " + k) + } + if !v.Role.IsNull() && !v.AccessGroupID.IsNull() { + panic("SAML roles must specify either a role or access group id, not both: " + k) + } if !v.Role.IsNull() { roles[k] = v.Role.ValueString() } else { @@ -363,7 +377,7 @@ func convertResponseToTeamConfig(ctx context.Context, response client.Team, avat } saml := types.ObjectNull(samlAttrTypes) - if response.Saml != nil && response.Saml.Roles != nil { + if response.Saml != nil { roles := map[string]SamlRoles{} for k, v := range response.Saml.Roles { role := SamlRoles{ From 64e49a2427c0b084b91faa1e51009a4a82a4b6d2 Mon Sep 17 00:00:00 2001 From: Kit Foster Date: Mon, 14 Apr 2025 18:17:21 +0200 Subject: [PATCH 04/10] =?UTF-8?q?=F0=9F=93=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vercel/resource_team_config.go | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/vercel/resource_team_config.go b/vercel/resource_team_config.go index 7e340c64..6d8e5202 100644 --- a/vercel/resource_team_config.go +++ b/vercel/resource_team_config.go @@ -244,9 +244,9 @@ var samlAttrTypes = map[string]attr.Type{ "roles": samlRolesType, } -func (s *Saml) toUpdateSamlConfig() *client.UpdateSamlConfig { +func (s *Saml) toUpdateSamlConfig() (*client.UpdateSamlConfig, diag.Diagnostics) { if s == nil { - return nil + return nil, nil } config := &client.UpdateSamlConfig{ @@ -254,11 +254,14 @@ func (s *Saml) toUpdateSamlConfig() *client.UpdateSamlConfig { } roles := map[string]any{} for k, v := range s.Roles { + var diags diag.Diagnostics if v.Role.IsNull() && v.AccessGroupID.IsNull() { - panic("SAML roles must specify either a role or access group id: " + k) + diags.AddError("SAML roles must specify either a role or access group id: %s", k) + return nil, diags } if !v.Role.IsNull() && !v.AccessGroupID.IsNull() { - panic("SAML roles must specify either a role or access group id, not both: " + k) + diags.AddError("SAML roles must specify either a role or access group id, not both: %s", k) + return nil, diags } if !v.Role.IsNull() { roles[k] = v.Role.ValueString() @@ -270,7 +273,7 @@ func (s *Saml) toUpdateSamlConfig() *client.UpdateSamlConfig { } config.Roles = roles - return config + return config, nil } type EnableConfig struct { @@ -335,6 +338,10 @@ func (t *TeamConfig) toUpdateTeamRequest(ctx context.Context, avatar string, sta if diags.HasError() { return client.UpdateTeamRequest{}, diags } + updateSamlConfig, diags := saml.toUpdateSamlConfig() + if diags.HasError() { + return client.UpdateTeamRequest{}, diags + } var hideIPAddressses *bool if !t.HideIPAddresses.IsUnknown() && !t.HideIPAddresses.IsNull() { @@ -360,7 +367,7 @@ func (t *TeamConfig) toUpdateTeamRequest(ctx context.Context, avatar string, sta RemoteCaching: rc.toUpdateTeamRequest(), HideIPAddresses: hideIPAddressses, HideIPAddressesInLogDrains: hideIPAddresssesInLogDrains, - Saml: saml.toUpdateSamlConfig(), + Saml: updateSamlConfig, }, nil } From b831b677a3169b22a626ec0e6be7705cd3d13b0c Mon Sep 17 00:00:00 2001 From: Kit Foster Date: Thu, 17 Apr 2025 16:31:52 +0200 Subject: [PATCH 05/10] =?UTF-8?q?=F0=9F=A7=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vercel/resource_team_config.go | 39 +++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/vercel/resource_team_config.go b/vercel/resource_team_config.go index 6d8e5202..53191d32 100644 --- a/vercel/resource_team_config.go +++ b/vercel/resource_team_config.go @@ -123,15 +123,31 @@ func (r *teamConfigResource) Schema(_ context.Context, req resource.SchemaReques Description: "Indicates if SAML is enforced for the team.", Required: true, }, - "roles": schema.MapAttribute{ + "roles": schema.MapNestedAttribute{ Description: "Directory groups to role or access group mappings. For each directory key, specify either a role or access group id. The role should be one of 'MEMBER', 'OWNER', 'VIEWER', 'DEVELOPER', 'BILLING' or 'CONTRIBUTOR'. The access group id should be the id of an access group.", Optional: true, Computed: true, - ElementType: types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "role": types.StringType, - "access_group_id": types.StringType, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "role": schema.StringAttribute{ + Description: "The role to assign to the user. One of 'MEMBER', 'OWNER', 'VIEWER', 'DEVELOPER', 'BILLING' or 'CONTRIBUTOR'.", + Optional: true, + Validators: []validator.String{ + stringvalidator.OneOf("MEMBER", "OWNER", "VIEWER", "DEVELOPER", "BILLING", "CONTRIBUTOR"), + }, + }, + "access_group_id": schema.StringAttribute{ + Description: "The access group id to assign to the user.", + Optional: true, + }, }, + // Not sure why, but this does not work + // Validators: []validator.Object{ + // objectvalidator.ExactlyOneOf( + // path.MatchRelative().AtName("role"), + // path.MatchRelative().AtName("access_group_id"), + // ), + // }, }, Default: mapdefault.StaticValue(types.MapValueMust(types.ObjectType{ AttrTypes: map[string]attr.Type{ @@ -387,9 +403,16 @@ func convertResponseToTeamConfig(ctx context.Context, response client.Team, avat if response.Saml != nil { roles := map[string]SamlRoles{} for k, v := range response.Saml.Roles { - role := SamlRoles{ - Role: types.StringPointerValue(v.Role), - AccessGroupID: types.StringPointerValue(v.AccessGroupID), + role := SamlRoles{} + if v.Role != nil { + role = SamlRoles{ + Role: types.StringPointerValue(v.Role), + } + } + if v.AccessGroupID != nil { + role = SamlRoles{ + AccessGroupID: types.StringPointerValue(v.AccessGroupID), + } } roles[k] = role } From bf1a78342d593d6e853e147c313fd657ccf14791 Mon Sep 17 00:00:00 2001 From: Kit Foster Date: Fri, 25 Apr 2025 10:42:52 +0200 Subject: [PATCH 06/10] =?UTF-8?q?=F0=9F=9A=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/team.go | 32 ++++++++++++++++++-- docs/resources/team_config.md | 6 ++-- vercel/resource_team_config.go | 55 +++++++++++----------------------- 3 files changed, 51 insertions(+), 42 deletions(-) diff --git a/client/team.go b/client/team.go index 54b965b1..e22a5f8a 100644 --- a/client/team.go +++ b/client/team.go @@ -133,9 +133,37 @@ func (c *Client) GetTeam(ctx context.Context, idOrSlug string) (t Team, err erro return t, err } +type UpdateSamlConfigRole struct { + Role *string `json:"role"` + AccessGroupID *string `json:"accessGroupId"` +} + type UpdateSamlConfig struct { - Enforced bool `json:"enforced"` - Roles map[string]any `json:"roles"` + Enforced bool `json:"enforced"` + Roles map[string]UpdateSamlConfigRole `json:"roles"` +} + +func (r *UpdateSamlConfig) MarshalJSON() ([]byte, error) { + roles := map[string]any{} + for k, v := range r.Roles { + if v.Role != nil && v.AccessGroupID != nil { + return nil, fmt.Errorf("bad union") + } + if v.Role != nil { + roles[k] = v.Role + } else if v.AccessGroupID != nil { + roles[k] = map[string]any{ + "accessGroupId": v.AccessGroupID, + } + } else { + return nil, fmt.Errorf("bad union") + } + + } + return json.Marshal(map[string]any{ + "enforced": r.Enforced, + "roles": roles, + }) } type UpdateTeamRequest struct { diff --git a/docs/resources/team_config.md b/docs/resources/team_config.md index 5c3b71fb..1b570f8a 100644 --- a/docs/resources/team_config.md +++ b/docs/resources/team_config.md @@ -78,12 +78,12 @@ Required: Optional: -- `roles` (Map of Object) Directory groups to role or access group mappings. For each directory key, specify either a role or access group id. The role should be one of 'MEMBER', 'OWNER', 'VIEWER', 'DEVELOPER', 'BILLING' or 'CONTRIBUTOR'. The access group id should be the id of an access group. (see [below for nested schema](#nestedatt--saml--roles)) +- `roles` (Attributes Map) Directory groups to role or access group mappings. For each directory key, specify either a role or access group id. The role should be one of 'MEMBER', 'OWNER', 'VIEWER', 'DEVELOPER', 'BILLING' or 'CONTRIBUTOR'. The access group id should be the id of an access group. (see [below for nested schema](#nestedatt--saml--roles)) ### Nested Schema for `saml.roles` Optional: -- `access_group_id` (String) -- `role` (String) +- `access_group_id` (String) The access group id to assign to the user. +- `role` (String) The role to assign to the user. One of 'MEMBER', 'OWNER', 'VIEWER', 'DEVELOPER', 'BILLING' or 'CONTRIBUTOR'. diff --git a/vercel/resource_team_config.go b/vercel/resource_team_config.go index 53191d32..21accc23 100644 --- a/vercel/resource_team_config.go +++ b/vercel/resource_team_config.go @@ -260,38 +260,6 @@ var samlAttrTypes = map[string]attr.Type{ "roles": samlRolesType, } -func (s *Saml) toUpdateSamlConfig() (*client.UpdateSamlConfig, diag.Diagnostics) { - if s == nil { - return nil, nil - } - - config := &client.UpdateSamlConfig{ - Enforced: s.Enforced.ValueBool(), - } - roles := map[string]any{} - for k, v := range s.Roles { - var diags diag.Diagnostics - if v.Role.IsNull() && v.AccessGroupID.IsNull() { - diags.AddError("SAML roles must specify either a role or access group id: %s", k) - return nil, diags - } - if !v.Role.IsNull() && !v.AccessGroupID.IsNull() { - diags.AddError("SAML roles must specify either a role or access group id, not both: %s", k) - return nil, diags - } - if !v.Role.IsNull() { - roles[k] = v.Role.ValueString() - } else { - roles[k] = map[string]string{ - "accessGroupId": v.AccessGroupID.ValueString(), - } - } - } - config.Roles = roles - - return config, nil -} - type EnableConfig struct { Enabled types.Bool `tfsdk:"enabled"` } @@ -331,6 +299,23 @@ func (r *RemoteCaching) toUpdateTeamRequest() *client.RemoteCaching { } } +func (r *Saml) toUpdateTeamRequest() *client.UpdateSamlConfig { + if r == nil { + return nil + } + roles := map[string]client.UpdateSamlConfigRole{} + for k, v := range r.Roles { + roles[k] = client.UpdateSamlConfigRole{ + Role: v.Role.ValueStringPointer(), + AccessGroupID: v.AccessGroupID.ValueStringPointer(), + } + } + return &client.UpdateSamlConfig{ + Enforced: r.Enforced.ValueBool(), + Roles: roles, + } +} + func (t *TeamConfig) toUpdateTeamRequest(ctx context.Context, avatar string, stateSlug types.String) (client.UpdateTeamRequest, diag.Diagnostics) { slug := t.Slug.ValueString() if stateSlug.ValueString() == t.Slug.ValueString() { @@ -354,10 +339,6 @@ func (t *TeamConfig) toUpdateTeamRequest(ctx context.Context, avatar string, sta if diags.HasError() { return client.UpdateTeamRequest{}, diags } - updateSamlConfig, diags := saml.toUpdateSamlConfig() - if diags.HasError() { - return client.UpdateTeamRequest{}, diags - } var hideIPAddressses *bool if !t.HideIPAddresses.IsUnknown() && !t.HideIPAddresses.IsNull() { @@ -383,7 +364,7 @@ func (t *TeamConfig) toUpdateTeamRequest(ctx context.Context, avatar string, sta RemoteCaching: rc.toUpdateTeamRequest(), HideIPAddresses: hideIPAddressses, HideIPAddressesInLogDrains: hideIPAddresssesInLogDrains, - Saml: updateSamlConfig, + Saml: saml.toUpdateTeamRequest(), }, nil } From 319bd941078ec58961b90f6dce015aa20033ff13 Mon Sep 17 00:00:00 2001 From: Kit Foster Date: Mon, 28 Apr 2025 14:05:34 +0200 Subject: [PATCH 07/10] =?UTF-8?q?=F0=9F=9B=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vercel/resource_team_config.go | 8 +--- vercel/validator_saml_roles.go | 71 ++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 7 deletions(-) create mode 100644 vercel/validator_saml_roles.go diff --git a/vercel/resource_team_config.go b/vercel/resource_team_config.go index 21accc23..2edeb2dd 100644 --- a/vercel/resource_team_config.go +++ b/vercel/resource_team_config.go @@ -141,14 +141,8 @@ func (r *teamConfigResource) Schema(_ context.Context, req resource.SchemaReques Optional: true, }, }, - // Not sure why, but this does not work - // Validators: []validator.Object{ - // objectvalidator.ExactlyOneOf( - // path.MatchRelative().AtName("role"), - // path.MatchRelative().AtName("access_group_id"), - // ), - // }, }, + Validators: []validator.Map{validateSamlRoles()}, Default: mapdefault.StaticValue(types.MapValueMust(types.ObjectType{ AttrTypes: map[string]attr.Type{ "role": types.StringType, diff --git a/vercel/validator_saml_roles.go b/vercel/validator_saml_roles.go new file mode 100644 index 00000000..34329848 --- /dev/null +++ b/vercel/validator_saml_roles.go @@ -0,0 +1,71 @@ +package vercel + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type samlRolesValidator struct{} + +var _ validator.Map = &samlRolesValidator{} + +func validateSamlRoles() validator.Map { + return &samlRolesValidator{} +} + +func (v *samlRolesValidator) Description(ctx context.Context) string { + return "Validates that exactly one of role or access_group_id is defined for each SAML role entry" +} + +func (v *samlRolesValidator) MarkdownDescription(ctx context.Context) string { + return "Validates that exactly one of role or access_group_id is defined for each SAML role entry" +} + +func (v *samlRolesValidator) ValidateMap(ctx context.Context, req validator.MapRequest, resp *validator.MapResponse) { + if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { + return + } + + // Get all the map keys + keys := req.ConfigValue.Elements() + + // For each key in the map + for key, value := range keys { + // Convert the value to an object + obj, ok := value.(types.Object) + + if !ok || obj.IsNull() || obj.IsUnknown() { + resp.Diagnostics.AddAttributeError( + req.Path.AtMapKey(key), + "Invalid SAML Role Configuration: "+key, + "Expected an object with role or access_group_id", + ) + continue + } + + role := obj.Attributes()["role"] + accessGroupID := obj.Attributes()["access_group_id"] + + // Check if both are set + if !role.IsNull() && !role.IsUnknown() && !accessGroupID.IsNull() && !accessGroupID.IsUnknown() { + resp.Diagnostics.AddAttributeError( + req.Path.AtMapKey(key), + "Invalid SAML Role Configuration: "+key, + "Only one of 'role' or 'access_group_id' can be set, not both", + ) + continue + } + + // Check if neither is set + if (role.IsNull() || role.IsUnknown()) && (accessGroupID.IsNull() || accessGroupID.IsUnknown()) { + resp.Diagnostics.AddAttributeError( + req.Path.AtMapKey(key), + "Invalid SAML Role Configuration: "+key, + "Either 'role' or 'access_group_id' must be set", + ) + continue + } + } +} From cfdd691243530ceda8eb57654fb33e069aab5bba Mon Sep 17 00:00:00 2001 From: Kit Foster Date: Mon, 28 Apr 2025 15:12:31 +0200 Subject: [PATCH 08/10] =?UTF-8?q?=F0=9F=97=BA=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vercel/resource_team_config.go | 230 ++++++++++++++++++++++++++++++++- 1 file changed, 228 insertions(+), 2 deletions(-) diff --git a/vercel/resource_team_config.go b/vercel/resource_team_config.go index 2edeb2dd..7c5f1c20 100644 --- a/vercel/resource_team_config.go +++ b/vercel/resource_team_config.go @@ -7,10 +7,13 @@ import ( "os" "strings" + "regexp" + "github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" @@ -28,8 +31,9 @@ import ( // Ensure the implementation satisfies the expected interfaces. var ( - _ resource.Resource = &teamConfigResource{} - _ resource.ResourceWithConfigure = &teamConfigResource{} + _ resource.Resource = &teamConfigResource{} + _ resource.ResourceWithConfigure = &teamConfigResource{} + _ resource.ResourceWithUpgradeState = &teamConfigResource{} ) func newTeamConfigResource() resource.Resource { @@ -64,6 +68,7 @@ func (r *teamConfigResource) Configure(ctx context.Context, req resource.Configu func (r *teamConfigResource) Schema(_ context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ + Version: 1, Description: "Manages the configuration of an existing Vercel Team.", Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ @@ -236,6 +241,13 @@ type Saml struct { Roles map[string]SamlRoles `tfsdk:"roles"` } +// for backwards compatibility +type SamlV0 struct { + Enforced types.Bool `tfsdk:"enforced"` + Roles types.Map `tfsdk:"roles"` + AccessGroupId types.String `tfsdk:"access_group_id"` +} + var samlRoleAttrType = map[string]attr.Type{ "role": types.StringType, "access_group_id": types.StringType, @@ -608,3 +620,217 @@ func (r *teamConfigResource) Delete(ctx context.Context, req resource.DeleteRequ // We don't actually delete the team, just remove it from state resp.State.RemoveResource(ctx) } + +// https://developer.hashicorp.com/terraform/plugin/framework/resources/state-upgrade#implementing-state-upgrade-support +func (r *teamConfigResource) UpgradeState(ctx context.Context) map[int64]resource.StateUpgrader { + return map[int64]resource.StateUpgrader{ + // State upgrade implementation from 0 to 1. + // roles.saml.access_group_id has been removed + // roles.saml.roles is now a map of objects with role and access_group_id, instead of a map of strings + 0: { + PriorSchema: &schema.Schema{ + Description: "Manages the configuration of an existing Vercel Team.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "The ID of the existing Vercel Team.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + stringplanmodifier.RequiresReplace(), + }, + }, + "name": schema.StringAttribute{ + Description: "The name of the team.", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, + "slug": schema.StringAttribute{ + Description: "The slug of the team. Will be used in the URL of the team's dashboard.", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, + "avatar": schema.MapAttribute{ + Description: "The `avatar` should be a the 'file' attribute from a vercel_file data source.", + Optional: true, + PlanModifiers: []planmodifier.Map{mapplanmodifier.RequiresReplace()}, + ElementType: types.StringType, + Validators: []validator.Map{ + mapvalidator.SizeAtLeast(1), + mapvalidator.SizeAtMost(1), + }, + }, + "description": schema.StringAttribute{ + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + Description: "A description of the team.", + }, + "sensitive_environment_variable_policy": schema.StringAttribute{ + Description: "Ensures that all environment variables created by members of this team will be created as Sensitive Environment Variables which can only be decrypted by Vercel's deployment system.: one of on, off or default.", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + Validators: []validator.String{ + stringvalidator.OneOf("on", "off"), + }, + }, + "email_domain": schema.StringAttribute{ + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + Description: "Hostname that'll be matched with emails on sign-up to automatically join the Team.", + }, + "saml": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "enforced": schema.BoolAttribute{ + Description: "Indicates if SAML is enforced for the team.", + Required: true, + }, + "roles": schema.MapAttribute{ + Description: "Directory groups to role or access group mappings.", + Optional: true, + ElementType: types.StringType, + Validators: []validator.Map{ + // Validate only this attribute or roles is configured. + mapvalidator.ExactlyOneOf( + path.MatchRoot("saml.roles"), + path.MatchRoot("saml.access_group_id"), + ), + }, + }, + "access_group_id": schema.StringAttribute{ + Description: "The ID of the access group to use for the team.", + Optional: true, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexp.MustCompile("^ag_[A-z0-9_ -]+$"), "Access group ID must be a valid access group"), + // Validate only this attribute or roles is configured. + stringvalidator.ExactlyOneOf( + path.MatchRoot("saml.roles"), + path.MatchRoot("saml.access_group_id"), + ), + }, + }, + }, + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.Object{objectplanmodifier.UseStateForUnknown()}, + Description: "Configuration for SAML authentication.", + }, + "invite_code": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + Description: "A code that can be used to join this team. Only visible to Team owners.", + }, + "preview_deployment_suffix": schema.StringAttribute{ + Optional: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + Computed: true, + Description: "The hostname that is used as the preview deployment suffix.", + }, + "remote_caching": schema.SingleNestedAttribute{ + Description: "Configuration for Remote Caching.", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.Object{objectplanmodifier.UseStateForUnknown()}, + Attributes: map[string]schema.Attribute{ + "enabled": schema.BoolAttribute{ + Description: "Indicates if Remote Caching is enabled.", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.Bool{boolplanmodifier.UseStateForUnknown()}, + }, + }, + }, + "enable_preview_feedback": schema.StringAttribute{ + Description: "Enables the Vercel Toolbar on your preview deployments: one of on, off or default.", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + Validators: []validator.String{ + stringvalidator.OneOf("default", "on", "off"), + }, + }, + "enable_production_feedback": schema.StringAttribute{ + Description: "Enables the Vercel Toolbar on your production deployments: one of on, off or default.", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + Validators: []validator.String{ + stringvalidator.OneOf("default", "on", "off"), + }, + }, + "hide_ip_addresses": schema.BoolAttribute{ + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.Bool{boolplanmodifier.UseStateForUnknown()}, + Description: "Indicates if ip addresses should be accessible in o11y tooling.", + }, + "hide_ip_addresses_in_log_drains": schema.BoolAttribute{ + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.Bool{boolplanmodifier.UseStateForUnknown()}, + Description: "Indicates if ip addresses should be accessible in log drains.", + }, + }, + }, + StateUpgrader: func(ctx context.Context, req resource.UpgradeStateRequest, resp *resource.UpgradeStateResponse) { + var priorStateData TeamConfig + + resp.Diagnostics.Append(req.State.Get(ctx, &priorStateData)...) + + if resp.Diagnostics.HasError() { + return + } + + upgradedStateData := TeamConfig{ + ID: priorStateData.ID, + Avatar: priorStateData.Avatar, + Name: priorStateData.Name, + Slug: priorStateData.Slug, + Description: priorStateData.Description, + InviteCode: priorStateData.InviteCode, + SensitiveEnvironmentVariablePolicy: priorStateData.SensitiveEnvironmentVariablePolicy, + EmailDomain: priorStateData.EmailDomain, + PreviewDeploymentSuffix: priorStateData.PreviewDeploymentSuffix, + RemoteCaching: priorStateData.RemoteCaching, + EnablePreviewFeedback: priorStateData.EnablePreviewFeedback, + EnableProductionFeedback: priorStateData.EnableProductionFeedback, + HideIPAddresses: priorStateData.HideIPAddresses, + HideIPAddressesInLogDrains: priorStateData.HideIPAddressesInLogDrains, + } + + if !priorStateData.Saml.IsNull() { + var samlV0 *SamlV0 + diags := priorStateData.Saml.As(ctx, &samlV0, basetypes.ObjectAsOptions{ + UnhandledNullAsEmpty: true, + UnhandledUnknownAsEmpty: true, + }) + if diags.HasError() { + return + } + // samlV0 did not correctly handle access groups, so don't need to upgrade them. + // we do need to upgrade the roles object to the new format. + roles := map[string]SamlRoles{} + for k, v := range samlV0.Roles.Elements() { + role := v.String() + roles[k] = SamlRoles{ + Role: types.StringPointerValue(&role), + } + } + saml, diags := types.ObjectValueFrom(ctx, samlAttrTypes, &Saml{ + Enforced: samlV0.Enforced, + Roles: roles, + }) + if diags.HasError() { + return + } + upgradedStateData.Saml = saml + } + + resp.Diagnostics.Append(resp.State.Set(ctx, upgradedStateData)...) + }, + }, + } +} From 5dd04a1603502d7502b271814c178a9a2b2d3d4b Mon Sep 17 00:00:00 2001 From: Kit Foster Date: Tue, 29 Apr 2025 10:10:11 +0200 Subject: [PATCH 09/10] =?UTF-8?q?=F0=9F=A5=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/data-sources/team_config.md | 6 +++--- docs/resources/team_config.md | 2 +- vercel/data_source_team_config.go | 19 ++++++++++++------- vercel/resource_team_config.go | 5 ++--- 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/docs/data-sources/team_config.md b/docs/data-sources/team_config.md index 3dc1ed06..497b17b9 100644 --- a/docs/data-sources/team_config.md +++ b/docs/data-sources/team_config.md @@ -55,12 +55,12 @@ Read-Only: Read-Only: - `enforced` (Boolean) Indicates if SAML is enforced for the team. -- `roles` (Map of Object) Directory groups to role or access group mappings. For each directory key, either a role or access group id is specified. The role is one of 'MEMBER', 'OWNER', 'VIEWER', 'DEVELOPER', 'BILLING' or 'CONTRIBUTOR'. The access group id is the id of an access group. (see [below for nested schema](#nestedatt--saml--roles)) +- `roles` (Attributes Map) Directory groups to role or access group mappings. For each directory key, specify either a role or access group id. (see [below for nested schema](#nestedatt--saml--roles)) ### Nested Schema for `saml.roles` Read-Only: -- `access_group_id` (String) -- `role` (String) +- `access_group_id` (String) The access group id to assign to the user. +- `role` (String) The role to assign to the user. One of 'MEMBER', 'OWNER', 'VIEWER', 'DEVELOPER', 'BILLING' or 'CONTRIBUTOR'. diff --git a/docs/resources/team_config.md b/docs/resources/team_config.md index 1b570f8a..a20ecde4 100644 --- a/docs/resources/team_config.md +++ b/docs/resources/team_config.md @@ -78,7 +78,7 @@ Required: Optional: -- `roles` (Attributes Map) Directory groups to role or access group mappings. For each directory key, specify either a role or access group id. The role should be one of 'MEMBER', 'OWNER', 'VIEWER', 'DEVELOPER', 'BILLING' or 'CONTRIBUTOR'. The access group id should be the id of an access group. (see [below for nested schema](#nestedatt--saml--roles)) +- `roles` (Attributes Map) Directory groups to role or access group mappings. For each directory key, specify either a role or access group id. (see [below for nested schema](#nestedatt--saml--roles)) ### Nested Schema for `saml.roles` diff --git a/vercel/data_source_team_config.go b/vercel/data_source_team_config.go index 9bf38408..1cbc36e1 100644 --- a/vercel/data_source_team_config.go +++ b/vercel/data_source_team_config.go @@ -4,7 +4,6 @@ import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/types" @@ -113,13 +112,19 @@ func (d *teamConfigDataSource) Schema(_ context.Context, _ datasource.SchemaRequ Description: "Indicates if SAML is enforced for the team.", Computed: true, }, - "roles": schema.MapAttribute{ - Description: "Directory groups to role or access group mappings. For each directory key, either a role or access group id is specified. The role is one of 'MEMBER', 'OWNER', 'VIEWER', 'DEVELOPER', 'BILLING' or 'CONTRIBUTOR'. The access group id is the id of an access group.", + "roles": schema.MapNestedAttribute{ + Description: "Directory groups to role or access group mappings. For each directory key, specify either a role or access group id.", Computed: true, - ElementType: types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "role": types.StringType, - "access_group_id": types.StringType, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "role": schema.StringAttribute{ + Description: "The role to assign to the user. One of 'MEMBER', 'OWNER', 'VIEWER', 'DEVELOPER', 'BILLING' or 'CONTRIBUTOR'.", + Computed: true, + }, + "access_group_id": schema.StringAttribute{ + Description: "The access group id to assign to the user.", + Computed: true, + }, }, }, }, diff --git a/vercel/resource_team_config.go b/vercel/resource_team_config.go index 7c5f1c20..b37e7bc5 100644 --- a/vercel/resource_team_config.go +++ b/vercel/resource_team_config.go @@ -5,9 +5,8 @@ import ( "encoding/json" "fmt" "os" - "strings" - "regexp" + "strings" "github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" @@ -129,7 +128,7 @@ func (r *teamConfigResource) Schema(_ context.Context, req resource.SchemaReques Required: true, }, "roles": schema.MapNestedAttribute{ - Description: "Directory groups to role or access group mappings. For each directory key, specify either a role or access group id. The role should be one of 'MEMBER', 'OWNER', 'VIEWER', 'DEVELOPER', 'BILLING' or 'CONTRIBUTOR'. The access group id should be the id of an access group.", + Description: "Directory groups to role or access group mappings. For each directory key, specify either a role or access group id.", Optional: true, Computed: true, NestedObject: schema.NestedAttributeObject{ From 9b241afbc889d9417dfbc811849ec35ae9d89036 Mon Sep 17 00:00:00 2001 From: Kit Foster Date: Tue, 29 Apr 2025 10:12:44 +0200 Subject: [PATCH 10/10] =?UTF-8?q?=F0=9F=91=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/data-sources/team_config.md | 6 +++--- docs/resources/team_config.md | 4 ++-- vercel/data_source_team_config.go | 6 +++--- vercel/resource_team_config.go | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/data-sources/team_config.md b/docs/data-sources/team_config.md index 497b17b9..ec3138ab 100644 --- a/docs/data-sources/team_config.md +++ b/docs/data-sources/team_config.md @@ -55,12 +55,12 @@ Read-Only: Read-Only: - `enforced` (Boolean) Indicates if SAML is enforced for the team. -- `roles` (Attributes Map) Directory groups to role or access group mappings. For each directory key, specify either a role or access group id. (see [below for nested schema](#nestedatt--saml--roles)) +- `roles` (Attributes Map) Directory groups to role or access group mappings. For each directory group, either a role or access group id is specified. (see [below for nested schema](#nestedatt--saml--roles)) ### Nested Schema for `saml.roles` Read-Only: -- `access_group_id` (String) The access group id to assign to the user. -- `role` (String) The role to assign to the user. One of 'MEMBER', 'OWNER', 'VIEWER', 'DEVELOPER', 'BILLING' or 'CONTRIBUTOR'. +- `access_group_id` (String) The access group the assign is assigned to. +- `role` (String) The team level role the user is assigned. One of 'MEMBER', 'OWNER', 'VIEWER', 'DEVELOPER', 'BILLING' or 'CONTRIBUTOR'. diff --git a/docs/resources/team_config.md b/docs/resources/team_config.md index a20ecde4..d5bc02fc 100644 --- a/docs/resources/team_config.md +++ b/docs/resources/team_config.md @@ -78,7 +78,7 @@ Required: Optional: -- `roles` (Attributes Map) Directory groups to role or access group mappings. For each directory key, specify either a role or access group id. (see [below for nested schema](#nestedatt--saml--roles)) +- `roles` (Attributes Map) Directory groups to role or access group mappings. For each directory group, specify either a role or access group id. (see [below for nested schema](#nestedatt--saml--roles)) ### Nested Schema for `saml.roles` @@ -86,4 +86,4 @@ Optional: Optional: - `access_group_id` (String) The access group id to assign to the user. -- `role` (String) The role to assign to the user. One of 'MEMBER', 'OWNER', 'VIEWER', 'DEVELOPER', 'BILLING' or 'CONTRIBUTOR'. +- `role` (String) The team level role to assign to the user. One of 'MEMBER', 'OWNER', 'VIEWER', 'DEVELOPER', 'BILLING' or 'CONTRIBUTOR'. diff --git a/vercel/data_source_team_config.go b/vercel/data_source_team_config.go index 1cbc36e1..80026570 100644 --- a/vercel/data_source_team_config.go +++ b/vercel/data_source_team_config.go @@ -113,16 +113,16 @@ func (d *teamConfigDataSource) Schema(_ context.Context, _ datasource.SchemaRequ Computed: true, }, "roles": schema.MapNestedAttribute{ - Description: "Directory groups to role or access group mappings. For each directory key, specify either a role or access group id.", + Description: "Directory groups to role or access group mappings. For each directory group, either a role or access group id is specified.", Computed: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "role": schema.StringAttribute{ - Description: "The role to assign to the user. One of 'MEMBER', 'OWNER', 'VIEWER', 'DEVELOPER', 'BILLING' or 'CONTRIBUTOR'.", + Description: "The team level role the user is assigned. One of 'MEMBER', 'OWNER', 'VIEWER', 'DEVELOPER', 'BILLING' or 'CONTRIBUTOR'.", Computed: true, }, "access_group_id": schema.StringAttribute{ - Description: "The access group id to assign to the user.", + Description: "The access group the assign is assigned to.", Computed: true, }, }, diff --git a/vercel/resource_team_config.go b/vercel/resource_team_config.go index b37e7bc5..fd653819 100644 --- a/vercel/resource_team_config.go +++ b/vercel/resource_team_config.go @@ -128,13 +128,13 @@ func (r *teamConfigResource) Schema(_ context.Context, req resource.SchemaReques Required: true, }, "roles": schema.MapNestedAttribute{ - Description: "Directory groups to role or access group mappings. For each directory key, specify either a role or access group id.", + Description: "Directory groups to role or access group mappings. For each directory group, specify either a role or access group id.", Optional: true, Computed: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "role": schema.StringAttribute{ - Description: "The role to assign to the user. One of 'MEMBER', 'OWNER', 'VIEWER', 'DEVELOPER', 'BILLING' or 'CONTRIBUTOR'.", + Description: "The team level role to assign to the user. One of 'MEMBER', 'OWNER', 'VIEWER', 'DEVELOPER', 'BILLING' or 'CONTRIBUTOR'.", Optional: true, Validators: []validator.String{ stringvalidator.OneOf("MEMBER", "OWNER", "VIEWER", "DEVELOPER", "BILLING", "CONTRIBUTOR"),