Skip to content
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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
* Improve API error handling to decode both JSON:API error objects and regular JSON errors arrays by @uk1288 [#1304](https://github.com/hashicorp/go-tfe/pull/1304)

## Enhancements
* Adds the `ProviderType` field to `AdminSAMLSetting` and `AdminSAMLSettingsUpdateOptions` to support the `provider-type` SAML setting.
* Adds the `ProviderType` field to `AdminSAMLSetting` and `AdminSAMLSettingsUpdateOptions` to support the `provider-type` SAML setting by @skj-skj [#1303](https://github.com/hashicorp/go-tfe/pull/1303)
* Adds `AdminSCIMSetting` to support managing site-level SCIM settings by @skj-skj [#1307](https://github.com/hashicorp/go-tfe/pull/1307)

# v1.103.0

Expand Down
2 changes: 2 additions & 0 deletions admin_setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type AdminSettings struct {
Twilio TwilioSettings
Customization CustomizationSettings
OIDC OIDCSettings
SCIM SCIMSettings
}

func newAdminSettings(client *Client) *AdminSettings {
Expand All @@ -26,5 +27,6 @@ func newAdminSettings(client *Client) *AdminSettings {
Twilio: &adminTwilioSettings{client: client},
Customization: &adminCustomizationSettings{client: client},
OIDC: &adminOIDCSettings{client: client},
SCIM: &adminSCIMSettings{client: client},
}
}
88 changes: 88 additions & 0 deletions admin_setting_scim.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Copyright IBM Corp. 2018, 2026
// SPDX-License-Identifier: MPL-2.0

package tfe

import (
"context"
)

// Compile-time proof of interface implementation.
var _ SCIMSettings = (*adminSCIMSettings)(nil)

Comment thread
skj-skj marked this conversation as resolved.
// SCIMSettings describes all the scim settings related methods that the Terraform
// Enterprise API supports
//
// TFE API docs: https://developer.hashicorp.com/terraform/enterprise/api-docs/admin/settings
type SCIMSettings interface {
// Read scim settings
Read(ctx context.Context) (*AdminSCIMSetting, error)

// Update scim settings
Update(ctx context.Context, options AdminSCIMSettingUpdateOptions) (*AdminSCIMSetting, error)

// Delete scim settings
Delete(ctx context.Context) error
}

// adminSCIMSettings implements SCIMSettings.
type adminSCIMSettings struct {
client *Client
}

// AdminSCIMSetting represents the SCIM setting in Terraform Enterprise
type AdminSCIMSetting struct {
ID string `jsonapi:"primary,scim-settings"`
Enabled bool `jsonapi:"attr,enabled"`
Paused bool `jsonapi:"attr,paused"`
SiteAdminGroupSCIMID string `jsonapi:"attr,site-admin-group-scim-id"`
SiteAdminGroupDisplayName string `jsonapi:"attr,site-admin-group-display-name"`
}

// AdminSCIMSettingUpdateOptions represents the options for updating an admin SCIM setting.
type AdminSCIMSettingUpdateOptions struct {
Enabled *bool `jsonapi:"attr,enabled,omitempty"`
Paused *bool `jsonapi:"attr,paused,omitempty"`
SiteAdminGroupSCIMID *string `jsonapi:"attr,site-admin-group-scim-id,omitempty"`
}
Comment thread
skj-skj marked this conversation as resolved.

// Read scim setting.
func (a *adminSCIMSettings) Read(ctx context.Context) (*AdminSCIMSetting, error) {
req, err := a.client.NewRequest("GET", "admin/scim-settings", nil)
if err != nil {
return nil, err
}

scim := &AdminSCIMSetting{}
err = req.Do(ctx, scim)
if err != nil {
return nil, err
}

return scim, nil
}

// Update scim setting.
func (a *adminSCIMSettings) Update(ctx context.Context, options AdminSCIMSettingUpdateOptions) (*AdminSCIMSetting, error) {
req, err := a.client.NewRequest("PATCH", "admin/scim-settings", &options)
if err != nil {
return nil, err
}

scim := &AdminSCIMSetting{}
err = req.Do(ctx, scim)
if err != nil {
return nil, err
}
return scim, nil
}

// Delete scim setting.
func (a *adminSCIMSettings) Delete(ctx context.Context) error {
req, err := a.client.NewRequest("DELETE", "admin/scim-settings", nil)
if err != nil {
return err
}

return req.Do(ctx, nil)
}
196 changes: 196 additions & 0 deletions admin_setting_scim_integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
// Copyright IBM Corp. 2018, 2026
// SPDX-License-Identifier: MPL-2.0

package tfe

import (
"context"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestAdminSettings_SCIM_Read(t *testing.T) {
skipUnlessEnterprise(t)
client := testClient(t)
ctx := context.Background()

t.Run("read scim settings with default values", func(t *testing.T) {
scimSettings, err := client.Admin.Settings.SCIM.Read(ctx)
require.NoError(t, err)

assert.Equal(t, "scim", scimSettings.ID)
assert.False(t, scimSettings.Enabled)
assert.False(t, scimSettings.Paused)
assert.Empty(t, scimSettings.SiteAdminGroupSCIMID)
assert.Empty(t, scimSettings.SiteAdminGroupDisplayName)
})
}

func TestAdminSettings_SCIM_Update(t *testing.T) {
skipUnlessEnterprise(t)
client := testClient(t)
ctx := context.Background()

enableSAML(ctx, t, client, true)
defer enableSAML(ctx, t, client, false)

scimClient := client.Admin.Settings.SCIM

t.Run("enable scim settings", func(t *testing.T) {
err := setSAMLProviderType(ctx, t, client, true)
require.NoErrorf(t, err, "failed to set SAML provider type")
defer cleanupSCIMSettings(ctx, t, client)
Comment thread
skj-skj marked this conversation as resolved.

scimSettings, err := scimClient.Update(ctx, AdminSCIMSettingUpdateOptions{Enabled: Bool(true)})
require.NoError(t, err)

assert.True(t, scimSettings.Enabled)
})

t.Run("pause scim settings", func(t *testing.T) {
err := setSAMLProviderType(ctx, t, client, true)
require.NoErrorf(t, err, "failed to set SAML provider type")
defer cleanupSCIMSettings(ctx, t, client)
Comment thread
skj-skj marked this conversation as resolved.

_, err = scimClient.Update(ctx, AdminSCIMSettingUpdateOptions{
Enabled: Bool(true),
})
require.NoError(t, err)

testCases := []struct {
name string
paused bool
}{
{"pause scim provisioning", true},
{"unpause scim provisioning", false},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
_, err := scimClient.Update(ctx, AdminSCIMSettingUpdateOptions{Paused: &tc.paused})
require.NoError(t, err)
scimSettings, err := scimClient.Read(ctx)
require.NoError(t, err)
assert.Equal(t, tc.paused, scimSettings.Paused)
})
}
})

t.Run("update site admin group scim id", func(t *testing.T) {
err := setSAMLProviderType(ctx, t, client, true)
require.NoErrorf(t, err, "failed to set SAML provider type")
defer cleanupSCIMSettings(ctx, t, client)
Comment thread
skj-skj marked this conversation as resolved.

_, err = scimClient.Update(ctx, AdminSCIMSettingUpdateOptions{Enabled: Bool(true)})
require.NoError(t, err)

scimToken := generateSCIMToken(ctx, t, client)
scimGroupID := createSCIMGroup(ctx, t, client, "foo", scimToken)

testCases := []struct {
name string
scimGroupID string
raiseError bool
}{
{"link scim group to site admin role", scimGroupID, false},
{"trying to link non-existent group - should raise error", "this-group-doesn't-exist", true},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
_, err := scimClient.Update(ctx, AdminSCIMSettingUpdateOptions{SiteAdminGroupSCIMID: &tc.scimGroupID})
if tc.raiseError {
require.Error(t, err)
return
}
require.NoError(t, err)
scimSettings, err := scimClient.Read(ctx)
require.NoError(t, err)
assert.Equal(t, tc.scimGroupID, scimSettings.SiteAdminGroupSCIMID)
assert.Equal(t, "foo", scimSettings.SiteAdminGroupDisplayName)
})
}
})
}

func TestAdminSettings_SCIM_Delete(t *testing.T) {
skipUnlessEnterprise(t)
client := testClient(t)
ctx := context.Background()

enableSAML(ctx, t, client, true)
defer enableSAML(ctx, t, client, false)

scimClient := client.Admin.Settings.SCIM

t.Run("disable scim settings", func(t *testing.T) {
err := setSAMLProviderType(ctx, t, client, true)
require.NoErrorf(t, err, "failed to set SAML provider type")
defer cleanupSCIMSettings(ctx, t, client)

testCases := []struct {
name string
isScimEnabled bool
}{
{"disable scim provisioning when it's already enabled", true},
{"disable scim provisioning when it's already disabled - should not raise error", false},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if tc.isScimEnabled {
_, err := scimClient.Update(ctx, AdminSCIMSettingUpdateOptions{Enabled: Bool(true)})
require.NoError(t, err)
}

err := scimClient.Delete(ctx)
require.NoError(t, err)

scimSettings, err := scimClient.Read(ctx)
require.NoError(t, err)
assert.False(t, scimSettings.Enabled)
})
}
})
}

// cleanup scim settings by disabling scim provisioning and setting saml provider type to unknown.
func cleanupSCIMSettings(ctx context.Context, t *testing.T, client *Client) {
t.Helper()
scimSettings, err := client.Admin.Settings.SCIM.Read(ctx)
if err == nil && scimSettings.Enabled {
err = client.Admin.Settings.SCIM.Delete(ctx)
require.NoErrorf(t, err, "failed to disable SCIM provisioning")
Comment thread
skj-skj marked this conversation as resolved.
}

err = setSAMLProviderType(ctx, t, client, false)
require.NoErrorf(t, err, "failed to set SAML provider type")
}

// generate a SCIM token for testing
func generateSCIMToken(ctx context.Context, t *testing.T, client *Client) string {
t.Helper()
// TFE requires a minimum of 30 days for SCIM token expiration
expiredAt := time.Now().Add(30 * 24 * time.Hour)
Comment thread
skj-skj marked this conversation as resolved.

options := struct {
Description *string `jsonapi:"attr,description"`
ExpiredAt *time.Time `jsonapi:"attr,expired-at,iso8601"`
}{
Description: String("test-scim-token"),
ExpiredAt: &expiredAt,
}
Comment thread
skj-skj marked this conversation as resolved.
req, err := client.NewRequest("POST", "admin/scim-tokens", &options)
require.NoError(t, err)

var res struct {
Token string `jsonapi:"attr,token"`
}
err = req.Do(ctx, &res)
require.NoError(t, err)

return res.Token
}
1 change: 1 addition & 0 deletions generate_mocks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ mockgen -source=admin_setting_customization.go -destination=mocks/admin_setting_
mockgen -source=admin_setting_general.go -destination=mocks/admin_setting_general_mocks.go -package=mocks
mockgen -source=admin_setting_oidc.go -destination=mocks/admin_setting_oidc_mocks.go -package=mocks
mockgen -source=admin_setting_saml.go -destination=mocks/admin_setting_saml_mocks.go -package=mocks
mockgen -source=admin_setting_scim.go -destination=mocks/admin_setting_scim_mocks.go -package=mocks
mockgen -source=admin_setting_smtp.go -destination=mocks/admin_setting_smtp_mocks.go -package=mocks
mockgen -source=admin_setting_twilio.go -destination=mocks/admin_setting_twilio_mocks.go -package=mocks
mockgen -source=admin_terraform_version.go -destination=mocks/admin_terraform_version_mocks.go -package=mocks
Expand Down
Loading
Loading