diff --git a/.gitignore b/.gitignore index 290df26..61d1cb3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ .idea -provider/.github-action.local.env \ No newline at end of file +provider/.github-action.local.envprovider/terraform-provider-cdo diff --git a/client/client.go b/client/client.go index c51f787..33530a8 100644 --- a/client/client.go +++ b/client/client.go @@ -5,6 +5,7 @@ package client import ( "context" "github.com/CiscoDevnet/terraform-provider-cdo/go-client/msp/tenants" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/msp/usergroups" "github.com/CiscoDevnet/terraform-provider-cdo/go-client/msp/users" "net/http" @@ -311,3 +312,15 @@ func (c *Client) GenerateApiTokenForUserInMspManagedTenant(ctx context.Context, func (c *Client) RevokeApiTokenForUserInMspManagedTenant(ctx context.Context, revokeApiTokenInput users.MspRevokeApiTokenInput) (interface{}, error) { return users.RevokeApiToken(ctx, c.client, revokeApiTokenInput) } + +func (c *Client) CreateUserGroupsInMspManagedTenant(ctx context.Context, tenantUid string, userGroups *[]usergroups.MspManagedUserGroupInput) (*[]usergroups.MspManagedUserGroup, *usergroups.CreateError) { + return usergroups.Create(ctx, c.client, tenantUid, userGroups) +} + +func (c *Client) ReadUserGroupsInMspManagedTenant(ctx context.Context, tenantUid string, userGroups *[]usergroups.MspManagedUserGroupInput) (*[]usergroups.MspManagedUserGroup, error) { + return usergroups.ReadCreatedUserGroupsInTenant(ctx, c.client, tenantUid, userGroups) +} + +func (c *Client) DeleteUserGroupsInMspManagedTenant(ctx context.Context, tenantUid string, deleteInput *usergroups.MspManagedUserGroupDeleteInput) (interface{}, error) { + return usergroups.Delete(ctx, c.client, tenantUid, deleteInput) +} diff --git a/client/internal/publicapi/transaction/transactiontype/transactiontype.go b/client/internal/publicapi/transaction/transactiontype/transactiontype.go index f779478..ac28f96 100644 --- a/client/internal/publicapi/transaction/transactiontype/transactiontype.go +++ b/client/internal/publicapi/transaction/transactiontype/transactiontype.go @@ -3,16 +3,18 @@ package transactiontype type Type string const ( - ONBOARD_ASA Type = "ONBOARD_ASA" - ONBOARD_IOS Type = "ONBOARD_IOS" - ONBOARD_DUO_ADMIN_PANEL Type = "ONBOARD_DUO_ADMIN_PANEL" - CREATE_FTD Type = "CREATE_FTD" - REGISTER_FTD Type = "REGISTER_FTD" - DELETE_CDFMC_MANAGED_FTD Type = "DELETE_CDFMC_MANAGED_FTD" - RECONNECT_ASA Type = "RECONNECT_ASA" - READ_ASA Type = "READ_ASA" - DEPLOY_ASA_DEVICE_CHANGES Type = "DEPLOY_ASA_DEVICE_CHANGES" - MSP_CREATE_TENANT Type = "MSP_CREATE_TENANT" - MSP_ADD_USERS_TO_TENANT Type = "MSP_ADD_USERS_TO_TENANT" - MSP_DELETE_USERS_FROM_TENANT Type = "MSP_DELETE_USERS_FROM_TENANT" + ONBOARD_ASA Type = "ONBOARD_ASA" + ONBOARD_IOS Type = "ONBOARD_IOS" + ONBOARD_DUO_ADMIN_PANEL Type = "ONBOARD_DUO_ADMIN_PANEL" + CREATE_FTD Type = "CREATE_FTD" + REGISTER_FTD Type = "REGISTER_FTD" + DELETE_CDFMC_MANAGED_FTD Type = "DELETE_CDFMC_MANAGED_FTD" + RECONNECT_ASA Type = "RECONNECT_ASA" + READ_ASA Type = "READ_ASA" + DEPLOY_ASA_DEVICE_CHANGES Type = "DEPLOY_ASA_DEVICE_CHANGES" + MSP_CREATE_TENANT Type = "MSP_CREATE_TENANT" + MSP_ADD_USERS_TO_TENANT Type = "MSP_ADD_USERS_TO_TENANT" + MSP_DELETE_USERS_FROM_TENANT Type = "MSP_DELETE_USERS_FROM_TENANT" + MSP_ADD_USER_GROUPS_TO_TENANT Type = "MSP_ADD_USER_GROUPS_TO_TENANT" + MSP_DELETE_USER_GROUPS_FROM_TENANT Type = "MSP_DELETE_USER_GROUPS_FROM_TENANT" ) diff --git a/client/internal/url/url.go b/client/internal/url/url.go index 3068e31..da49849 100644 --- a/client/internal/url/url.go +++ b/client/internal/url/url.go @@ -237,3 +237,15 @@ func DeleteUsersInMspManagedTenant(baseUrl string, tenantUid string) string { func GenerateApiTokenForUserInMspManagedTenant(baseUrl string, tenantUid string, userUid string) string { return fmt.Sprintf("%s/api/rest/v1/msp/tenants/%s/users/%s/token", baseUrl, tenantUid, userUid) } + +func CreateUserGroupsInMspManagedTenant(baseUrl string, tenantUid string) string { + return fmt.Sprintf("%s/api/rest/v1/msp/tenants/%s/users/groups", baseUrl, tenantUid) +} + +func GetUserGroupsInMspManagedTenant(baseUrl string, tenantUid string, limit int, offset int) string { + return fmt.Sprintf("%s/api/rest/v1/msp/tenants/%s/users/groups?limit=%d&offset=%d", baseUrl, tenantUid, limit, offset) +} + +func DeleteUserGroupsInMspManagedTenant(baseUrl string, tenantUid string) string { + return fmt.Sprintf("%s/api/rest/v1/msp/tenants/%s/users/groups/delete", baseUrl, tenantUid) +} diff --git a/client/msp/usergroups/constants_test.go b/client/msp/usergroups/constants_test.go new file mode 100644 index 0000000..f7bf37e --- /dev/null +++ b/client/msp/usergroups/constants_test.go @@ -0,0 +1,5 @@ +package usergroups_test + +const ( + baseUrl = "https://unittest.cdo.cisco.com" +) diff --git a/client/msp/usergroups/create.go b/client/msp/usergroups/create.go new file mode 100644 index 0000000..3bd0242 --- /dev/null +++ b/client/msp/usergroups/create.go @@ -0,0 +1,43 @@ +package usergroups + +import ( + "context" + "fmt" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/http" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/publicapi" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/url" +) + +func Create(ctx context.Context, client http.Client, tenantUid string, userGroupsInput *[]MspManagedUserGroupInput) (*[]MspManagedUserGroup, *CreateError) { + client.Logger.Printf("Creating %d user groups in %s\n", len(*userGroupsInput), tenantUid) + createUrl := url.CreateUserGroupsInMspManagedTenant(client.BaseUrl(), tenantUid) + transaction, err := publicapi.TriggerTransaction(ctx, client, createUrl, userGroupsInput) + if err != nil { + return nil, &CreateError{ + Err: err, + CreatedResourceId: &transaction.EntityUid, + } + } + transaction, err = publicapi.WaitForTransactionToFinishWithDefaults( + ctx, + client, + transaction, + fmt.Sprintf("Waiting for users to be created and added to MSP-managed tenant %s...", tenantUid), + ) + if err != nil { + return nil, &CreateError{ + Err: err, + CreatedResourceId: &transaction.EntityUid, + } + } + + readUserGroupDetails, err := ReadCreatedUserGroupsInTenant(ctx, client, tenantUid, userGroupsInput) + if err != nil { + client.Logger.Println("Failed to read users from tenant after creation") + return nil, &CreateError{ + Err: err, + CreatedResourceId: &transaction.EntityUid, + } + } + return readUserGroupDetails, nil +} diff --git a/client/msp/usergroups/create_test.go b/client/msp/usergroups/create_test.go new file mode 100644 index 0000000..0abe2ca --- /dev/null +++ b/client/msp/usergroups/create_test.go @@ -0,0 +1,258 @@ +package usergroups_test + +import ( + "context" + "fmt" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/http" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/publicapi" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/publicapi/transaction" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/publicapi/transaction/transactionstatus" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/publicapi/transaction/transactiontype" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/msp/usergroups" + "github.com/google/uuid" + "github.com/jarcoal/httpmock" + "github.com/stretchr/testify/assert" + netHttp "net/http" + "sort" + "strconv" + "testing" + "time" +) + +// Function to generate user groups +func generateUserGroups(num int) []usergroups.MspManagedUserGroup { + var createdUserGroups []usergroups.MspManagedUserGroup + for i := 1; i <= num; i++ { + uid := "uid" + strconv.Itoa(i) // Generate unique UID + var role string + if i%2 == 0 { + role = "ROLE_SUPER_ADMIN" + } else { + role = "ROLE_ADMIN" + } + var notes string + if i%2 == 0 { + notes = "notes" + strconv.Itoa(i) + } + + createdUserGroups = append(createdUserGroups, usergroups.MspManagedUserGroup{ + Uid: uid, + GroupIdentifier: "groupIdentifier" + strconv.Itoa(i), + Name: "name" + strconv.Itoa(i), + Role: role, + Notes: ¬es, + }) + } + return createdUserGroups +} + +func TestCreate(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + t.Run("successfully create user groups in MSP-managed tenant", func(t *testing.T) { + httpmock.Reset() + var managedTenantUid = uuid.New().String() + var notes = "This is a group of developers" + var createInp = []usergroups.MspManagedUserGroupInput{ + { + GroupIdentifier: "developers", + IssuerUrl: "https://okta.com/123456", + Name: "Developers", + Role: "ROLE_ADMIN", + Notes: ¬es, + }, + { + GroupIdentifier: "managers", + IssuerUrl: "https://okta.com/123456", + Name: "Managers", + Role: "ROLE_READ_ONLY", + }, + } + var userGroupsInCdoTenant = generateUserGroups(250) + var userGroupsWithIds []usergroups.MspManagedUserGroup + for _, userGroup := range createInp { + userGroupWithId := usergroups.MspManagedUserGroup{ + Uid: uuid.New().String(), + GroupIdentifier: userGroup.GroupIdentifier, + IssuerUrl: userGroup.IssuerUrl, + Name: userGroup.Name, + Role: userGroup.Role, + Notes: userGroup.Notes, + } + userGroupsInCdoTenant = append(userGroupsInCdoTenant, userGroupWithId) + userGroupsWithIds = append(userGroupsWithIds, userGroupWithId) + } + firstUserGroupPage := usergroups.MspManagedUserGroupPage{Items: userGroupsInCdoTenant[:200], Count: len(userGroupsInCdoTenant), Limit: 200, Offset: 0} + secondUserGroupPage := usergroups.MspManagedUserGroupPage{Items: userGroupsInCdoTenant[200:], Count: len(userGroupsInCdoTenant), Limit: 200, Offset: 200} + var transactionUid = uuid.New().String() + var inProgressTransaction = transaction.Type{ + TransactionUid: transactionUid, + TenantUid: uuid.New().String(), + EntityUid: managedTenantUid, + EntityUrl: "https://unittest.cdo.cisco.com/api/rest/v1/msp/tenants/" + managedTenantUid, + PollingUrl: "https://unittest.cdo.cisco.com/api/rest/v1/transactions/" + transactionUid, + SubmissionTime: "2024-09-10T20:10:00Z", + LastUpdatedTime: "2024-10-10T20:10:00Z", + Type: transactiontype.MSP_ADD_USER_GROUPS_TO_TENANT, + Status: transactionstatus.IN_PROGRESS, + } + var doneTransaction = transaction.Type{ + TransactionUid: transactionUid, + TenantUid: uuid.New().String(), + EntityUid: managedTenantUid, + EntityUrl: "https://unittest.cdo.cisco.com/api/rest/v1/msp/tenants/" + managedTenantUid, + PollingUrl: "https://unittest.cdo.cisco.com/api/rest/v1/transactions/" + transactionUid, + SubmissionTime: "2024-09-10T20:10:00Z", + LastUpdatedTime: "2024-10-10T20:10:00Z", + Type: transactiontype.MSP_ADD_USER_GROUPS_TO_TENANT, + Status: transactionstatus.DONE, + } + + httpmock.RegisterResponder( + netHttp.MethodPost, + fmt.Sprintf("/api/rest/v1/msp/tenants/%s/users/groups", managedTenantUid), + httpmock.NewJsonResponderOrPanic(200, inProgressTransaction), + ) + httpmock.RegisterResponder( + netHttp.MethodGet, + inProgressTransaction.PollingUrl, + httpmock.NewJsonResponderOrPanic(200, doneTransaction), + ) + httpmock.RegisterResponder( + netHttp.MethodGet, + fmt.Sprintf("/api/rest/v1/msp/tenants/%s/users/groups?limit=200&offset=0", managedTenantUid), + httpmock.NewJsonResponderOrPanic(200, firstUserGroupPage), + ) + httpmock.RegisterResponder( + netHttp.MethodGet, + fmt.Sprintf("/api/rest/v1/msp/tenants/%s/users/groups?limit=200&offset=200", managedTenantUid), + httpmock.NewJsonResponderOrPanic(200, secondUserGroupPage), + ) + + actual, err := usergroups.Create(context.Background(), *http.MustNewWithConfig(baseUrl, "valid token", 0, 0, time.Minute), managedTenantUid, &createInp) + + assert.NotNil(t, actual, "Created user groups should have not been nil") + assert.Nil(t, err, "Created user groups operation should have not been an error") + sort.Slice(userGroupsWithIds, func(i, j int) bool { + return userGroupsWithIds[i].Uid < userGroupsWithIds[j].Uid + }) + sort.Slice(*actual, func(i, j int) bool { + return (*actual)[i].Uid < (*actual)[j].Uid + }) + assert.Equal(t, userGroupsWithIds, *actual, "Created users operation should have been the same as the created tenant") + }) + + t.Run("user group creation transaction fails", func(t *testing.T) { + httpmock.Reset() + var managedTenantUid = uuid.New().String() + var notes = "This is a group of developers" + var createInp = []usergroups.MspManagedUserGroupInput{ + { + GroupIdentifier: "developers", + IssuerUrl: "https://okta.com/123456", + Name: "Developers", + Role: "ROLE_ADMIN", + Notes: ¬es, + }, + { + GroupIdentifier: "managers", + IssuerUrl: "https://okta.com/123456", + Name: "Managers", + Role: "ROLE_READ_ONLY", + }, + } + var transactionUid = uuid.New().String() + var inProgressTransaction = transaction.Type{ + TransactionUid: transactionUid, + TenantUid: uuid.New().String(), + EntityUid: managedTenantUid, + EntityUrl: "https://unittest.cdo.cisco.com/api/rest/v1/msp/tenants/" + managedTenantUid, + PollingUrl: "https://unittest.cdo.cisco.com/api/rest/v1/transactions/" + transactionUid, + SubmissionTime: "2024-09-10T20:10:00Z", + LastUpdatedTime: "2024-10-10T20:10:00Z", + Type: transactiontype.MSP_ADD_USERS_TO_TENANT, + Status: transactionstatus.IN_PROGRESS, + } + var errorTransaction = transaction.Type{ + TransactionUid: transactionUid, + TenantUid: uuid.New().String(), + EntityUid: managedTenantUid, + EntityUrl: "https://unittest.cdo.cisco.com/api/rest/v1/msp/tenants/" + managedTenantUid, + PollingUrl: "https://unittest.cdo.cisco.com/api/rest/v1/transactions/" + transactionUid, + SubmissionTime: "2024-09-10T20:10:00Z", + LastUpdatedTime: "2024-10-10T20:10:00Z", + Type: transactiontype.MSP_ADD_USERS_TO_TENANT, + Status: transactionstatus.ERROR, + } + + httpmock.RegisterResponder( + netHttp.MethodPost, + fmt.Sprintf("/api/rest/v1/msp/tenants/%s/users/groups", managedTenantUid), + httpmock.NewJsonResponderOrPanic(200, inProgressTransaction), + ) + httpmock.RegisterResponder( + netHttp.MethodGet, + inProgressTransaction.PollingUrl, + httpmock.NewJsonResponderOrPanic(200, errorTransaction), + ) + + actual, err := usergroups.Create(context.Background(), *http.MustNewWithConfig(baseUrl, "valid token", 0, 0, time.Minute), managedTenantUid, &createInp) + + assert.Nil(t, actual, "Created user groups should be nil") + assert.NotNil(t, err, "Created user groups in tenant operation should have an error") + assert.Equal(t, usergroups.CreateError{ + Err: publicapi.NewTransactionErrorFromTransaction(errorTransaction), + CreatedResourceId: &managedTenantUid, + }, *err, "created transaction error does not match") + }) + + t.Run("user group creation API call fails with an error transaction", func(t *testing.T) { + httpmock.Reset() + var managedTenantUid = uuid.New().String() + var notes = "This is a group of developers" + var createInp = []usergroups.MspManagedUserGroupInput{ + { + GroupIdentifier: "developers", + IssuerUrl: "https://okta.com/123456", + Name: "Developers", + Role: "ROLE_ADMIN", + Notes: ¬es, + }, + { + GroupIdentifier: "managers", + IssuerUrl: "https://okta.com/123456", + Name: "Managers", + Role: "ROLE_READ_ONLY", + }, + } + var transactionUid = uuid.New().String() + var errorTransaction = transaction.Type{ + TransactionUid: transactionUid, + TenantUid: uuid.New().String(), + EntityUid: managedTenantUid, + EntityUrl: "https://unittest.cdo.cisco.com/api/rest/v1/msp/tenants/" + managedTenantUid, + PollingUrl: "https://unittest.cdo.cisco.com/api/rest/v1/transactions/" + transactionUid, + SubmissionTime: "2024-09-10T20:10:00Z", + LastUpdatedTime: "2024-10-10T20:10:00Z", + Type: transactiontype.MSP_ADD_USERS_TO_TENANT, + Status: transactionstatus.ERROR, + } + + httpmock.RegisterResponder( + netHttp.MethodPost, + fmt.Sprintf("/api/rest/v1/msp/tenants/%s/users/groups", managedTenantUid), + httpmock.NewJsonResponderOrPanic(200, errorTransaction), + ) + + actual, err := usergroups.Create(context.Background(), *http.MustNewWithConfig(baseUrl, "valid token", 0, 0, time.Minute), managedTenantUid, &createInp) + + assert.Nil(t, actual, "Created user groups should be nil") + assert.NotNil(t, err, "Created user groups in tenant operation should have an error") + var emptyCreatedResourceId = "" + assert.Equal(t, usergroups.CreateError{ + Err: publicapi.NewTransactionErrorFromTransaction(errorTransaction), + CreatedResourceId: &emptyCreatedResourceId, + }, *err, "created transaction error does not match") + }) +} diff --git a/client/msp/usergroups/delete.go b/client/msp/usergroups/delete.go new file mode 100644 index 0000000..5d9211d --- /dev/null +++ b/client/msp/usergroups/delete.go @@ -0,0 +1,35 @@ +package usergroups + +import ( + "context" + "fmt" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/http" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/publicapi" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/url" +) + +func Delete(ctx context.Context, client http.Client, tenantUid string, deleteInp *MspManagedUserGroupDeleteInput) (interface{}, error) { + client.Logger.Printf("Deleting %d user groups in %s\n", len(deleteInp.UserGroupUids), tenantUid) + deleteUrl := url.DeleteUserGroupsInMspManagedTenant(client.BaseUrl(), tenantUid) + transaction, err := publicapi.TriggerTransaction( + ctx, + client, + deleteUrl, + deleteInp, + ) + if err != nil { + return nil, err + } + + _, err = publicapi.WaitForTransactionToFinishWithDefaults( + ctx, + client, + transaction, + fmt.Sprintf("Waiting for user groups to be deleted from MSP-managed tenant %s...", tenantUid), + ) + if err != nil { + return nil, err + } + + return nil, nil +} diff --git a/client/msp/usergroups/delete_test.go b/client/msp/usergroups/delete_test.go new file mode 100644 index 0000000..83b0339 --- /dev/null +++ b/client/msp/usergroups/delete_test.go @@ -0,0 +1,142 @@ +package usergroups_test + +import ( + "context" + "fmt" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/http" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/publicapi/transaction" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/publicapi/transaction/transactionstatus" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/publicapi/transaction/transactiontype" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/msp/usergroups" + "github.com/google/uuid" + "github.com/jarcoal/httpmock" + "github.com/stretchr/testify/assert" + netHttp "net/http" + "testing" + "time" +) + +func TestDelete(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + t.Run("Should successfully delete user groups in MSP-managed tenant", func(t *testing.T) { + deleteInput := usergroups.MspManagedUserGroupDeleteInput{ + UserGroupUids: []string{uuid.New().String(), uuid.New().String()}, + } + managedTenantUid := uuid.New().String() + transactionUid := uuid.New().String() + var inProgressTransaction = transaction.Type{ + TransactionUid: transactionUid, + TenantUid: uuid.New().String(), + EntityUid: managedTenantUid, + EntityUrl: "https://unittest.cdo.cisco.com/api/rest/v1/msp/tenants/" + managedTenantUid, + PollingUrl: "https://unittest.cdo.cisco.com/api/rest/v1/transactions/" + transactionUid, + SubmissionTime: "2024-09-10T20:10:00Z", + LastUpdatedTime: "2024-10-10T20:10:00Z", + Type: transactiontype.MSP_DELETE_USER_GROUPS_FROM_TENANT, + Status: transactionstatus.IN_PROGRESS, + } + var doneTransaction = transaction.Type{ + TransactionUid: transactionUid, + TenantUid: uuid.New().String(), + EntityUid: managedTenantUid, + EntityUrl: "https://unittest.cdo.cisco.com/api/rest/v1/msp/tenants/" + managedTenantUid, + PollingUrl: "https://unittest.cdo.cisco.com/api/rest/v1/transactions/" + transactionUid, + SubmissionTime: "2024-09-10T20:10:00Z", + LastUpdatedTime: "2024-10-10T20:10:00Z", + Type: transactiontype.MSP_DELETE_USER_GROUPS_FROM_TENANT, + Status: transactionstatus.DONE, + } + + httpmock.RegisterResponder( + netHttp.MethodPost, + fmt.Sprintf("/api/rest/v1/msp/tenants/%s/users/groups/delete", managedTenantUid), + httpmock.NewJsonResponderOrPanic(200, inProgressTransaction), + ) + httpmock.RegisterResponder( + netHttp.MethodGet, + inProgressTransaction.PollingUrl, + httpmock.NewJsonResponderOrPanic(200, doneTransaction), + ) + + _, err := usergroups.Delete(context.Background(), *http.MustNewWithConfig(baseUrl, "valid_token", 0, 0, time.Minute), managedTenantUid, &deleteInput) + + assert.Nil(t, err) + }) + + t.Run("Should return error if deletion transaction fails", func(t *testing.T) { + deleteInput := usergroups.MspManagedUserGroupDeleteInput{ + UserGroupUids: []string{uuid.New().String(), uuid.New().String()}, + } + managedTenantUid := uuid.New().String() + transactionUid := uuid.New().String() + var inProgressTransaction = transaction.Type{ + TransactionUid: transactionUid, + TenantUid: uuid.New().String(), + EntityUid: managedTenantUid, + EntityUrl: "https://unittest.cdo.cisco.com/api/rest/v1/msp/tenants/" + managedTenantUid, + PollingUrl: "https://unittest.cdo.cisco.com/api/rest/v1/transactions/" + transactionUid, + SubmissionTime: "2024-09-10T20:10:00Z", + LastUpdatedTime: "2024-10-10T20:10:00Z", + Type: transactiontype.MSP_DELETE_USER_GROUPS_FROM_TENANT, + Status: transactionstatus.IN_PROGRESS, + } + var errorTransaction = transaction.Type{ + TransactionUid: transactionUid, + TenantUid: uuid.New().String(), + EntityUid: managedTenantUid, + EntityUrl: "https://unittest.cdo.cisco.com/api/rest/v1/msp/tenants/" + managedTenantUid, + PollingUrl: "https://unittest.cdo.cisco.com/api/rest/v1/transactions/" + transactionUid, + SubmissionTime: "2024-09-10T20:10:00Z", + LastUpdatedTime: "2024-10-10T20:10:00Z", + Type: transactiontype.MSP_DELETE_USER_GROUPS_FROM_TENANT, + Status: transactionstatus.ERROR, + } + + httpmock.RegisterResponder( + netHttp.MethodPost, + fmt.Sprintf("/api/rest/v1/msp/tenants/%s/users/groups/delete", managedTenantUid), + httpmock.NewJsonResponderOrPanic(200, inProgressTransaction), + ) + httpmock.RegisterResponder( + netHttp.MethodGet, + inProgressTransaction.PollingUrl, + httpmock.NewJsonResponderOrPanic(200, errorTransaction), + ) + + _, err := usergroups.Delete(context.Background(), *http.MustNewWithConfig(baseUrl, "valid_token", 0, 0, time.Minute), managedTenantUid, &deleteInput) + + assert.NotNil(t, err) + }) + + t.Run("Should return error if deletion API call fails", func(t *testing.T) { + deleteInput := usergroups.MspManagedUserGroupDeleteInput{ + UserGroupUids: []string{uuid.New().String(), uuid.New().String()}, + } + managedTenantUid := uuid.New().String() + transactionUid := uuid.New().String() + var errorTransaction = transaction.Type{ + TransactionUid: transactionUid, + TenantUid: uuid.New().String(), + EntityUid: managedTenantUid, + EntityUrl: "https://unittest.cdo.cisco.com/api/rest/v1/msp/tenants/" + managedTenantUid, + PollingUrl: "https://unittest.cdo.cisco.com/api/rest/v1/transactions/" + transactionUid, + SubmissionTime: "2024-09-10T20:10:00Z", + LastUpdatedTime: "2024-10-10T20:10:00Z", + Type: transactiontype.MSP_DELETE_USER_GROUPS_FROM_TENANT, + Status: transactionstatus.ERROR, + } + + httpmock.RegisterResponder( + netHttp.MethodPost, + fmt.Sprintf("/api/rest/v1/msp/tenants/%s/users/groups/delete", managedTenantUid), + httpmock.NewJsonResponderOrPanic(200, errorTransaction), + ) + + _, err := usergroups.Delete(context.Background(), *http.MustNewWithConfig(baseUrl, "valid_token", 0, 0, time.Minute), managedTenantUid, &deleteInput) + + assert.NotNil(t, err) + }) + +} diff --git a/client/msp/usergroups/models.go b/client/msp/usergroups/models.go new file mode 100644 index 0000000..b30e25e --- /dev/null +++ b/client/msp/usergroups/models.go @@ -0,0 +1,38 @@ +package usergroups + +type MspManagedUserGroupInput struct { + GroupIdentifier string `json:"groupIdentifier"` + IssuerUrl string `json:"issuerUrl"` + Name string `json:"name"` + Role string `json:"role"` + Notes *string `json:"notes"` +} + +type MspManagedUserGroup struct { + GroupIdentifier string `json:"groupIdentifier"` + IssuerUrl string `json:"issuerUrl"` + Name string `json:"name"` + Role string `json:"role"` + Notes *string `json:"notes"` + Uid string `json:"uid"` +} + +type MspManagedUserGroupPage struct { + Count int `json:"count"` + Offset int `json:"offset"` + Limit int `json:"limit"` + Items []MspManagedUserGroup `json:"items"` +} + +type MspManagedUserGroupDeleteInput struct { + UserGroupUids []string `json:"userGroupUids"` +} + +type CreateError struct { + Err error + CreatedResourceId *string +} + +func (r *CreateError) Error() string { + return r.Err.Error() +} diff --git a/client/msp/usergroups/read.go b/client/msp/usergroups/read.go new file mode 100644 index 0000000..05519e1 --- /dev/null +++ b/client/msp/usergroups/read.go @@ -0,0 +1,62 @@ +package usergroups + +import ( + "context" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/http" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/url" + mapset "github.com/deckarep/golang-set/v2" +) + +func ReadCreatedUserGroupsInTenant(ctx context.Context, client http.Client, tenantUid string, readInput *[]MspManagedUserGroupInput) (*[]MspManagedUserGroup, error) { + client.Logger.Printf("Reading user groups in tenant %s\n", tenantUid) + // create a map of the user groups that were created + // find the list of deleted users by removing from the list every time a user is found in the response + readUserGroupDetailsMap := map[string]MspManagedUserGroup{} + for _, createdUserGroup := range *readInput { + readUserGroupDetailsMap[createdUserGroup.GroupIdentifier] = MspManagedUserGroup{ + GroupIdentifier: createdUserGroup.GroupIdentifier, + IssuerUrl: createdUserGroup.IssuerUrl, + Role: createdUserGroup.Role, + Name: createdUserGroup.Name, + Notes: createdUserGroup.Notes, + } + } + + limit := 200 + offset := 0 + count := 1 + var readUrl string + var userGroupPage MspManagedUserGroupPage + foundUsernames := mapset.NewSet[string]() + + for count > offset { + client.Logger.Printf("Getting users from %d to %d\n", offset, offset+limit) + readUrl = url.GetUserGroupsInMspManagedTenant(client.BaseUrl(), tenantUid, limit, offset) + req := client.NewGet(ctx, readUrl) + if err := req.Send(&userGroupPage); err != nil { + return nil, err + } + for _, userGroup := range userGroupPage.Items { + // add userGroup to map if not present + if _, exists := readUserGroupDetailsMap[userGroup.GroupIdentifier]; exists { + client.Logger.Printf("Updating user group information for %v\n", userGroup) + readUserGroupDetailsMap[userGroup.GroupIdentifier] = userGroup + foundUsernames.Add(userGroup.GroupIdentifier) + } + } + + offset += limit + count = userGroupPage.Count + client.Logger.Printf("Got %d user groups in tenant %s\n", count, tenantUid) + } + + var readUserDetails []MspManagedUserGroup + for _, value := range readUserGroupDetailsMap { + // do not add in any users that were not found when we read from the API + if foundUsernames.Contains(value.GroupIdentifier) { + readUserDetails = append(readUserDetails, value) + } + } + + return &readUserDetails, nil +} diff --git a/docs/resources/msp_managed_tenant_user_groups.md b/docs/resources/msp_managed_tenant_user_groups.md new file mode 100644 index 0000000..e92213a --- /dev/null +++ b/docs/resources/msp_managed_tenant_user_groups.md @@ -0,0 +1,39 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "cdo_msp_managed_tenant_user_groups Resource - cdo" +subcategory: "" +description: |- + Provides a resource to add user groups to an MSP managed tenant. +--- + +# cdo_msp_managed_tenant_user_groups (Resource) + +Provides a resource to add user groups to an MSP managed tenant. + + + + +## Schema + +### Required + +- `tenant_uid` (String) Universally unique identifier of the tenant to which the user group should be added. +- `user_groups` (Attributes List) The list of user groups to be added to the tenant. You can add a maximum of 50 user groups at a time. (see [below for nested schema](#nestedatt--user_groups)) + + +### Nested Schema for `user_groups` + +Required: + +- `group_identifier` (String) The unique identifier of the user group in your Identity Provider (IdP) +- `issuer_url` (String) The Identity Provider (IdP) URL, which Security Cloud Control will use to validate SAML assertions during the sign-in process +- `name` (String) The name of the user group. Security Cloud Control does not support special characters for this field. +- `role` (String) The role assigned in Security Cloud Control to all users in this user group + +Optional: + +- `notes` (String) Any human-readable notes that are applicable to this user group + +Read-Only: + +- `id` (String) Universally unique identifier of the user group on Security Cloud Control diff --git a/provider/examples/resources/msp/usergroups/api_token.txt b/provider/examples/resources/msp/usergroups/api_token.txt new file mode 100644 index 0000000..6da4508 --- /dev/null +++ b/provider/examples/resources/msp/usergroups/api_token.txt @@ -0,0 +1 @@ +Paste your API token here \ No newline at end of file diff --git a/provider/examples/resources/msp/usergroups/main.tf b/provider/examples/resources/msp/usergroups/main.tf new file mode 100644 index 0000000..d9a5b63 --- /dev/null +++ b/provider/examples/resources/msp/usergroups/main.tf @@ -0,0 +1,30 @@ +data "cdo_msp_managed_tenant" "tenant" { + name = "CDO_tenant-name" +} + +resource "cdo_msp_managed_tenant_user_groups" "example" { + tenant_uid = data.cdo_msp_managed_tenant.tenant.id + user_groups = [ + { + group_identifier = "customer-managers" + issuer_url = "https://www.customer-idp.com" + name = "customer-managers" + notes = "Managers in customer's organization" + role = "ROLE_READ_ONLY" + }, + { + group_identifier = "msp-managers" + issuer_url = "https://www.msp-idp.com" + name = "msp-managers" + notes = "Managers in MSP organization" + role = "ROLE_READ_ONLY" + }, + { + group_identifier = "msp-developers" + issuer_url = "https://www.msp-idp.com" + name = "msp-developers" + # notes is an optional field, skipped + role = "ROLE_SUPER_ADMIN" + } + ] +} diff --git a/provider/examples/resources/msp/usergroups/providers.tf b/provider/examples/resources/msp/usergroups/providers.tf new file mode 100644 index 0000000..c020207 --- /dev/null +++ b/provider/examples/resources/msp/usergroups/providers.tf @@ -0,0 +1,12 @@ +terraform { + required_providers { + cdo = { + source = "CiscoDevnet/cdo" + } + } +} + +provider "cdo" { + base_url = "https://aus.manage.security.cisco.com" + api_token = file("${path.module}/api_token.txt") +} diff --git a/provider/internal/msp/msp_tenant/data_source.go b/provider/internal/msp/msp_tenant/data_source.go index a30b085..5909ce1 100644 --- a/provider/internal/msp/msp_tenant/data_source.go +++ b/provider/internal/msp/msp_tenant/data_source.go @@ -63,12 +63,12 @@ func (d *DataSource) Read(ctx context.Context, request datasource.ReadRequest, r response.Diagnostics.AddError("Failed to read MSP Managed Tenant", err.Error()) return } + tflog.Debug(ctx, fmt.Sprintf("Found %d MSP managed tenants by name %s", mspManagedTenants.Count, planData.Name.ValueString())) if mspManagedTenants.Count != 1 { response.Diagnostics.AddError("Cannot find MSP managed tenant by name "+planData.Name.ValueString(), fmt.Sprintf("Found %d tenants by name %s", mspManagedTenants.Count, planData.Name.ValueString())) + return } - tflog.Debug(ctx, fmt.Sprintf("fuckity shit: %v", mspManagedTenants)) - mspManagedTenant := mspManagedTenants.Items[0] planData.Id = types.StringValue(mspManagedTenant.Uid) planData.Name = types.StringValue(mspManagedTenant.Name) diff --git a/provider/internal/msp/msp_tenant_user_groups/models.go b/provider/internal/msp/msp_tenant_user_groups/models.go new file mode 100644 index 0000000..2b0ac92 --- /dev/null +++ b/provider/internal/msp/msp_tenant_user_groups/models.go @@ -0,0 +1,17 @@ +package msp_tenant_user_groups + +import "github.com/hashicorp/terraform-plugin-framework/types" + +type MspManagedTenantUserGroupsResourceModel struct { + TenantUid types.String `tfsdk:"tenant_uid"` + UserGroups []UserGroup `tfsdk:"user_groups"` +} + +type UserGroup struct { + Id types.String `tfsdk:"id"` + GroupIdentifier types.String `tfsdk:"group_identifier"` + IssuerUrl types.String `tfsdk:"issuer_url"` + Name types.String `tfsdk:"name"` + Role types.String `tfsdk:"role"` + Notes types.String `tfsdk:"notes"` +} diff --git a/provider/internal/msp/msp_tenant_user_groups/resource.go b/provider/internal/msp/msp_tenant_user_groups/resource.go new file mode 100644 index 0000000..1269189 --- /dev/null +++ b/provider/internal/msp/msp_tenant_user_groups/resource.go @@ -0,0 +1,215 @@ +package msp_tenant_user_groups + +import ( + "context" + "fmt" + cdoClient "github.com/CiscoDevnet/terraform-provider-cdo/go-client" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/msp/usergroups" + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-log/tflog" + "sort" +) + +func NewMspManagedTenantUserGroupsResource() resource.Resource { + return &MspManagedTenantUserGroupsResource{} +} + +type MspManagedTenantUserGroupsResource struct { + client *cdoClient.Client +} + +func sortUserGroupsToOrderInPlanData(users []UserGroup, planData *MspManagedTenantUserGroupsResourceModel) *[]UserGroup { + userOrder := make(map[string]int) + for i, user := range planData.UserGroups { + userOrder[user.GroupIdentifier.ValueString()] = i + } + + sort.Slice(users, func(i, j int) bool { + return userOrder[users[i].GroupIdentifier.ValueString()] < userOrder[users[j].GroupIdentifier.ValueString()] + }) + + return &users +} + +func (resource *MspManagedTenantUserGroupsResource) Metadata(ctx context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) { + response.TypeName = request.ProviderTypeName + "_msp_managed_tenant_user_groups" +} + +func (resource *MspManagedTenantUserGroupsResource) Schema(ctx context.Context, request resource.SchemaRequest, response *resource.SchemaResponse) { + response.Schema = schema.Schema{ + MarkdownDescription: "Provides a resource to add user groups to an MSP managed tenant.", + Attributes: map[string]schema.Attribute{ + "tenant_uid": schema.StringAttribute{ + MarkdownDescription: "Universally unique identifier of the tenant to which the user group should be added.", + Required: true, + }, + "user_groups": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: "Universally unique identifier of the user group on Security Cloud Control", + Computed: true, + }, + "group_identifier": schema.StringAttribute{ + MarkdownDescription: "The unique identifier of the user group in your Identity Provider (IdP)", + Required: true, + }, + "issuer_url": schema.StringAttribute{ + MarkdownDescription: "The Identity Provider (IdP) URL, which Security Cloud Control will use to validate SAML assertions during the sign-in process", + Required: true, + }, + "name": schema.StringAttribute{ + MarkdownDescription: "The name of the user group. Security Cloud Control does not support special characters for this field.", + Required: true, + }, + "notes": schema.StringAttribute{ + MarkdownDescription: "Any human-readable notes that are applicable to this user group", + Optional: true, + }, + "role": schema.StringAttribute{ + MarkdownDescription: "The role assigned in Security Cloud Control to all users in this user group", + Required: true, + Validators: []validator.String{stringvalidator.OneOf("ROLE_READ_ONLY", "ROLE_ADMIN", "ROLE_SUPER_ADMIN", "ROLE_DEPLOY_ONLY", "ROLE_EDIT_ONLY", "ROLE_VPN_SESSIONS_MANAGER")}, + }, + }, + PlanModifiers: []planmodifier.Object{ + objectplanmodifier.RequiresReplace(), + }, + }, + MarkdownDescription: "The list of user groups to be added to the tenant. You can add a maximum of 50 user groups at a time.", + Required: true, + Validators: []validator.List{ + listvalidator.SizeAtLeast(1), + }, + }, + }, + } +} + +func (resource *MspManagedTenantUserGroupsResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { + var planData MspManagedTenantUserGroupsResourceModel + response.Diagnostics.Append(request.Plan.Get(ctx, &planData)...) + if response.Diagnostics.HasError() { + tflog.Error(ctx, "Failed to parse plan data") + return + } + tflog.Debug(ctx, fmt.Sprintf("Adding user-group %v to MSSP-managed CDO tenant", planData)) + createdUserGroups, err := resource.client.CreateUserGroupsInMspManagedTenant(ctx, planData.TenantUid.ValueString(), resource.buildMspUserGroupInput(&planData)) + if err != nil { + response.Diagnostics.AddError("Failed to create user group: %v", err.Error()) + return + } + + planData.UserGroups = *sortUserGroupsToOrderInPlanData(*resource.transformApiResponseToPlan(createdUserGroups), &planData) + + response.Diagnostics.Append(response.State.Set(ctx, &planData)...) +} + +func (resource *MspManagedTenantUserGroupsResource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) { + tflog.Debug(ctx, "Reading user groups from MSP-managed CDO tenant") + var stateData MspManagedTenantUserGroupsResourceModel + response.Diagnostics.Append(request.State.Get(ctx, &stateData)...) + userGroupDetails, err := resource.client.ReadUserGroupsInMspManagedTenant(ctx, stateData.TenantUid.ValueString(), resource.buildMspUserGroupInput(&stateData)) + if err != nil { + response.Diagnostics.AddError("failed to read users in MSP-managed tenant", err.Error()) + return + } + stateData.UserGroups = *sortUserGroupsToOrderInPlanData(*resource.transformApiResponseToPlan(userGroupDetails), &stateData) + response.Diagnostics.Append(response.State.Set(ctx, &stateData)...) +} + +func (resource *MspManagedTenantUserGroupsResource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) { +} + +func (resource *MspManagedTenantUserGroupsResource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) { + tflog.Debug(ctx, "Deleting user groups from MSP-managed CDO tenant") + var stateData MspManagedTenantUserGroupsResourceModel + response.Diagnostics.Append(request.State.Get(ctx, &stateData)...) + if response.Diagnostics.HasError() { + return + } + _, err := resource.deleteAllUserGroupsInState(ctx, &stateData) + if err != nil { + response.Diagnostics.AddError("failed to delete users", err.Error()) + } + + stateData.UserGroups = []UserGroup{} + response.Diagnostics.Append(response.State.Set(ctx, &stateData)...) +} + +func (resource *MspManagedTenantUserGroupsResource) Configure(ctx context.Context, req resource.ConfigureRequest, res *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*cdoClient.Client) + + if !ok { + res.Diagnostics.AddError( + "Unexpected Data Source Configure Type", + fmt.Sprintf("Expected *cdoClient.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + + return + } + + resource.client = client +} + +func (resource *MspManagedTenantUserGroupsResource) buildMspUserGroupInput(planData *MspManagedTenantUserGroupsResourceModel) *[]usergroups.MspManagedUserGroupInput { + var userGroupCreateOrUpdateInput []usergroups.MspManagedUserGroupInput + for _, userGroup := range planData.UserGroups { + var notes *string + if !userGroup.Notes.IsNull() { + notes = userGroup.Notes.ValueStringPointer() + } + userGroupCreateOrUpdateInput = append(userGroupCreateOrUpdateInput, usergroups.MspManagedUserGroupInput{ + GroupIdentifier: userGroup.GroupIdentifier.ValueString(), + IssuerUrl: userGroup.IssuerUrl.ValueString(), + Name: userGroup.Name.ValueString(), + Role: userGroup.Role.ValueString(), + Notes: notes, + }) + } + + return &userGroupCreateOrUpdateInput +} + +func (resource *MspManagedTenantUserGroupsResource) transformApiResponseToPlan(createdUserGroupDetails *[]usergroups.MspManagedUserGroup) *[]UserGroup { + var userGroups []UserGroup + for _, userGroupDetails := range *createdUserGroupDetails { + var notes basetypes.StringValue + if userGroupDetails.Notes != nil { + notes = types.StringValue(*userGroupDetails.Notes) + } + userGroups = append(userGroups, UserGroup{ + Id: types.StringValue(userGroupDetails.Uid), + GroupIdentifier: types.StringValue(userGroupDetails.GroupIdentifier), + IssuerUrl: types.StringValue(userGroupDetails.IssuerUrl), + Name: types.StringValue(userGroupDetails.Name), + Role: types.StringValue(userGroupDetails.Role), + Notes: notes, + }) + } + + return &userGroups +} + +func (resource *MspManagedTenantUserGroupsResource) deleteAllUserGroupsInState(ctx context.Context, stateData *MspManagedTenantUserGroupsResourceModel) (interface{}, error) { + var userGroupUids []string + for _, user := range stateData.UserGroups { + userGroupUids = append(userGroupUids, user.Id.ValueString()) + } + deleteInput := usergroups.MspManagedUserGroupDeleteInput{ + UserGroupUids: userGroupUids, + } + return resource.client.DeleteUserGroupsInMspManagedTenant(ctx, stateData.TenantUid.ValueString(), &deleteInput) +} diff --git a/provider/internal/msp/msp_tenant_user_groups/resource_test.go b/provider/internal/msp/msp_tenant_user_groups/resource_test.go new file mode 100644 index 0000000..eafab1e --- /dev/null +++ b/provider/internal/msp/msp_tenant_user_groups/resource_test.go @@ -0,0 +1,101 @@ +package msp_tenant_user_groups_test + +import ( + "github.com/CiscoDevnet/terraform-provider-cdo/internal/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "testing" +) + +type UserGroup struct { + GroupIdentifier string + IssuerUrl string + Name string + Role string + Notes string +} + +var testMspManagedTenantUserGroupsResource = struct { + TenantUid string + UserGroups []UserGroup +}{ + UserGroups: []UserGroup{ + {GroupIdentifier: "customer-managers", IssuerUrl: "https://www.customer-idp.com", Name: "developers", Role: "ROLE_SUPER_ADMIN", Notes: "Managers in customer's organization"}, + {GroupIdentifier: "msp-managers", IssuerUrl: "https://www.msp-idp.com", Name: "managers", Role: "ROLE_ADMIN", Notes: "Managers in MSP organization"}, + }, + TenantUid: acctest.Env.MspTenantId(), +} + +const testMspManagedTenantUserGroupsTemplate = ` +resource "cdo_msp_managed_tenant_user_groups" "test" { + tenant_uid = "{{.TenantUid}}" + user_groups = [ + { + "group_identifier": "{{(index .UserGroups 0).GroupIdentifier }}" + "issuer_url": "{{ (index .UserGroups 0).IssuerUrl }}" + "name": "{{ (index .UserGroups 0).Name }}" + "role": "{{ (index .UserGroups 0).Role }}" + "notes": "{{ (index .UserGroups 0).Notes }}" + }, + { + "group_identifier": "{{(index .UserGroups 1).GroupIdentifier }}" + "issuer_url": "{{ (index .UserGroups 1).IssuerUrl }}" + "name": "{{ (index .UserGroups 1).Name }}" + "role": "{{ (index .UserGroups 1).Role }}" + "notes": "{{ (index .UserGroups 1).Notes }}" + } + ] +}` + +var testMspManagedTenantUserGroupsResourceConfig = acctest.MustParseTemplate(testMspManagedTenantUserGroupsTemplate, testMspManagedTenantUserGroupsResource) + +func TestAccMspManagedTenantUserGroupsResource(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: acctest.PreCheckFunc(t), + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Read testing + { + Config: acctest.MspProviderConfig() + testMspManagedTenantUserGroupsResourceConfig, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("cdo_msp_managed_tenant_user_groups.test", + "tenant_uid", + testMspManagedTenantUserGroupsResource.TenantUid), + resource.TestCheckResourceAttr("cdo_msp_managed_tenant_user_groups.test", + "user_groups.0.group_identifier", + testMspManagedTenantUserGroupsResource.UserGroups[0].GroupIdentifier), + resource.TestCheckResourceAttr("cdo_msp_managed_tenant_user_groups.test", + "user_groups.0.issuer_url", + testMspManagedTenantUserGroupsResource.UserGroups[0].IssuerUrl), + resource.TestCheckResourceAttr("cdo_msp_managed_tenant_user_groups.test", + "user_groups.0.name", + testMspManagedTenantUserGroupsResource.UserGroups[0].Name), + resource.TestCheckResourceAttr("cdo_msp_managed_tenant_user_groups.test", + "user_groups.0.notes", + testMspManagedTenantUserGroupsResource.UserGroups[0].Notes), + resource.TestCheckResourceAttr( + "cdo_msp_managed_tenant_user_groups.test", + "user_groups.0.role", + testMspManagedTenantUserGroupsResource.UserGroups[0].Role, + ), + resource.TestCheckResourceAttr("cdo_msp_managed_tenant_user_groups.test", + "user_groups.1.group_identifier", + testMspManagedTenantUserGroupsResource.UserGroups[1].GroupIdentifier), + resource.TestCheckResourceAttr("cdo_msp_managed_tenant_user_groups.test", + "user_groups.1.issuer_url", + testMspManagedTenantUserGroupsResource.UserGroups[1].IssuerUrl), + resource.TestCheckResourceAttr("cdo_msp_managed_tenant_user_groups.test", + "user_groups.1.name", + testMspManagedTenantUserGroupsResource.UserGroups[1].Name), + resource.TestCheckResourceAttr("cdo_msp_managed_tenant_user_groups.test", + "user_groups.1.notes", + testMspManagedTenantUserGroupsResource.UserGroups[1].Notes), + resource.TestCheckResourceAttr( + "cdo_msp_managed_tenant_user_groups.test", + "user_groups.1.role", + testMspManagedTenantUserGroupsResource.UserGroups[1].Role, + ), + ), + }, + }, + }) +} diff --git a/provider/internal/msp/msp_tenant_users/resource.go b/provider/internal/msp/msp_tenant_users/resource.go index 6bec76c..c9eeccb 100644 --- a/provider/internal/msp/msp_tenant_users/resource.go +++ b/provider/internal/msp/msp_tenant_users/resource.go @@ -78,6 +78,9 @@ func (resource *MspManagedTenantUsersResource) Schema(ctx context.Context, reque }, MarkdownDescription: "The list of users to be added to the tenant. You can add a maximum of 50 users at a time.", Required: true, + Validators: []validator.List{ + listvalidator.SizeAtLeast(1), + }, }, }, } diff --git a/provider/internal/provider/provider.go b/provider/internal/provider/provider.go index 633b151..c90f000 100644 --- a/provider/internal/provider/provider.go +++ b/provider/internal/provider/provider.go @@ -8,6 +8,7 @@ import ( "fmt" "github.com/CiscoDevnet/terraform-provider-cdo/internal/msp/msp_tenant" "github.com/CiscoDevnet/terraform-provider-cdo/internal/msp/msp_tenant_user_api_token" + "github.com/CiscoDevnet/terraform-provider-cdo/internal/msp/msp_tenant_user_groups" "github.com/CiscoDevnet/terraform-provider-cdo/internal/msp/msp_tenant_users" "os" @@ -178,6 +179,7 @@ func (p *CdoProvider) Resources(ctx context.Context) []func() resource.Resource msp_tenant.NewTenantResource, msp_tenant_users.NewMspManagedTenantUsersResource, msp_tenant_user_api_token.NewMspManagedTenantUserApiTokenResource, + msp_tenant_user_groups.NewMspManagedTenantUserGroupsResource, } }