Skip to content

Commit

Permalink
feat(lh-86969): add the ability to read users from an MSP tenant when…
Browse files Browse the repository at this point in the history
… creating (#148)

* feat(lh-86969): add the ability to read users from an MSP tenant when creating

This commit reads the users in an MSP-managed tenant after creating, and before deleting or updating
them.

BREAKING CHANGE: The `role` field has been changed to a list field called `roles`.

* chore(lh-86969): clean up example
  • Loading branch information
siddhuwarrier authored Nov 1, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent 1022180 commit 978abd0
Showing 16 changed files with 292 additions and 54 deletions.
6 changes: 5 additions & 1 deletion client/client.go
Original file line number Diff line number Diff line change
@@ -292,10 +292,14 @@ func (c *Client) FindMspManagedTenantByName(ctx context.Context, readByNameInput
return tenants.ReadByName(ctx, c.client, readByNameInput)
}

func (c *Client) CreateUsersInMspManagedTenant(ctx context.Context, createInput users.MspCreateUsersInput) (*[]users.UserDetails, *users.CreateError) {
func (c *Client) CreateUsersInMspManagedTenant(ctx context.Context, createInput users.MspUsersInput) (*[]users.UserDetails, *users.CreateError) {
return users.Create(ctx, c.client, createInput)
}

func (c *Client) ReadUsersInMspManagedTenant(ctx context.Context, readInput users.MspUsersInput) (*[]users.UserDetails, error) {
return users.ReadCreatedUsersInTenant(ctx, c.client, readInput)
}

func (c *Client) DeleteUsersInMspManagedTenant(ctx context.Context, deleteInput users.MspDeleteUsersInput) (interface{}, error) {
return users.Delete(ctx, c.client, deleteInput)
}
4 changes: 4 additions & 0 deletions client/internal/url/url.go
Original file line number Diff line number Diff line change
@@ -222,6 +222,10 @@ func CreateUsersInMspManagedTenant(baseUrl string, tenantUid string) string {
return fmt.Sprintf("%s/api/rest/v1/msp/tenants/%s/users", baseUrl, tenantUid)
}

func GetUsersInMspManagedTenant(baseUrl string, tenantUid string, limit int, offset int) string {
return fmt.Sprintf("%s/api/rest/v1/msp/tenants/%s/users?limit=%d&offset=%d", baseUrl, tenantUid, limit, offset)
}

func DeleteUsersInMspManagedTenant(baseUrl string, tenantUid string) string {
return fmt.Sprintf("%s/api/rest/v1/msp/tenants/%s/users/delete", baseUrl, tenantUid)
}
25 changes: 22 additions & 3 deletions client/msp/users/create.go
Original file line number Diff line number Diff line change
@@ -8,14 +8,25 @@ import (
"github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/url"
)

func Create(ctx context.Context, client http.Client, createInp MspCreateUsersInput) (*[]UserDetails, *CreateError) {
func Create(ctx context.Context, client http.Client, createInp MspUsersInput) (*[]UserDetails, *CreateError) {
client.Logger.Printf("Creating %d users in %s\n", len(createInp.Users), createInp.TenantUid)
createUrl := url.CreateUsersInMspManagedTenant(client.BaseUrl(), createInp.TenantUid)
var userDetailsPublicApiInput []UserDetailsPublicApiInput
for _, user := range createInp.Users {
userDetailsPublicApiInput = append(userDetailsPublicApiInput, UserDetailsPublicApiInput{
Username: user.Username,
Role: user.Roles[0],
ApiOnlyUser: user.ApiOnlyUser,
})
}
transaction, err := publicapi.TriggerTransaction(
ctx,
client,
createUrl,
createInp,
MspUsersPublicApiInput{
TenantUid: createInp.TenantUid,
Users: userDetailsPublicApiInput,
},
)
if err != nil {
return nil, &CreateError{
@@ -36,5 +47,13 @@ func Create(ctx context.Context, client http.Client, createInp MspCreateUsersInp
}
}

return &createInp.Users, nil
readUserDetrails, err := ReadCreatedUsersInTenant(ctx, client, createInp)
if err != nil {
client.Logger.Println("Failed to read users from tenant after creation")
return nil, &CreateError{
Err: err,
CreatedResourceId: &transaction.EntityUid,
}
}
return readUserDetrails, nil
}
66 changes: 56 additions & 10 deletions client/msp/users/create_test.go
Original file line number Diff line number Diff line change
@@ -14,24 +14,60 @@ import (
"github.com/jarcoal/httpmock"
"github.com/stretchr/testify/assert"
netHttp "net/http"
"strconv"
"testing"
"time"
)

// Function to generate users
func generateUsers(num int) []users.UserDetails {
var createdUsers []users.UserDetails
for i := 1; i <= num; i++ {
uid := "uid" + strconv.Itoa(i) // Generate unique UID
username := "user" + strconv.Itoa(i) // Generate usernames like user1, user2, etc.
roles := []string{"ROLE_USER"} // Assign a default role; you can modify this as needed
apiOnlyUser := i%2 == 0 // Example: alternate between true/false for ApiOnlyUser

createdUsers = append(createdUsers, users.UserDetails{
Uid: uid,
Username: username,
Roles: roles,
ApiOnlyUser: apiOnlyUser,
})
}
return createdUsers
}

// the create test also tests read!
func TestCreate(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()

t.Run("successfully create users in MSP-managed tenant", func(t *testing.T) {
httpmock.Reset()
var managedTenantUid = uuid.New().String()
var createInp = users.MspCreateUsersInput{
var createInp = users.MspUsersInput{
TenantUid: managedTenantUid,
Users: []users.UserDetails{
{Username: "[email protected]", Role: string(role.SuperAdmin), ApiOnlyUser: false},
{Username: "api-only-user", Role: string(role.ReadOnly), ApiOnlyUser: true},
{Username: "[email protected]", Roles: []string{string(role.SuperAdmin)}, ApiOnlyUser: false},
{Username: "api-only-user", Roles: []string{string(role.ReadOnly)}, ApiOnlyUser: true},
},
}

var usersInCdoTenant = generateUsers(250)
var usersWithIds []users.UserDetails
for _, user := range createInp.Users {
userWithId := users.UserDetails{
Uid: uuid.New().String(),
Username: user.Username,
Roles: user.Roles,
ApiOnlyUser: user.ApiOnlyUser,
}
usersInCdoTenant = append(usersInCdoTenant, userWithId)
usersWithIds = append(usersWithIds, userWithId)
}
firstUserPage := users.UserPage{Items: usersInCdoTenant[:200], Count: len(usersInCdoTenant), Limit: 200, Offset: 0}
secondUserPage := users.UserPage{Items: usersInCdoTenant[200:], Count: len(usersInCdoTenant), Limit: 200, Offset: 200}
var transactionUid = uuid.New().String()
var inProgressTransaction = transaction.Type{
TransactionUid: transactionUid,
@@ -66,22 +102,32 @@ func TestCreate(t *testing.T) {
inProgressTransaction.PollingUrl,
httpmock.NewJsonResponderOrPanic(200, doneTransaction),
)
httpmock.RegisterResponder(
netHttp.MethodGet,
fmt.Sprintf("/api/rest/v1/msp/tenants/%s/users?limit=200&offset=0", managedTenantUid),
httpmock.NewJsonResponderOrPanic(200, firstUserPage),
)
httpmock.RegisterResponder(
netHttp.MethodGet,
fmt.Sprintf("/api/rest/v1/msp/tenants/%s/users?limit=200&offset=200", managedTenantUid),
httpmock.NewJsonResponderOrPanic(200, secondUserPage),
)

actual, err := users.Create(context.Background(), *http.MustNewWithConfig(baseUrl, "valid_token", 0, 0, time.Minute), createInp)

assert.NotNil(t, actual, "Created users should have not been nil")
assert.Nil(t, err, "Created users operation should have not been an error")
assert.Equal(t, createInp.Users, *actual, "Created users operation should have been the same as the created tenant")
assert.Equal(t, usersWithIds, *actual, "Created users operation should have been the same as the created tenant")
})

t.Run("user creation transaction fails", func(t *testing.T) {
httpmock.Reset()
var managedTenantUid = uuid.New().String()
var createInp = users.MspCreateUsersInput{
var createInp = users.MspUsersInput{
TenantUid: managedTenantUid,
Users: []users.UserDetails{
{Username: "[email protected]", Role: string(role.SuperAdmin), ApiOnlyUser: false},
{Username: "api-only-user", Role: string(role.ReadOnly), ApiOnlyUser: true},
{Username: "[email protected]", Roles: []string{string(role.SuperAdmin)}, ApiOnlyUser: false},
{Username: "api-only-user", Roles: []string{string(role.ReadOnly)}, ApiOnlyUser: true},
},
}
var transactionUid = uuid.New().String()
@@ -132,11 +178,11 @@ func TestCreate(t *testing.T) {
t.Run("user creation API call fails", func(t *testing.T) {
httpmock.Reset()
var managedTenantUid = uuid.New().String()
var createInp = users.MspCreateUsersInput{
var createInp = users.MspUsersInput{
TenantUid: managedTenantUid,
Users: []users.UserDetails{
{Username: "[email protected]", Role: string(role.SuperAdmin), ApiOnlyUser: false},
{Username: "api-only-user", Role: string(role.ReadOnly), ApiOnlyUser: true},
{Username: "[email protected]", Roles: []string{string(role.SuperAdmin)}, ApiOnlyUser: false},
{Username: "api-only-user", Roles: []string{string(role.ReadOnly)}, ApiOnlyUser: true},
},
}
var transactionUid = uuid.New().String()
28 changes: 24 additions & 4 deletions client/msp/users/models.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,39 @@
package users

type MspCreateUsersInput struct {
type MspUsersInput struct {
TenantUid string `json:"tenantUid"`
Users []UserDetails `json:"users"`
}

type MspUsersPublicApiInput struct {
TenantUid string `json:"tenantUid"`
Users []UserDetailsPublicApiInput `json:"users"`
}

type UserDetailsPublicApiInput struct {
Uid string `json:"uid"`
Username string `json:"username"`
Role string `json:"role"`
ApiOnlyUser bool `json:"apiOnlyUser"`
}

type MspDeleteUsersInput struct {
TenantUid string `json:"tenantUid"`
Usernames []string `json:"usernames"`
}

type UserDetails struct {
Username string `json:"username"`
Role string `json:"role"`
ApiOnlyUser bool `json:"apiOnlyUser"`
Uid string `json:"uid"`
Username string `json:"name"`
Roles []string `json:"roles"`
ApiOnlyUser bool `json:"apiOnlyUser"`
}

type UserPage struct {
Count int `json:"count"`
Offset int `json:"offset"`
Limit int `json:"limit"`
Items []UserDetails `json:"items"`
}

type CreateError struct {
57 changes: 57 additions & 0 deletions client/msp/users/read.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package users

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 ReadCreatedUsersInTenant(ctx context.Context, client http.Client, readInput MspUsersInput) (*[]UserDetails, error) {
client.Logger.Printf("Reading users in tenant %s\n", readInput.TenantUid)

// create a map of the users that were created
// find the list of deleted users by removing from the list every time a user is found in the response
readUserDetailsMap := map[string]UserDetails{}
for _, createdUser := range readInput.Users {
readUserDetailsMap[createdUser.Username] = createdUser
}

limit := 200
offset := 0
count := 1
var readUrl string
var userPage UserPage
foundUsernames := mapset.NewSet[string]()

for count > offset {
client.Logger.Printf("Getting users from %d to %d\n", offset, offset+limit)
readUrl = url.GetUsersInMspManagedTenant(client.BaseUrl(), readInput.TenantUid, limit, offset)
req := client.NewGet(ctx, readUrl)
if err := req.Send(&userPage); err != nil {
return nil, err
}
for _, user := range userPage.Items {
// add user to map if not present
if _, exists := readUserDetailsMap[user.Username]; exists {
client.Logger.Printf("Updating user information for %v\n", user)
readUserDetailsMap[user.Username] = user
foundUsernames.Add(user.Username)
}
}

offset += limit
count = userPage.Count
client.Logger.Printf("Got %d users in tenant %s\n", count, readInput.TenantUid)
}

var readUserDetails []UserDetails
for _, value := range readUserDetailsMap {
// do not add in any users that were not found when we read from the API
if foundUsernames.Contains(value.Username) {
readUserDetails = append(readUserDetails, value)
}
}

return &readUserDetails, nil
}
2 changes: 1 addition & 1 deletion docs/data-sources/user.md
Original file line number Diff line number Diff line change
@@ -23,4 +23,4 @@ Use this data source to get the identifiers of users to be referenced elsewhere,

- `id` (String) Universally unique identifier for the user.
- `is_api_only_user` (Boolean) CDO has two kinds of users: actual users with email addresses and API-only users for programmatic access. This boolean indicates what type of user this is.
- `role` (String) Role assigned to the user in this tenant.
- `role` (String) Roles assigned to the user in this tenant.
6 changes: 5 additions & 1 deletion docs/resources/msp_managed_tenant_users.md
Original file line number Diff line number Diff line change
@@ -26,5 +26,9 @@ Provides a resource to add users to an MSP managed tenant.
Required:

- `api_only_user` (Boolean) Whether the user is an API-only user
- `role` (String) The role to assign to the user in the CDO tenant.
- `roles` (List of String) The roles to assign to the user in the CDO tenant. Note: this list can only contain one entry.
- `username` (String) The name of the user in CDO. This must be a valid e-mail address if the user is not an API-only user.

Read-Only:

- `id` (String) Universally unique identifier of the user
6 changes: 3 additions & 3 deletions provider/examples/resources/msp/users/main.tf
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
data "cdo_msp_managed_tenant" "tenant" {
name = "CDO_tenant-name"
name = "CDO_test-tenant-name"
}

resource "cdo_msp_managed_tenant_users" "example" {
tenant_uid = data.cdo_msp_managed_tenant.tenant.id
users = [
{
username = "[email protected]",
role = "ROLE_SUPER_ADMIN"
roles = ["ROLE_SUPER_ADMIN"]
api_only_user = false
},
{
username = "[email protected]",
role = "ROLE_ADMIN"
roles = ["ROLE_ADMIN"]
api_only_user = false
}
]
1 change: 1 addition & 0 deletions provider/go.mod
Original file line number Diff line number Diff line change
@@ -32,6 +32,7 @@ require (
github.com/aws/smithy-go v1.16.0 // indirect
github.com/bgentry/speakeasy v0.1.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/deckarep/golang-set/v2 v2.6.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/cli v1.1.6 // indirect
github.com/hashicorp/terraform-plugin-docs v0.18.0 // indirect
2 changes: 2 additions & 0 deletions provider/go.sum
Original file line number Diff line number Diff line change
@@ -58,6 +58,8 @@ github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53E
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM=
github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
8 changes: 8 additions & 0 deletions provider/internal/acctest/helper.go
Original file line number Diff line number Diff line change
@@ -20,6 +20,14 @@ func MustParseTemplate(tmpl string, obj any) string {
return buf.String()
}

func MustParseTemplateWithFuncMap(tmpl string, obj any, funcMap template.FuncMap) string {
buf := bytes.Buffer{}
if err := template.Must(template.New("").Funcs(funcMap).Parse(tmpl)).Execute(&buf, obj); err != nil {
panic(err)
}
return buf.String()
}

func MustOverrideFields[K any](obj K, fields map[string]any) K {
copyObj := obj
copyValue := reflect.ValueOf(&copyObj).Elem()
3 changes: 2 additions & 1 deletion provider/internal/msp/msp_tenant_users/models.go
Original file line number Diff line number Diff line change
@@ -8,7 +8,8 @@ type MspManagedTenantUsersResourceModel struct {
}

type User struct {
Id types.String `tfsdk:"id"`
Username types.String `tfsdk:"username"`
Role types.String `tfsdk:"role"`
Roles types.List `tfsdk:"roles"`
ApiOnlyUser types.Bool `tfsdk:"api_only_user"`
}
Loading

0 comments on commit 978abd0

Please sign in to comment.