Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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`, `AdminScimSettingUpdateOptions`, and `AdminSettings.SCIM` 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},
}
}
89 changes: 89 additions & 0 deletions admin_setting_scim.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// 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 {

Comment thread
skj-skj marked this conversation as resolved.
Outdated
// 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,omitempty"`
SiteAdminGroupDisplayName string `jsonapi:"attr,site-admin-group-display-name,omitempty"`
Comment thread
skj-skj marked this conversation as resolved.
Outdated
}

// AdminScimSettingUpdateOptions represents the options for updating a admin setting scim
Comment thread
skj-skj marked this conversation as resolved.
Outdated
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)
}
211 changes: 211 additions & 0 deletions admin_setting_scim_integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
// Copyright IBM Corp. 2018, 2026
// SPDX-License-Identifier: MPL-2.0

package tfe

import (
"context"
"strings"
"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", func(t *testing.T) {
scimSettings, err := client.Admin.Settings.SCIM.Read(ctx)
require.NoError(t, err)

assert.Equal(t, "scim", scimSettings.ID)
assert.NotNil(t, scimSettings.Enabled)
assert.NotNil(t, scimSettings.Paused)
assert.NotNil(t, scimSettings.SiteAdminGroupScimID)
assert.NotNil(t, scimSettings.SiteAdminGroupDisplayName)
Comment thread
skj-skj marked this conversation as resolved.
Outdated
})
}

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)
if err != nil {
t.Fatalf("failed to set SAML provider type: %v", err)
}
require.NoError(t, err)
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)
if err != nil {
t.Fatalf("failed to set SAML provider type: %v", err)
}
require.NoError(t, err)
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)
if err != nil {
t.Fatalf("failed to set SAML provider type: %v", err)
}
require.NoError(t, err)
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 side admin role", scimGroupID, false},
Comment thread
skj-skj marked this conversation as resolved.
Outdated
{"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)
if err != nil {
t.Fatalf("failed to set SAML provider type: %v", err)
}
require.NoError(t, err)
Comment thread
skj-skj marked this conversation as resolved.
Outdated
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) {
err := setSAMLProviderType(ctx, t, client, false)
if err != nil && strings.Contains(err.Error(), "Provider type cannot be changed while SCIM provisioning is enabled") {
err := client.Admin.Settings.SCIM.Delete(ctx)
Comment thread
skj-skj marked this conversation as resolved.
Outdated
if err != nil {
t.Fatalf("failed to disable SCIM provisioning: %v", err)
}
err = setSAMLProviderType(ctx, t, client, false)
if err != nil {
t.Fatalf("failed to set SAML provider type: %v", err)
}
} else if err != nil {
t.Fatalf("failed to set SAML provider type: %v", err)
}
}

// generate a SCIM token for testing
func generateSCIMToken(ctx context.Context, t *testing.T, client *Client) string {
expiresAt := time.Now().Add(30 * 24 * time.Hour)

options := struct {
Description *string `jsonapi:"attr,description"`
ExpireAt *time.Time `jsonapi:"attr,expired-at,iso8601"`
}{
Description: String("test-scim-token"),
ExpireAt: &expiresAt,
Comment thread
skj-skj marked this conversation as resolved.
Outdated
}
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 @@ -78,3 +78,4 @@ mockgen -source=project.go -destination=mocks/project_mocks.go -package=mocks
mockgen -source=registry_no_code_module.go -destination=mocks/registry_no_code_module_mocks.go -package=mocks
mockgen -source=registry_module.go -destination=mocks/registry_module_mocks.go -package=mocks
mockgen -source=workspace_resources.go -destination=mocks/workspace_resources.go -package=mocks
mockgen -source=admin_setting_scim.go -destination=mocks/admin_setting_scim_mocks.go -package=mocks
Comment thread
skj-skj marked this conversation as resolved.
Outdated
Loading
Loading