Skip to content

Commit

Permalink
feat(lh-86968): add terraform resource to add user groups to MSP-mana…
Browse files Browse the repository at this point in the history
…ged tenant (#152)

* feat(lh-86968): add terraform resource to add user groups to MSP-managed tenant

Allow an MSP admin to use terraform to add user groups to a tenant managed by the MSP portal. TODO:
acceptance tests

* test(lh-86968): add acceptance test
  • Loading branch information
siddhuwarrier authored Nov 19, 2024
1 parent 09f3ed5 commit 688b711
Show file tree
Hide file tree
Showing 21 changed files with 1,045 additions and 15 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
.idea
provider/.github-action.local.env
provider/.github-action.local.envprovider/terraform-provider-cdo
13 changes: 13 additions & 0 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
12 changes: 12 additions & 0 deletions client/internal/url/url.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
5 changes: 5 additions & 0 deletions client/msp/usergroups/constants_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package usergroups_test

const (
baseUrl = "https://unittest.cdo.cisco.com"
)
43 changes: 43 additions & 0 deletions client/msp/usergroups/create.go
Original file line number Diff line number Diff line change
@@ -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
}
258 changes: 258 additions & 0 deletions client/msp/usergroups/create_test.go
Original file line number Diff line number Diff line change
@@ -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: &notes,
})
}
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: &notes,
},
{
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: &notes,
},
{
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: &notes,
},
{
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")
})
}
Loading

0 comments on commit 688b711

Please sign in to comment.