From 221bf1326fb6ff32864ed4c28eb01a02206a4c12 Mon Sep 17 00:00:00 2001 From: Ewan Harris Date: Wed, 17 Sep 2025 17:55:10 +0100 Subject: [PATCH] feat(tf/generate): support creating the auth0_self_service_profile for import --- docs/auth0_terraform_generate.md | 2 +- internal/auth0/auth0.go | 2 + internal/auth0/mock/self_service_profiles.go | 153 +++++++++++++++++++ internal/auth0/self_service_profiles.go | 29 ++++ internal/cli/terraform.go | 2 + internal/cli/terraform_fetcher.go | 51 ++++++- internal/cli/terraform_fetcher_test.go | 74 +++++++++ 7 files changed, 311 insertions(+), 2 deletions(-) create mode 100644 internal/auth0/mock/self_service_profiles.go create mode 100644 internal/auth0/self_service_profiles.go diff --git a/docs/auth0_terraform_generate.md b/docs/auth0_terraform_generate.md index 4a190fcb1..14c899396 100644 --- a/docs/auth0_terraform_generate.md +++ b/docs/auth0_terraform_generate.md @@ -33,7 +33,7 @@ auth0 terraform generate [flags] ``` --force Skip confirmation. -o, --output-dir string Output directory for the generated Terraform config files. If not provided, the files will be saved in the current working directory. (default "./") - -r, --resources strings Resource types to generate Terraform config for. If not provided, config files for all available resources will be generated. (default [auth0_action,auth0_attack_protection,auth0_branding,auth0_phone_provider,auth0_client,auth0_client_grant,auth0_connection,auth0_custom_domain,auth0_flow,auth0_flow_vault_connection,auth0_form,auth0_email_provider,auth0_email_template,auth0_guardian,auth0_log_stream,auth0_network_acl,auth0_organization,auth0_pages,auth0_prompt,auth0_prompt_custom_text,auth0_prompt_screen_renderer,auth0_resource_server,auth0_role,auth0_tenant,auth0_trigger_actions]) + -r, --resources strings Resource types to generate Terraform config for. If not provided, config files for all available resources will be generated. (default [auth0_action,auth0_attack_protection,auth0_branding,auth0_phone_provider,auth0_client,auth0_client_grant,auth0_connection,auth0_custom_domain,auth0_flow,auth0_flow_vault_connection,auth0_form,auth0_email_provider,auth0_email_template,auth0_guardian,auth0_log_stream,auth0_network_acl,auth0_organization,auth0_pages,auth0_prompt,auth0_prompt_custom_text,auth0_prompt_screen_renderer,auth0_resource_server,auth0_role,auth0_self_service_profile,auth0_tenant,auth0_trigger_actions]) -v, --tf-version string Terraform version that ought to be used while generating the terraform files for resources. If not provided, 1.5.0 is used by default (default "1.5.0") ``` diff --git a/internal/auth0/auth0.go b/internal/auth0/auth0.go index e85aaf5d8..583a017c5 100644 --- a/internal/auth0/auth0.go +++ b/internal/auth0/auth0.go @@ -34,6 +34,7 @@ type API struct { Tenant TenantAPI User UserAPI Jobs JobsAPI + SelfServiceProfile SelfServiceProfileAPI HTTPClient HTTPClientAPI } @@ -66,6 +67,7 @@ func NewAPI(m *management.Management) *API { Tenant: m.Tenant, User: m.User, Jobs: m.Job, + SelfServiceProfile: m.SelfServiceProfile, HTTPClient: m, } } diff --git a/internal/auth0/mock/self_service_profiles.go b/internal/auth0/mock/self_service_profiles.go new file mode 100644 index 000000000..40f4a8aa8 --- /dev/null +++ b/internal/auth0/mock/self_service_profiles.go @@ -0,0 +1,153 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: self_service_profiles.go + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + management "github.com/auth0/go-auth0/management" + gomock "github.com/golang/mock/gomock" +) + +// MockSelfServiceProfileAPI is a mock of SelfServiceProfileAPI interface. +type MockSelfServiceProfileAPI struct { + ctrl *gomock.Controller + recorder *MockSelfServiceProfileAPIMockRecorder +} + +// MockSelfServiceProfileAPIMockRecorder is the mock recorder for MockSelfServiceProfileAPI. +type MockSelfServiceProfileAPIMockRecorder struct { + mock *MockSelfServiceProfileAPI +} + +// NewMockSelfServiceProfileAPI creates a new mock instance. +func NewMockSelfServiceProfileAPI(ctrl *gomock.Controller) *MockSelfServiceProfileAPI { + mock := &MockSelfServiceProfileAPI{ctrl: ctrl} + mock.recorder = &MockSelfServiceProfileAPIMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockSelfServiceProfileAPI) EXPECT() *MockSelfServiceProfileAPIMockRecorder { + return m.recorder +} + +// Create mocks base method. +func (m *MockSelfServiceProfileAPI) Create(ctx context.Context, p *management.SelfServiceProfile, opts ...management.RequestOption) error { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, p} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Create", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// Create indicates an expected call of Create. +func (mr *MockSelfServiceProfileAPIMockRecorder) Create(ctx, p interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, p}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockSelfServiceProfileAPI)(nil).Create), varargs...) +} + +// Delete mocks base method. +func (m *MockSelfServiceProfileAPI) Delete(ctx context.Context, id string, opts ...management.RequestOption) error { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, id} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Delete", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// Delete indicates an expected call of Delete. +func (mr *MockSelfServiceProfileAPIMockRecorder) Delete(ctx, id interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, id}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockSelfServiceProfileAPI)(nil).Delete), varargs...) +} + +// GetCustomText mocks base method. +func (m *MockSelfServiceProfileAPI) GetCustomText(ctx context.Context, id, language, page string, opts ...management.RequestOption) (map[string]interface{}, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, id, language, page} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetCustomText", varargs...) + ret0, _ := ret[0].(map[string]interface{}) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCustomText indicates an expected call of GetCustomText. +func (mr *MockSelfServiceProfileAPIMockRecorder) GetCustomText(ctx, id, language, page interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, id, language, page}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCustomText", reflect.TypeOf((*MockSelfServiceProfileAPI)(nil).GetCustomText), varargs...) +} + +// List mocks base method. +func (m *MockSelfServiceProfileAPI) List(ctx context.Context, opts ...management.RequestOption) (*management.SelfServiceProfileList, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "List", varargs...) + ret0, _ := ret[0].(*management.SelfServiceProfileList) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// List indicates an expected call of List. +func (mr *MockSelfServiceProfileAPIMockRecorder) List(ctx interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockSelfServiceProfileAPI)(nil).List), varargs...) +} + +// Read mocks base method. +func (m *MockSelfServiceProfileAPI) Read(ctx context.Context, id string, opts ...management.RequestOption) (*management.SelfServiceProfile, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, id} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Read", varargs...) + ret0, _ := ret[0].(*management.SelfServiceProfile) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Read indicates an expected call of Read. +func (mr *MockSelfServiceProfileAPIMockRecorder) Read(ctx, id interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, id}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockSelfServiceProfileAPI)(nil).Read), varargs...) +} + +// Update mocks base method. +func (m *MockSelfServiceProfileAPI) Update(ctx context.Context, id string, p *management.SelfServiceProfile, opts ...management.RequestOption) error { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, id, p} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Update", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// Update indicates an expected call of Update. +func (mr *MockSelfServiceProfileAPIMockRecorder) Update(ctx, id, p interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, id, p}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockSelfServiceProfileAPI)(nil).Update), varargs...) +} diff --git a/internal/auth0/self_service_profiles.go b/internal/auth0/self_service_profiles.go new file mode 100644 index 000000000..abdac2028 --- /dev/null +++ b/internal/auth0/self_service_profiles.go @@ -0,0 +1,29 @@ +//go:generate mockgen -source=self_service_profiles.go -destination=mock/self_service_profiles.go -package=mock + +package auth0 + +import ( + "context" + + "github.com/auth0/go-auth0/management" +) + +type SelfServiceProfileAPI interface { + // Create a new sSelf Service Profiles. + Create(ctx context.Context, p *management.SelfServiceProfile, opts ...management.RequestOption) error + + // List all Self Service Profiles. + List(ctx context.Context, opts ...management.RequestOption) (p *management.SelfServiceProfileList, err error) + + // Read Self Service Profile details for a given profile ID. + Read(ctx context.Context, id string, opts ...management.RequestOption) (p *management.SelfServiceProfile, err error) + + // Update an existing Self Service Profile. + Update(ctx context.Context, id string, p *management.SelfServiceProfile, opts ...management.RequestOption) error + + // Delete a Self Service Profile. + Delete(ctx context.Context, id string, opts ...management.RequestOption) error + + // GetCustomText retrieves text customizations for a given self-service profile, language and Self Service SSO Flow page. + GetCustomText(ctx context.Context, id string, language string, page string, opts ...management.RequestOption) (payload map[string]interface{}, err error) +} diff --git a/internal/cli/terraform.go b/internal/cli/terraform.go index d4a89963c..050f6f33c 100644 --- a/internal/cli/terraform.go +++ b/internal/cli/terraform.go @@ -113,6 +113,8 @@ func (i *terraformInputs) parseResourceFetchers(api *auth0.API) ([]resourceDataF fetchers = append(fetchers, &resourceServerResourceFetcher{api}) case "auth0_role", "auth0_role_permissions": fetchers = append(fetchers, &roleResourceFetcher{api}) + case "auth0_self_service_profile": + fetchers = append(fetchers, &selfServiceProfileFetcher{api}) case "auth0_tenant": fetchers = append(fetchers, &tenantResourceFetcher{}) case "auth0_trigger_actions": diff --git a/internal/cli/terraform_fetcher.go b/internal/cli/terraform_fetcher.go index 2b3385e29..dfffbffff 100644 --- a/internal/cli/terraform_fetcher.go +++ b/internal/cli/terraform_fetcher.go @@ -12,7 +12,7 @@ import ( ) var ( - defaultResources = []string{"auth0_action", "auth0_attack_protection", "auth0_branding", "auth0_phone_provider", "auth0_client", "auth0_client_grant", "auth0_connection", "auth0_custom_domain", "auth0_flow", "auth0_flow_vault_connection", "auth0_form", "auth0_email_provider", "auth0_email_template", "auth0_guardian", "auth0_log_stream", "auth0_network_acl", "auth0_organization", "auth0_pages", "auth0_prompt", "auth0_prompt_custom_text", "auth0_prompt_screen_renderer", "auth0_resource_server", "auth0_role", "auth0_tenant", "auth0_trigger_actions"} + defaultResources = []string{"auth0_action", "auth0_attack_protection", "auth0_branding", "auth0_phone_provider", "auth0_client", "auth0_client_grant", "auth0_connection", "auth0_custom_domain", "auth0_flow", "auth0_flow_vault_connection", "auth0_form", "auth0_email_provider", "auth0_email_template", "auth0_guardian", "auth0_log_stream", "auth0_network_acl", "auth0_organization", "auth0_pages", "auth0_prompt", "auth0_prompt_custom_text", "auth0_prompt_screen_renderer", "auth0_resource_server", "auth0_role", "auth0_self_service_profile", "auth0_tenant", "auth0_trigger_actions"} ) type ( @@ -107,6 +107,10 @@ type ( api *auth0.API } + selfServiceProfileFetcher struct { + api *auth0.API + } + tenantResourceFetcher struct{} triggerActionsResourceFetcher struct { @@ -610,6 +614,51 @@ func (f *roleResourceFetcher) FetchData(ctx context.Context) (importDataList, er return data, nil } +var selfServiceProfileLanguages = []string{"en"} +var selfServiceProfilePages = []string{"get-started"} + +func (f *selfServiceProfileFetcher) FetchData(ctx context.Context) (importDataList, error) { + var data importDataList + + var page int + for { + profiles, err := f.api.SelfServiceProfile.List(ctx, management.Page(page)) + if err != nil { + return nil, err + } + + for _, profile := range profiles.SelfServiceProfile { + data = append(data, importDataItem{ + ResourceName: "auth0_self_service_profile." + sanitizeResourceName(profile.GetName()), + ImportID: profile.GetID(), + }) + + for _, lang := range selfServiceProfileLanguages { + for _, page := range selfServiceProfilePages { + customText, err := f.api.SelfServiceProfile.GetCustomText(ctx, profile.GetID(), lang, page) + if err != nil { + return nil, err + } + if len(customText) == 0 { + continue + } + + data = append(data, importDataItem{ + ResourceName: "auth0_self_service_profile_custom_text." + sanitizeResourceName(profile.GetName()+"_"+lang+"_"+page), + ImportID: profile.GetID() + "::" + lang + "::" + page, + }) + } + } + } + + if !profiles.HasNext() { + break + } + } + + return data, nil +} + func (f *tenantResourceFetcher) FetchData(_ context.Context) (importDataList, error) { return []importDataItem{ { diff --git a/internal/cli/terraform_fetcher_test.go b/internal/cli/terraform_fetcher_test.go index e37fa9ae3..d59a4b8ff 100644 --- a/internal/cli/terraform_fetcher_test.go +++ b/internal/cli/terraform_fetcher_test.go @@ -1916,6 +1916,80 @@ func TestRoleResourceFetcher_FetchData(t *testing.T) { }) } +func TestSelfServiceProfileFetcher(t *testing.T) { + t.Run("it successfully generates self service profile import data with custom text", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + selfServiceProfileAPI := mock.NewMockSelfServiceProfileAPI(ctrl) + + selfServiceProfileAPI.EXPECT(). + List(gomock.Any(), gomock.Any()). + Return(&management.SelfServiceProfileList{ + SelfServiceProfile: []*management.SelfServiceProfile{ + { + ID: auth0.String("ss_profile_1"), + Name: auth0.String("Self Service Profile 1"), + }, + }, + }, nil) + + selfServiceProfileAPI.EXPECT(). + GetCustomText(gomock.Any(), "ss_profile_1", "en", "get-started"). + Return(map[string]interface{}{ + "introduction": "hello, world", + }, nil) + + fetcher := selfServiceProfileFetcher{ + api: &auth0.API{ + SelfServiceProfile: selfServiceProfileAPI, + }, + } + + data, err := fetcher.FetchData(context.Background()) + assert.NoError(t, err) + assert.Len(t, data, 2) + assert.Equal(t, data[0].ResourceName, "auth0_self_service_profile.self_service_profile_1") + assert.Greater(t, len(data[0].ImportID), 0) + assert.Equal(t, data[1].ResourceName, "auth0_self_service_profile_custom_text.self_service_profile_1_en_get_started") + assert.Greater(t, len(data[1].ImportID), 0) + }) + + t.Run("it successfully generates self service profile import data without custom text", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + selfServiceProfileAPI := mock.NewMockSelfServiceProfileAPI(ctrl) + + selfServiceProfileAPI.EXPECT(). + List(gomock.Any(), gomock.Any()). + Return(&management.SelfServiceProfileList{ + SelfServiceProfile: []*management.SelfServiceProfile{ + { + ID: auth0.String("ss_profile_1"), + Name: auth0.String("Self Service Profile 1"), + }, + }, + }, nil) + + selfServiceProfileAPI.EXPECT(). + GetCustomText(gomock.Any(), "ss_profile_1", "en", "get-started"). + Return(map[string]interface{}{}, nil) + + fetcher := selfServiceProfileFetcher{ + api: &auth0.API{ + SelfServiceProfile: selfServiceProfileAPI, + }, + } + + data, err := fetcher.FetchData(context.Background()) + assert.NoError(t, err) + assert.Len(t, data, 1) + assert.Equal(t, data[0].ResourceName, "auth0_self_service_profile.self_service_profile_1") + assert.Greater(t, len(data[0].ImportID), 0) + }) +} + func TestTenantResourceFetcher_FetchData(t *testing.T) { t.Run("it successfully generates tenant import data", func(t *testing.T) { fetcher := tenantResourceFetcher{}