Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Saml on team config #229

Merged
merged 1 commit into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 2 additions & 13 deletions client/team.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,9 @@ type TeamCreateRequest struct {
Plan string `json:"plan"`
}

type SamlConnection struct {
Status string `json:"status"`
}

type SamlDirectory struct {
Type string `json:"type"`
State string `json:"state"`
}

type SamlConfig struct {
Connection *SamlConnection `json:"connection"`
Directory *SamlDirectory `json:"directory"`
Enforced bool `json:"enforced,omitempty"`
Roles map[string]string `json:"roles,omitempty"`
Enforced bool `json:"enforced,omitempty"`
Roles map[string]string `json:"roles,omitempty"`
}

type TaxID struct {
Expand Down
11 changes: 11 additions & 0 deletions docs/data-sources/team_config.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ data "vercel_team_config" "example" {
- `name` (String) The name of the team.
- `preview_deployment_suffix` (String) The hostname that is used as the preview deployment suffix.
- `remote_caching` (Attributes) Configuration for Remote Caching. (see [below for nested schema](#nestedatt--remote_caching))
- `saml` (Attributes) Configuration for SAML authentication. (see [below for nested schema](#nestedatt--saml))
- `sensitive_environment_variable_policy` (String) The policy for sensitive environment variables.
- `slug` (String) The slug of the team. Used in the URL of the team's dashboard.

Expand All @@ -46,3 +47,13 @@ data "vercel_team_config" "example" {
Read-Only:

- `enabled` (Boolean) Indicates if Remote Caching is enabled.


<a id="nestedatt--saml"></a>
### Nested Schema for `saml`

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.
14 changes: 14 additions & 0 deletions docs/resources/team_config.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ resource "vercel_team_config" "example" {
- `name` (String) The name of the team.
- `preview_deployment_suffix` (String) The hostname that is used as the preview deployment suffix.
- `remote_caching` (Attributes) Configuration for Remote Caching. (see [below for nested schema](#nestedatt--remote_caching))
- `saml` (Attributes) Configuration for SAML authentication. (see [below for nested schema](#nestedatt--saml))
- `sensitive_environment_variable_policy` (String)
- `slug` (String) The slug of the team. Will be used in the URL of the team's dashboard.

Expand All @@ -66,3 +67,16 @@ resource "vercel_team_config" "example" {
Optional:

- `enabled` (Boolean) Indicates if Remote Caching is enabled.


<a id="nestedatt--saml"></a>
### Nested Schema for `saml`

Required:

- `enforced` (Boolean) Indicates if SAML is enforced for the team.

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.
22 changes: 21 additions & 1 deletion vercel/data_source_team_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,25 @@ func (d *teamConfigDataSource) Schema(_ context.Context, _ datasource.SchemaRequ
Computed: true,
Description: "Indicates if ip addresses should be accessible in log drains.",
},
"saml": schema.SingleNestedAttribute{
Attributes: map[string]schema.Attribute{
"enforced": schema.BoolAttribute{
Description: "Indicates if SAML is enforced for the team.",
Computed: true,
},
"roles": schema.MapAttribute{
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,
},
},
Computed: true,
Description: "Configuration for SAML authentication.",
},
},
}
}
Expand All @@ -124,7 +143,7 @@ type TeamConfigData struct {
EnableProductionFeedback types.String `tfsdk:"enable_production_feedback"`
HideIPAddresses types.Bool `tfsdk:"hide_ip_addresses"`
HideIPAddressesInLogDrains types.Bool `tfsdk:"hide_ip_addresses_in_log_drains"`
// Saml types.Object `tfsdk:"saml"`
Saml types.Object `tfsdk:"saml"`
}

func (d *teamConfigDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
Expand Down Expand Up @@ -165,6 +184,7 @@ func (d *teamConfigDataSource) Read(ctx context.Context, req datasource.ReadRequ
HideIPAddresses: out.HideIPAddresses,
HideIPAddressesInLogDrains: out.HideIPAddressesInLogDrains,
RemoteCaching: out.RemoteCaching,
Saml: out.Saml,
})
resp.Diagnostics.Append(diags...)
}
183 changes: 121 additions & 62 deletions vercel/resource_team_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,22 @@ package vercel

import (
"context"
"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"
"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"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
Expand Down Expand Up @@ -111,54 +117,40 @@ func (r *teamConfigResource) Schema(_ context.Context, req resource.SchemaReques
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,
},
"access_group_id": schema.StringAttribute{
// TODO - enforce either accessGroupId or roles.
Description: "The ID of the access group to use for the team.",
Optional: true,
Validators: []validator.String{
stringRegex(regexp.MustCompile("^ag_[A-z0-9_ -]+$"), "Access group ID must be a valid access group"),
},
},
"connection": schema.SingleNestedAttribute{
Attributes: map[string]schema.Attribute{
"status": schema.StringAttribute{
Computed: true,
Description: "The current status of the connection.",
},
},
Description: "Info about the SAML connection.",
Computed: true,
"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.Expressions{
path.MatchRoot("saml.access_group_id"),
}...),
},
"directory": schema.SingleNestedAttribute{
Attributes: map[string]schema.Attribute{
"type": schema.StringAttribute{
Computed: true,
Description: "The identity provider type.",
},
"state": schema.StringAttribute{
Computed: true,
Description: "The current state of the SAML connection.",
},
},
Description: "Info about the SAML directory.",
Computed: true,
},
"access_group_id": schema.StringAttribute{
Description: "The ID of the access group to use for the team.",
Optional: true,
Validators: []validator.String{
stringRegex(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.Expressions{
path.MatchRoot("saml.roles"),
}...),
},
},
Optional: true,
Description: "Configuration for SAML authentication.",
},
*/
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()},
Expand All @@ -171,9 +163,10 @@ func (r *teamConfigResource) Schema(_ context.Context, req resource.SchemaReques
Description: "The hostname that is used as the preview deployment suffix.",
},
"remote_caching": schema.SingleNestedAttribute{
Description: "Configuration for Remote Caching.",
Optional: true,
Computed: true,
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.",
Expand Down Expand Up @@ -215,7 +208,6 @@ func (r *teamConfigResource) Schema(_ context.Context, req resource.SchemaReques
}
}

/*
type SamlConnection struct {
Status types.String `tfsdk:"status"`
}
Expand All @@ -226,16 +218,35 @@ type SamlDirectory struct {
}

type Saml struct {
Enforced types.Bool `tfsdk:"enforced"`
Roles types.Map `tfsdk:"roles"`
AccessGroupId types.String `tfsdk:"access_group_id"`
Connection *SamlConnection `tfsdk:"connection"`
Directory *SamlDirectory `tfsdk:"directory"`
Enforced types.Bool `tfsdk:"enforced"`
Roles types.Map `tfsdk:"roles"`
AccessGroupId types.String `tfsdk:"access_group_id"`
}

var samlAttrTypes = map[string]attr.Type{
"enforced": types.BoolType,
"roles": types.MapType{ElemType: types.StringType},
"access_group_id": types.StringType,
}

func (s Saml) toUpdateSamlConfig() *client.UpdateSamlConfig {
func (s *Saml) toUpdateSamlConfig(ctx context.Context) *client.UpdateSamlConfig {
if s == nil {
return nil
}

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

return config
}
*/

type EnableConfig struct {
Enabled types.Bool `tfsdk:"enabled"`
Expand All @@ -256,7 +267,7 @@ type TeamConfig struct {
EnableProductionFeedback types.String `tfsdk:"enable_production_feedback"`
HideIPAddresses types.Bool `tfsdk:"hide_ip_addresses"`
HideIPAddressesInLogDrains types.Bool `tfsdk:"hide_ip_addresses_in_log_drains"`
// Saml types.Object `tfsdk:"saml"`
Saml types.Object `tfsdk:"saml"`
}

type RemoteCaching struct {
Expand Down Expand Up @@ -291,6 +302,25 @@ func (t *TeamConfig) toUpdateTeamRequest(ctx context.Context, avatar string, sta
return client.UpdateTeamRequest{}, diags
}

var saml *Saml
diags = t.Saml.As(ctx, &saml, basetypes.ObjectAsOptions{
UnhandledNullAsEmpty: true,
UnhandledUnknownAsEmpty: true,
})
if diags.HasError() {
return client.UpdateTeamRequest{}, diags
}

var hideIPAddressses *bool
if !t.HideIPAddresses.IsUnknown() && !t.HideIPAddresses.IsNull() {
v := t.HideIPAddresses.ValueBool()
hideIPAddressses = &v
}
var hideIPAddresssesInLogDrains *bool
if !t.HideIPAddressesInLogDrains.IsUnknown() && !t.HideIPAddressesInLogDrains.IsNull() {
v := t.HideIPAddressesInLogDrains.ValueBool()
hideIPAddresssesInLogDrains = &v
}
return client.UpdateTeamRequest{
TeamID: t.ID.ValueString(),
Avatar: avatar,
Expand All @@ -303,9 +333,9 @@ func (t *TeamConfig) toUpdateTeamRequest(ctx context.Context, avatar string, sta
EnableProductionFeedback: t.EnableProductionFeedback.ValueString(),
SensitiveEnvironmentVariablePolicy: t.SensitiveEnvironmentVariablePolicy.ValueString(),
RemoteCaching: rc.toUpdateTeamRequest(),
HideIPAddresses: t.HideIPAddresses.ValueBoolPointer(),
HideIPAddressesInLogDrains: t.HideIPAddressesInLogDrains.ValueBoolPointer(),
// Saml: t.Saml.toUpdateSamlConfig(),
HideIPAddresses: hideIPAddressses,
HideIPAddressesInLogDrains: hideIPAddresssesInLogDrains,
Saml: saml.toUpdateSamlConfig(ctx),
}, nil
}

Expand All @@ -320,6 +350,30 @@ func convertResponseToTeamConfig(ctx context.Context, response client.Team, avat
return TeamConfig{}, diags
}
}

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(),
}
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
}
var diags diag.Diagnostics
saml, diags = types.ObjectValue(samlAttrTypes, samlValue)
if diags.HasError() {
return TeamConfig{}, diags
}
}

return TeamConfig{
Avatar: avatar,
ID: types.StringValue(response.ID),
Expand All @@ -335,7 +389,7 @@ func convertResponseToTeamConfig(ctx context.Context, response client.Team, avat
HideIPAddresses: types.BoolPointerValue(response.HideIPAddresses),
HideIPAddressesInLogDrains: types.BoolPointerValue(response.HideIPAddressesInLogDrains),
RemoteCaching: remoteCaching,
// Saml: types.StringValue(response.Saml),
Saml: saml,
}, nil
}

Expand Down Expand Up @@ -419,8 +473,10 @@ func (r *teamConfigResource) Create(ctx context.Context, req resource.CreateRequ
return
}

tflog.Info(ctx, "updated Team Configuration", map[string]interface{}{
"team_id": response.ID,
jsonResp, _ := json.Marshal(response)
tflog.Info(ctx, "created Team Configuration", map[string]interface{}{
"team_id": response.ID,
"response": string(jsonResp),
})

teamConfig, diags := convertResponseToTeamConfig(ctx, response, plan.Avatar)
Expand Down Expand Up @@ -454,6 +510,9 @@ func (r *teamConfigResource) Read(ctx context.Context, req resource.ReadRequest,
}

result, diags := convertResponseToTeamConfig(ctx, out, state.Avatar)
tflog.Info(ctx, "result", map[string]any{
"result": result,
})
if diags.HasError() {
resp.Diagnostics.Append(diags...)
return
Expand Down
Loading