From 1d2b669ff1250ab97267b388d6d247c538fb8f0e Mon Sep 17 00:00:00 2001 From: Siddhu Warrier Date: Fri, 1 Nov 2024 16:33:43 +0000 Subject: [PATCH 01/10] 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`. --- client/client.go | 6 +- client/internal/url/url.go | 4 + client/msp/users/create.go | 25 ++++- client/msp/users/create_test.go | 66 +++++++++++-- client/msp/users/models.go | 28 +++++- client/msp/users/read.go | 57 ++++++++++++ docs/data-sources/user.md | 2 +- docs/resources/msp_managed_tenant_users.md | 6 +- .../resources/msp/users/api_token.txt | 2 +- provider/examples/resources/msp/users/main.tf | 6 +- .../examples/resources/msp/users/providers.tf | 2 +- provider/go.mod | 1 + provider/go.sum | 2 + provider/internal/acctest/helper.go | 8 ++ .../internal/msp/msp_tenant_users/models.go | 3 +- .../internal/msp/msp_tenant_users/resource.go | 93 ++++++++++++++----- .../msp/msp_tenant_users/resource_test.go | 37 ++++++-- provider/internal/user/data_source.go | 2 +- 18 files changed, 294 insertions(+), 56 deletions(-) create mode 100644 client/msp/users/read.go diff --git a/client/client.go b/client/client.go index 2e5c64fc..4e706688 100644 --- a/client/client.go +++ b/client/client.go @@ -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) } diff --git a/client/internal/url/url.go b/client/internal/url/url.go index 41f9e815..005d2029 100644 --- a/client/internal/url/url.go +++ b/client/internal/url/url.go @@ -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) } diff --git a/client/msp/users/create.go b/client/msp/users/create.go index d7793864..a74b758d 100644 --- a/client/msp/users/create.go +++ b/client/msp/users/create.go @@ -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 } diff --git a/client/msp/users/create_test.go b/client/msp/users/create_test.go index a05d76a9..819eb97a 100644 --- a/client/msp/users/create_test.go +++ b/client/msp/users/create_test.go @@ -14,10 +14,31 @@ 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() @@ -25,13 +46,28 @@ func TestCreate(t *testing.T) { 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: "apples@bananas.com", Role: string(role.SuperAdmin), ApiOnlyUser: false}, - {Username: "api-only-user", Role: string(role.ReadOnly), ApiOnlyUser: true}, + {Username: "apples@bananas.com", 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: "apples@bananas.com", Role: string(role.SuperAdmin), ApiOnlyUser: false}, - {Username: "api-only-user", Role: string(role.ReadOnly), ApiOnlyUser: true}, + {Username: "apples@bananas.com", 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: "apples@bananas.com", Role: string(role.SuperAdmin), ApiOnlyUser: false}, - {Username: "api-only-user", Role: string(role.ReadOnly), ApiOnlyUser: true}, + {Username: "apples@bananas.com", Roles: []string{string(role.SuperAdmin)}, ApiOnlyUser: false}, + {Username: "api-only-user", Roles: []string{string(role.ReadOnly)}, ApiOnlyUser: true}, }, } var transactionUid = uuid.New().String() diff --git a/client/msp/users/models.go b/client/msp/users/models.go index 89f3152d..ba8fc285 100644 --- a/client/msp/users/models.go +++ b/client/msp/users/models.go @@ -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 { diff --git a/client/msp/users/read.go b/client/msp/users/read.go new file mode 100644 index 00000000..95813306 --- /dev/null +++ b/client/msp/users/read.go @@ -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 +} diff --git a/docs/data-sources/user.md b/docs/data-sources/user.md index b3cf9888..0ff2eda3 100644 --- a/docs/data-sources/user.md +++ b/docs/data-sources/user.md @@ -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. diff --git a/docs/resources/msp_managed_tenant_users.md b/docs/resources/msp_managed_tenant_users.md index 7a6a337e..2ffe3893 100644 --- a/docs/resources/msp_managed_tenant_users.md +++ b/docs/resources/msp_managed_tenant_users.md @@ -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 diff --git a/provider/examples/resources/msp/users/api_token.txt b/provider/examples/resources/msp/users/api_token.txt index 6da45083..1468a4eb 100644 --- a/provider/examples/resources/msp/users/api_token.txt +++ b/provider/examples/resources/msp/users/api_token.txt @@ -1 +1 @@ -Paste your API token here \ No newline at end of file +eyJraWQiOiIwIiwidHlwIjoiSldUIiwiYWxnIjoiUlMyNTYifQ.eyJ2ZXIiOiIwIiwicm9sZXMiOlsiUk9MRV9TVVBFUl9BRE1JTiJdLCJhbXIiOiJzYW1sIiwiaXNzIjoiaXRkIiwiY2x1c3RlcklkIjoiMSIsInN1YmplY3RUeXBlIjoidXNlciIsImNsaWVudF9pZCI6ImFwaS1jbGllbnQiLCJwYXJlbnRJZCI6IjJhMTFiNmIzLTliOWQtNDNkZi05ODhiLTI4NWYwMjY4NjQ1OSIsInNjb3BlIjpbInRydXN0IiwicmVhZCIsIjJhMTFiNmIzLTliOWQtNDNkZi05ODhiLTI4NWYwMjY4NjQ1OSIsIndyaXRlIl0sImlkIjoiNjAyMjYxNTUtOTRjYi00YWY5LWIzYTQtZDk0ZTcxYjhmOThkIiwiZXhwIjozODc3MDE3Mzc3LCJpYXQiOjE3Mjk1MzM3OTAsImp0aSI6ImEwMGRlNjFkLWNkZDUtNDZjZi05ZmZiLTRmNWI3YzMzNTc2OCJ9.JjD-Q8waAIRoIW4WCKI9ri7tNuoRYFIKehl5yzIh_RVZdK2uftgeeHYrp6OR6X4Atze0yw8vkMo-xCaIM9jIHTFXrbpMW5tQvefKaKQp7_TlpQI4T_t_8TxWkq6gAOAsnVgsZ-Tnr4vQQz8X0qVHq-yXvPXJP0pucdJtEgytBn2OjW6o3wkvclnbnCKM_xApNMJBN5lJYUs0RpZrvAUrkKtBQi3DL9o8aMI92t03PIHRKWOrDXHxX44R6Q48p_LModB8B1bNB6-IL_P3rZeL9l763yGwY6TBIF62pWWZo1000g2KJBgejyJCO1TKUtaJsbwS98QI8WPVcKmM71waRg \ No newline at end of file diff --git a/provider/examples/resources/msp/users/main.tf b/provider/examples/resources/msp/users/main.tf index b482af25..df619d40 100644 --- a/provider/examples/resources/msp/users/main.tf +++ b/provider/examples/resources/msp/users/main.tf @@ -1,5 +1,5 @@ data "cdo_msp_managed_tenant" "tenant" { - name = "CDO_tenant-name" + name = "CDO_isaks-birthday-surprise__skfh2r" } resource "cdo_msp_managed_tenant_users" "example" { @@ -7,12 +7,12 @@ resource "cdo_msp_managed_tenant_users" "example" { users = [ { username = "username@example.com", - role = "ROLE_SUPER_ADMIN" + roles = ["ROLE_SUPER_ADMIN"] api_only_user = false }, { username = "username2@example.com", - role = "ROLE_ADMIN" + roles = ["ROLE_ADMIN"] api_only_user = false } ] diff --git a/provider/examples/resources/msp/users/providers.tf b/provider/examples/resources/msp/users/providers.tf index e6ed7fb8..271024b9 100644 --- a/provider/examples/resources/msp/users/providers.tf +++ b/provider/examples/resources/msp/users/providers.tf @@ -7,6 +7,6 @@ terraform { } provider "cdo" { - base_url = "" + base_url = "https://staging.dev.lockhart.io" api_token = file("${path.module}/api_token.txt") } diff --git a/provider/go.mod b/provider/go.mod index bc18824f..c052ac1a 100644 --- a/provider/go.mod +++ b/provider/go.mod @@ -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 diff --git a/provider/go.sum b/provider/go.sum index f156cc4c..a41e70e3 100644 --- a/provider/go.sum +++ b/provider/go.sum @@ -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= diff --git a/provider/internal/acctest/helper.go b/provider/internal/acctest/helper.go index a5506554..6625e9f0 100644 --- a/provider/internal/acctest/helper.go +++ b/provider/internal/acctest/helper.go @@ -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(©Obj).Elem() diff --git a/provider/internal/msp/msp_tenant_users/models.go b/provider/internal/msp/msp_tenant_users/models.go index a85ca0b1..b081aad6 100644 --- a/provider/internal/msp/msp_tenant_users/models.go +++ b/provider/internal/msp/msp_tenant_users/models.go @@ -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"` } diff --git a/provider/internal/msp/msp_tenant_users/resource.go b/provider/internal/msp/msp_tenant_users/resource.go index 8d3dd970..0d77819e 100644 --- a/provider/internal/msp/msp_tenant_users/resource.go +++ b/provider/internal/msp/msp_tenant_users/resource.go @@ -5,12 +5,15 @@ import ( "fmt" cdoClient "github.com/CiscoDevnet/terraform-provider-cdo/go-client" "github.com/CiscoDevnet/terraform-provider-cdo/go-client/msp/users" + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" "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-log/tflog" ) @@ -20,7 +23,7 @@ type MspManagedTenantUsersResource struct { client *cdoClient.Client } -func (r *MspManagedTenantUsersResource) Schema(ctx context.Context, request resource.SchemaRequest, response *resource.SchemaResponse) { +func (resource *MspManagedTenantUsersResource) Schema(ctx context.Context, request resource.SchemaRequest, response *resource.SchemaResponse) { response.Schema = schema.Schema{ MarkdownDescription: "Provides a resource to add users to an MSP managed tenant.", Attributes: map[string]schema.Attribute{ @@ -31,15 +34,23 @@ func (r *MspManagedTenantUsersResource) Schema(ctx context.Context, request reso "users": schema.ListNestedAttribute{ NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: "Universally unique identifier of the user", + Computed: true, + }, "username": schema.StringAttribute{ Required: true, MarkdownDescription: "The name of the user in CDO. This must be a valid e-mail address if the user is not an API-only user.", }, - "role": schema.StringAttribute{ + "roles": schema.ListAttribute{ Required: true, - MarkdownDescription: "The role to assign to the user in the CDO tenant.", - Validators: []validator.String{ - stringvalidator.OneOf("ROLE_READ_ONLY", "ROLE_ADMIN", "ROLE_SUPER_ADMIN", "ROLE_DEPLOY_ONLY", "ROLE_EDIT_ONLY", "ROLE_VPN_SESSIONS_MANAGER"), + MarkdownDescription: "The roles to assign to the user in the CDO tenant. Note: this list can only contain one entry.", + ElementType: types.StringType, + Validators: []validator.List{ + listvalidator.ValueStringsAre( + stringvalidator.OneOf("ROLE_READ_ONLY", "ROLE_ADMIN", "ROLE_SUPER_ADMIN", "ROLE_DEPLOY_ONLY", "ROLE_EDIT_ONLY", "ROLE_VPN_SESSIONS_MANAGER"), + ), + listvalidator.SizeAtMost(1), }, }, "api_only_user": schema.BoolAttribute{ @@ -58,7 +69,7 @@ func (r *MspManagedTenantUsersResource) Schema(ctx context.Context, request reso } } -func (r *MspManagedTenantUsersResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { +func (resource *MspManagedTenantUsersResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { tflog.Debug(ctx, "Adding users to the MSSP-managed CDO tenant") var planData MspManagedTenantUsersResourceModel response.Diagnostics.Append(request.Plan.Get(ctx, &planData)...) @@ -67,22 +78,37 @@ func (r *MspManagedTenantUsersResource) Create(ctx context.Context, request reso return } - _, err := r.createAllUsersInPlan(ctx, &planData) + createdUserDetails, err := resource.client.CreateUsersInMspManagedTenant(ctx, *resource.buildMspUsersInput(&planData)) + if err != nil { response.Diagnostics.AddError("failed to create users in MSP-managed tenant", err.Error()) return } + + planData.Users = *resource.transformApiResponseToPlan(createdUserDetails) + response.Diagnostics.Append(response.State.Set(ctx, &planData)...) } -func (r *MspManagedTenantUsersResource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) { - tflog.Debug(ctx, "Reading users from MSP-managed CDO tenant is a NOOP") +func (resource *MspManagedTenantUsersResource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) { + tflog.Debug(ctx, "Reading users from MSP-managed CDO tenant") + var stateData MspManagedTenantUsersResourceModel + response.Diagnostics.Append(request.State.Get(ctx, &stateData)...) + + userDetails, err := resource.client.ReadUsersInMspManagedTenant(ctx, *resource.buildMspUsersInput(&stateData)) + if err != nil { + response.Diagnostics.AddError("failed to read users in MSP-managed tenant", err.Error()) + return + } + + stateData.Users = *resource.transformApiResponseToPlan(userDetails) + response.Diagnostics.Append(response.State.Set(ctx, &stateData)...) } -func (r *MspManagedTenantUsersResource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) { +func (resource *MspManagedTenantUsersResource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) { } -func (r *MspManagedTenantUsersResource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) { +func (resource *MspManagedTenantUsersResource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) { tflog.Debug(ctx, "Deleting users from MSP-managed CDO tenant") var stateData MspManagedTenantUsersResourceModel response.Diagnostics.Append(request.State.Get(ctx, &stateData)...) @@ -90,10 +116,12 @@ func (r *MspManagedTenantUsersResource) Delete(ctx context.Context, request reso return } - _, err := r.deleteAllUsersInState(ctx, &stateData) + _, err := resource.deleteAllUsersInState(ctx, &stateData) if err != nil { response.Diagnostics.AddError("failed to delete users", err.Error()) } + stateData.Users = []User{} + response.Diagnostics.Append(response.State.Set(ctx, &stateData)...) } func (*MspManagedTenantUsersResource) Metadata(ctx context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) { @@ -119,7 +147,7 @@ func (resource *MspManagedTenantUsersResource) Configure(ctx context.Context, re resource.client = client } -func (r *MspManagedTenantUsersResource) deleteAllUsersInState(ctx context.Context, stateData *MspManagedTenantUsersResourceModel) (interface{}, error) { +func (resource *MspManagedTenantUsersResource) deleteAllUsersInState(ctx context.Context, stateData *MspManagedTenantUsersResourceModel) (interface{}, error) { var usernames []string for _, user := range stateData.Users { usernames = append(usernames, user.Username.ValueString()) @@ -128,27 +156,50 @@ func (r *MspManagedTenantUsersResource) deleteAllUsersInState(ctx context.Contex TenantUid: stateData.TenantUid.ValueString(), Usernames: usernames, } - return r.client.DeleteUsersInMspManagedTenant(ctx, deleteInput) + return resource.client.DeleteUsersInMspManagedTenant(ctx, deleteInput) } -func (r *MspManagedTenantUsersResource) createAllUsersInPlan(ctx context.Context, planData *MspManagedTenantUsersResourceModel) (*[]users.UserDetails, *users.CreateError) { +func (resource *MspManagedTenantUsersResource) buildMspUsersInput(planData *MspManagedTenantUsersResourceModel) *users.MspUsersInput { var nativeUsers []users.UserDetails - // 2. use plan data to create tenant and fill up rest of the model + // 2. use plan data to create user and fill up rest of the model for _, user := range planData.Users { username := user.Username.ValueString() - role := user.Role.ValueString() + // Convert user.Roles to a slice of strings + var roles []string + for _, roleValue := range user.Roles.Elements() { + if roleStr, ok := roleValue.(types.String); ok { + roles = append(roles, roleStr.ValueString()) + } + } apiOnlyUser := user.ApiOnlyUser.ValueBool() nativeUsers = append(nativeUsers, users.UserDetails{ Username: username, - Role: role, + Roles: roles, ApiOnlyUser: apiOnlyUser, }) } - // TODO we need endpoint to read users in an MSP-managed tenant - return r.client.CreateUsersInMspManagedTenant(ctx, users.MspCreateUsersInput{ + return &users.MspUsersInput{ TenantUid: planData.TenantUid.ValueString(), Users: nativeUsers, - }) + } +} + +func (resource *MspManagedTenantUsersResource) transformApiResponseToPlan(createdUserDetails *[]users.UserDetails) *[]User { + var users []User + for _, userDetails := range *createdUserDetails { + var roles []attr.Value + for _, role := range userDetails.Roles { + roles = append(roles, types.StringValue(role)) + } + users = append(users, User{ + Id: types.StringValue(userDetails.Uid), + Username: types.StringValue(userDetails.Username), + ApiOnlyUser: types.BoolValue(userDetails.ApiOnlyUser), + Roles: types.ListValueMust(types.StringType, roles), + }) + } + + return &users } diff --git a/provider/internal/msp/msp_tenant_users/resource_test.go b/provider/internal/msp/msp_tenant_users/resource_test.go index 87f08584..e78e9f53 100644 --- a/provider/internal/msp/msp_tenant_users/resource_test.go +++ b/provider/internal/msp/msp_tenant_users/resource_test.go @@ -4,12 +4,14 @@ import ( "fmt" "github.com/CiscoDevnet/terraform-provider-cdo/internal/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "strings" "testing" + "text/template" ) type Users struct { Username string - Role string + Roles []string ApiOnlyUser bool } @@ -18,30 +20,41 @@ var testMspManagedTenantUsersResource = struct { Users []Users }{ Users: []Users{ - {Username: "user1@example.com", Role: "ROLE_SUPER_ADMIN", ApiOnlyUser: false}, - {Username: "example-api-user", Role: "ROLE_ADMIN", ApiOnlyUser: true}, + {Username: "user1@example.com", Roles: []string{"ROLE_SUPER_ADMIN"}, ApiOnlyUser: false}, + {Username: "example-api-user", Roles: []string{"ROLE_ADMIN"}, ApiOnlyUser: true}, }, TenantUid: acctest.Env.MspTenantId(), } +// Join function to concatenate elements of a slice into a JSON array string. +func join(slice []string) string { + quoted := make([]string, len(slice)) + for i, s := range slice { + quoted[i] = fmt.Sprintf("%q", s) // Quotes each role to make it valid JSON + } + return strings.Join(quoted, ", ") // Joins with a comma +} + const testMspManagedTenantUsersTemplate = ` resource "cdo_msp_managed_tenant_users" "test" { tenant_uid = "{{.TenantUid}}" users = [ { "username": "{{(index .Users 0).Username}}" - "role": "{{(index .Users 0).Role}}" + "roles": [{{ join (index .Users 0).Roles }}] "api_only_user": "{{(index .Users 0).ApiOnlyUser}}" }, { "username": "{{(index .Users 1).Username}}" - "role": "{{(index .Users 1).Role}}" + "roles": [{{ join (index .Users 1).Roles }}] "api_only_user": {{(index .Users 1).ApiOnlyUser}} } ] }` -var testMspManagedTenantUsersResourceConfig = acctest.MustParseTemplate(testMspManagedTenantUsersTemplate, testMspManagedTenantUsersResource) +var testMspManagedTenantUsersResourceConfig = acctest.MustParseTemplateWithFuncMap(testMspManagedTenantUsersTemplate, testMspManagedTenantUsersResource, template.FuncMap{ + "join": join, +}) func TestAccMspManagedTenantUsersResource(t *testing.T) { resource.Test(t, resource.TestCase{ @@ -54,10 +67,18 @@ func TestAccMspManagedTenantUsersResource(t *testing.T) { Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr("cdo_msp_managed_tenant_users.test", "tenant_uid", testMspManagedTenantUsersResource.TenantUid), resource.TestCheckResourceAttr("cdo_msp_managed_tenant_users.test", "users.0.username", testMspManagedTenantUsersResource.Users[0].Username), - resource.TestCheckResourceAttr("cdo_msp_managed_tenant_users.test", "users.0.role", testMspManagedTenantUsersResource.Users[0].Role), + resource.TestCheckResourceAttr( + "cdo_msp_managed_tenant_users.test", + "users.0.roles.0", + testMspManagedTenantUsersResource.Users[0].Roles[0], + ), resource.TestCheckResourceAttr("cdo_msp_managed_tenant_users.test", "users.0.api_only_user", fmt.Sprintf("%t", testMspManagedTenantUsersResource.Users[0].ApiOnlyUser)), resource.TestCheckResourceAttr("cdo_msp_managed_tenant_users.test", "users.1.username", testMspManagedTenantUsersResource.Users[1].Username), - resource.TestCheckResourceAttr("cdo_msp_managed_tenant_users.test", "users.1.role", testMspManagedTenantUsersResource.Users[1].Role), + resource.TestCheckResourceAttr( + "cdo_msp_managed_tenant_users.test", + "users.1.roles.0", + testMspManagedTenantUsersResource.Users[1].Roles[0], + ), resource.TestCheckResourceAttr("cdo_msp_managed_tenant_users.test", "users.1.api_only_user", fmt.Sprintf("%t", testMspManagedTenantUsersResource.Users[1].ApiOnlyUser)), ), }, diff --git a/provider/internal/user/data_source.go b/provider/internal/user/data_source.go index d9ae2f25..372b26a0 100644 --- a/provider/internal/user/data_source.go +++ b/provider/internal/user/data_source.go @@ -47,7 +47,7 @@ func (d *DataSource) Schema(ctx context.Context, req datasource.SchemaRequest, r Computed: true, }, "role": schema.StringAttribute{ - MarkdownDescription: "Role assigned to the user in this tenant.", + MarkdownDescription: "Roles assigned to the user in this tenant.", Computed: true, }, }, From e3baf2cac9a3509c1290d560d62332109a10dbfe Mon Sep 17 00:00:00 2001 From: Siddhu Warrier Date: Fri, 1 Nov 2024 16:35:18 +0000 Subject: [PATCH 02/10] chore(lh-86969): clean up example --- provider/examples/resources/msp/users/api_token.txt | 2 +- provider/examples/resources/msp/users/main.tf | 2 +- provider/examples/resources/msp/users/providers.tf | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/provider/examples/resources/msp/users/api_token.txt b/provider/examples/resources/msp/users/api_token.txt index 1468a4eb..6da45083 100644 --- a/provider/examples/resources/msp/users/api_token.txt +++ b/provider/examples/resources/msp/users/api_token.txt @@ -1 +1 @@ -eyJraWQiOiIwIiwidHlwIjoiSldUIiwiYWxnIjoiUlMyNTYifQ.eyJ2ZXIiOiIwIiwicm9sZXMiOlsiUk9MRV9TVVBFUl9BRE1JTiJdLCJhbXIiOiJzYW1sIiwiaXNzIjoiaXRkIiwiY2x1c3RlcklkIjoiMSIsInN1YmplY3RUeXBlIjoidXNlciIsImNsaWVudF9pZCI6ImFwaS1jbGllbnQiLCJwYXJlbnRJZCI6IjJhMTFiNmIzLTliOWQtNDNkZi05ODhiLTI4NWYwMjY4NjQ1OSIsInNjb3BlIjpbInRydXN0IiwicmVhZCIsIjJhMTFiNmIzLTliOWQtNDNkZi05ODhiLTI4NWYwMjY4NjQ1OSIsIndyaXRlIl0sImlkIjoiNjAyMjYxNTUtOTRjYi00YWY5LWIzYTQtZDk0ZTcxYjhmOThkIiwiZXhwIjozODc3MDE3Mzc3LCJpYXQiOjE3Mjk1MzM3OTAsImp0aSI6ImEwMGRlNjFkLWNkZDUtNDZjZi05ZmZiLTRmNWI3YzMzNTc2OCJ9.JjD-Q8waAIRoIW4WCKI9ri7tNuoRYFIKehl5yzIh_RVZdK2uftgeeHYrp6OR6X4Atze0yw8vkMo-xCaIM9jIHTFXrbpMW5tQvefKaKQp7_TlpQI4T_t_8TxWkq6gAOAsnVgsZ-Tnr4vQQz8X0qVHq-yXvPXJP0pucdJtEgytBn2OjW6o3wkvclnbnCKM_xApNMJBN5lJYUs0RpZrvAUrkKtBQi3DL9o8aMI92t03PIHRKWOrDXHxX44R6Q48p_LModB8B1bNB6-IL_P3rZeL9l763yGwY6TBIF62pWWZo1000g2KJBgejyJCO1TKUtaJsbwS98QI8WPVcKmM71waRg \ No newline at end of file +Paste your API token here \ No newline at end of file diff --git a/provider/examples/resources/msp/users/main.tf b/provider/examples/resources/msp/users/main.tf index df619d40..e3d332ad 100644 --- a/provider/examples/resources/msp/users/main.tf +++ b/provider/examples/resources/msp/users/main.tf @@ -1,5 +1,5 @@ data "cdo_msp_managed_tenant" "tenant" { - name = "CDO_isaks-birthday-surprise__skfh2r" + name = "CDO_test-tenant-name" } resource "cdo_msp_managed_tenant_users" "example" { diff --git a/provider/examples/resources/msp/users/providers.tf b/provider/examples/resources/msp/users/providers.tf index 271024b9..e6ed7fb8 100644 --- a/provider/examples/resources/msp/users/providers.tf +++ b/provider/examples/resources/msp/users/providers.tf @@ -7,6 +7,6 @@ terraform { } provider "cdo" { - base_url = "https://staging.dev.lockhart.io" + base_url = "" api_token = file("${path.module}/api_token.txt") } From 2d9dbe9b33b285edcbf05c16362b8f1adec496d5 Mon Sep 17 00:00:00 2001 From: Siddhu Warrier Date: Fri, 1 Nov 2024 20:25:48 +0000 Subject: [PATCH 03/10] feat(lh-86969): add a new resource called `cdo_msp_managed_tenant_user_api_token` Add a new resource to generate an API token for a user in a MSP-managed tenant. --- client/client.go | 8 ++ client/internal/url/url.go | 8 ++ client/msp/users/create.go | 11 ++ client/msp/users/delete.go | 13 ++ client/msp/users/models.go | 13 ++ .../msp_managed_tenant_user_api_token.md | 25 ++++ .../examples/resources/msp/users/README.md | 3 +- provider/examples/resources/msp/users/main.tf | 15 +++ .../msp/msp_tenant_user_api_token/models.go | 11 ++ .../msp/msp_tenant_user_api_token/resource.go | 114 ++++++++++++++++++ provider/internal/provider/provider.go | 2 + 11 files changed, 222 insertions(+), 1 deletion(-) create mode 100644 docs/resources/msp_managed_tenant_user_api_token.md create mode 100644 provider/internal/msp/msp_tenant_user_api_token/models.go create mode 100644 provider/internal/msp/msp_tenant_user_api_token/resource.go diff --git a/client/client.go b/client/client.go index 4e706688..c51f787b 100644 --- a/client/client.go +++ b/client/client.go @@ -303,3 +303,11 @@ func (c *Client) ReadUsersInMspManagedTenant(ctx context.Context, readInput user func (c *Client) DeleteUsersInMspManagedTenant(ctx context.Context, deleteInput users.MspDeleteUsersInput) (interface{}, error) { return users.Delete(ctx, c.client, deleteInput) } + +func (c *Client) GenerateApiTokenForUserInMspManagedTenant(ctx context.Context, generateApiTokenInput users.MspGenerateApiTokenInput) (*users.MspGenerateApiTokenOutput, error) { + return users.GenerateApiToken(ctx, c.client, generateApiTokenInput) +} + +func (c *Client) RevokeApiTokenForUserInMspManagedTenant(ctx context.Context, revokeApiTokenInput users.MspRevokeApiTokenInput) (interface{}, error) { + return users.RevokeApiToken(ctx, c.client, revokeApiTokenInput) +} diff --git a/client/internal/url/url.go b/client/internal/url/url.go index 005d2029..3068e310 100644 --- a/client/internal/url/url.go +++ b/client/internal/url/url.go @@ -126,6 +126,10 @@ func RevokeApiToken(baseUrl string, tokenId string) string { return fmt.Sprintf("%s/anubis/rest/v1/oauth/revoke/%s", baseUrl, tokenId) } +func RevokeApiTokenUsingPublicApi(baseUrl string) string { + return fmt.Sprintf("%s/api/rest/v1/token/revoke", baseUrl) +} + func ReadTokenInfo(baseUrl string) string { return fmt.Sprintf("%s/anubis/rest/v1/oauth/check_token", baseUrl) } @@ -229,3 +233,7 @@ func GetUsersInMspManagedTenant(baseUrl string, tenantUid string, limit int, off func DeleteUsersInMspManagedTenant(baseUrl string, tenantUid string) string { return fmt.Sprintf("%s/api/rest/v1/msp/tenants/%s/users/delete", baseUrl, tenantUid) } + +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) +} diff --git a/client/msp/users/create.go b/client/msp/users/create.go index a74b758d..1c3525e5 100644 --- a/client/msp/users/create.go +++ b/client/msp/users/create.go @@ -57,3 +57,14 @@ func Create(ctx context.Context, client http.Client, createInp MspUsersInput) (* } return readUserDetrails, nil } + +func GenerateApiToken(ctx context.Context, client http.Client, generateApiTokenInp MspGenerateApiTokenInput) (*MspGenerateApiTokenOutput, error) { + client.Logger.Printf("Generating API token for user %s in tenant %s", generateApiTokenInp.UserUid, generateApiTokenInp.TenantUid) + genApiTokenUrl := url.GenerateApiTokenForUserInMspManagedTenant(client.BaseUrl(), generateApiTokenInp.TenantUid, generateApiTokenInp.UserUid) + var mspApiTokenOutput MspGenerateApiTokenOutput + req := client.NewPost(ctx, genApiTokenUrl, nil) + if err := req.Send(&mspApiTokenOutput); err != nil { + return nil, err + } + return &mspApiTokenOutput, nil +} diff --git a/client/msp/users/delete.go b/client/msp/users/delete.go index a42402dc..d8a13f02 100644 --- a/client/msp/users/delete.go +++ b/client/msp/users/delete.go @@ -33,3 +33,16 @@ func Delete(ctx context.Context, client http.Client, deleteInp MspDeleteUsersInp return nil, nil } + +func RevokeApiToken(ctx context.Context, client http.Client, revokeInput MspRevokeApiTokenInput) (interface{}, error) { + revokeTokenUrl := url.RevokeApiTokenUsingPublicApi(client.BaseUrl()) + client.Logger.Printf("Revoking api token at %s\n", revokeTokenUrl) + req := client.NewPost(ctx, revokeTokenUrl, nil) + // overwrite token in header with API token for the user that we are revoking + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", revokeInput.ApiToken)) + if err := req.Send(&struct{}{}); err != nil { + return nil, err + } + + return nil, nil +} diff --git a/client/msp/users/models.go b/client/msp/users/models.go index ba8fc285..07bc8193 100644 --- a/client/msp/users/models.go +++ b/client/msp/users/models.go @@ -36,6 +36,19 @@ type UserPage struct { Items []UserDetails `json:"items"` } +type MspGenerateApiTokenInput struct { + TenantUid string `json:"tenantUid"` + UserUid string `json:"userUid"` +} + +type MspRevokeApiTokenInput struct { + ApiToken string `json:"apiToken"` +} + +type MspGenerateApiTokenOutput struct { + ApiToken string `json:"apiToken"` +} + type CreateError struct { Err error CreatedResourceId *string diff --git a/docs/resources/msp_managed_tenant_user_api_token.md b/docs/resources/msp_managed_tenant_user_api_token.md new file mode 100644 index 00000000..e5663e0f --- /dev/null +++ b/docs/resources/msp_managed_tenant_user_api_token.md @@ -0,0 +1,25 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "cdo_msp_managed_tenant_user_api_token Resource - cdo" +subcategory: "" +description: |- + Provides a resource to generate an API token for a user in an MSP-managed tenant. +--- + +# cdo_msp_managed_tenant_user_api_token (Resource) + +Provides a resource to generate an API token for a user in an MSP-managed tenant. + + + + +## Schema + +### Required + +- `tenant_uid` (String) Universally unique identifier of the tenant in which the API token for the user should be generated. +- `user_uid` (String) Universally unique identifier of the user for whom the API token should be generated. + +### Read-Only + +- `api_token` (String, Sensitive) The generated API token for the user. diff --git a/provider/examples/resources/msp/users/README.md b/provider/examples/resources/msp/users/README.md index f5bd4ea5..b7ba6a01 100644 --- a/provider/examples/resources/msp/users/README.md +++ b/provider/examples/resources/msp/users/README.md @@ -10,4 +10,5 @@ You need access to an MSP Portal, and API token for the MSP portal. - Modify `providers.tf` accordingly. - Paste CDO API token for an MSP portal into `api_token.txt` - see https://docs.defenseorchestrator.com/#!c-api-tokens.html for how to generate this. -- Specify the name of a tenant managed by the MSP Portal. You can get the tenant name by going to Settings in the MSP portal. \ No newline at end of file +- Specify the name of a tenant managed by the MSP Portal. You can get the tenant name by going to Settings in the MSP portal. +- To see the generated API token for the created user, run `terraform show -json | jq -r ".values.outputs.api_token.value"` \ No newline at end of file diff --git a/provider/examples/resources/msp/users/main.tf b/provider/examples/resources/msp/users/main.tf index e3d332ad..663eab0b 100644 --- a/provider/examples/resources/msp/users/main.tf +++ b/provider/examples/resources/msp/users/main.tf @@ -14,6 +14,21 @@ resource "cdo_msp_managed_tenant_users" "example" { username = "username2@example.com", roles = ["ROLE_ADMIN"] api_only_user = false + }, + { + username = "api-only-user", + roles = ["ROLE_SUPER_ADMIN"] + api_only_user = true } ] +} + +resource "cdo_msp_managed_tenant_user_api_token" "user_token" { + tenant_uid = data.cdo_msp_managed_tenant.tenant.id + user_uid = cdo_msp_managed_tenant_users.example.users[2].id +} + +output "api_token" { + value = cdo_msp_managed_tenant_user_api_token.user_token.api_token + sensitive = true } \ No newline at end of file diff --git a/provider/internal/msp/msp_tenant_user_api_token/models.go b/provider/internal/msp/msp_tenant_user_api_token/models.go new file mode 100644 index 00000000..0189074e --- /dev/null +++ b/provider/internal/msp/msp_tenant_user_api_token/models.go @@ -0,0 +1,11 @@ +package msp_tenant_user_api_token + +import ( + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type MspManagedTenantUserApiTokenResourceModel struct { + TenantUid types.String `tfsdk:"tenant_uid"` + UserUid types.String `tfsdk:"user_uid"` + ApiToken types.String `tfsdk:"api_token"` // Additional field +} diff --git a/provider/internal/msp/msp_tenant_user_api_token/resource.go b/provider/internal/msp/msp_tenant_user_api_token/resource.go new file mode 100644 index 00000000..1fe63080 --- /dev/null +++ b/provider/internal/msp/msp_tenant_user_api_token/resource.go @@ -0,0 +1,114 @@ +package msp_tenant_user_api_token + +import ( + "context" + "fmt" + cdoClient "github.com/CiscoDevnet/terraform-provider-cdo/go-client" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/msp/users" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +func NewMspManagedTenantUserApiTokenResource() resource.Resource { + return &MspManagedTenantUserApiTokenResource{} +} + +type MspManagedTenantUserApiTokenResource struct { + client *cdoClient.Client +} + +func (m *MspManagedTenantUserApiTokenResource) Metadata(ctx context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) { + response.TypeName = request.ProviderTypeName + "_msp_managed_tenant_user_api_token" +} + +func (m *MspManagedTenantUserApiTokenResource) Schema(ctx context.Context, request resource.SchemaRequest, response *resource.SchemaResponse) { + response.Schema = schema.Schema{ + MarkdownDescription: "Provides a resource to generate an API token for a user in an MSP-managed tenant.", + Attributes: map[string]schema.Attribute{ + "tenant_uid": schema.StringAttribute{ + MarkdownDescription: "Universally unique identifier of the tenant in which the API token for the user should be generated.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "user_uid": schema.StringAttribute{ + MarkdownDescription: "Universally unique identifier of the user for whom the API token should be generated.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "api_token": schema.StringAttribute{ + MarkdownDescription: "The generated API token for the user.", + Computed: true, + Sensitive: true, + }, + }, + } +} + +func (m *MspManagedTenantUserApiTokenResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { + var planData MspManagedTenantUserApiTokenResourceModel + response.Diagnostics.Append(request.Plan.Get(ctx, &planData)...) + if response.Diagnostics.HasError() { + return + } + tflog.Debug(ctx, fmt.Sprintf("Generating an API token for a user %s in the tenant %s...", planData.UserUid, planData.TenantUid)) + + apiTokenInfo, err := m.client.GenerateApiTokenForUserInMspManagedTenant(ctx, users.MspGenerateApiTokenInput{ + UserUid: planData.UserUid.ValueString(), + TenantUid: planData.TenantUid.ValueString(), + }) + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("failed to generate API token for user %s in MSP-managed tenant %s", planData.UserUid, planData.TenantUid), err.Error()) + return + } + + planData.ApiToken = types.StringValue(apiTokenInfo.ApiToken) + response.Diagnostics.Append(response.State.Set(ctx, &planData)...) +} + +func (m *MspManagedTenantUserApiTokenResource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) { + tflog.Debug(ctx, "This is a NOOP") +} + +func (m *MspManagedTenantUserApiTokenResource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) { +} + +func (m *MspManagedTenantUserApiTokenResource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) { + var stateData MspManagedTenantUserApiTokenResourceModel + response.Diagnostics.Append(request.State.Get(ctx, &stateData)...) + if response.Diagnostics.HasError() { + return + } + tflog.Debug(ctx, fmt.Sprintf("Revoking an API token for a user %s in the tenant %s...", stateData.UserUid, stateData.TenantUid)) + + _, err := m.client.RevokeApiTokenForUserInMspManagedTenant(ctx, users.MspRevokeApiTokenInput{ApiToken: stateData.ApiToken.ValueString()}) + if err != nil { + response.Diagnostics.AddError("Failed to revoke API token for user", err.Error()) + } +} + +func (m *MspManagedTenantUserApiTokenResource) 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 + } + + m.client = client +} diff --git a/provider/internal/provider/provider.go b/provider/internal/provider/provider.go index bf57b39b..633b151f 100644 --- a/provider/internal/provider/provider.go +++ b/provider/internal/provider/provider.go @@ -7,6 +7,7 @@ import ( "context" "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_users" "os" @@ -176,6 +177,7 @@ func (p *CdoProvider) Resources(ctx context.Context) []func() resource.Resource tenantsettings.NewTenantSettingsResource, msp_tenant.NewTenantResource, msp_tenant_users.NewMspManagedTenantUsersResource, + msp_tenant_user_api_token.NewMspManagedTenantUserApiTokenResource, } } From b6ad1ccd9aae7a7eafbff75481eafe51c4e07677 Mon Sep 17 00:00:00 2001 From: Siddhu Warrier Date: Fri, 1 Nov 2024 22:33:24 +0000 Subject: [PATCH 04/10] test(lh-86969): add tests --- client/internal/http/request.go | 25 ++++--- client/msp/users/create_test.go | 61 ++++++++++++++++ client/msp/users/delete.go | 2 +- client/msp/users/delete_test.go | 23 ++++++ .../resource_test.go | 73 +++++++++++++++++++ 5 files changed, 172 insertions(+), 12 deletions(-) create mode 100644 provider/internal/msp/msp_tenant_user_api_token/resource_test.go diff --git a/client/internal/http/request.go b/client/internal/http/request.go index 5c3e175b..349a8bee 100644 --- a/client/internal/http/request.go +++ b/client/internal/http/request.go @@ -54,10 +54,14 @@ func NewRequest(config cdo.Config, httpClient *http.Client, logger *log.Logger, } } +func (r *Request) Send(output any) error { + return r.SendWithToken(output, &r.config.ApiToken) +} + // Send wrap send() with retry & delay & timeout... stuff // TODO: cancel retry when context done // output: if given, will unmarshal response body into this object, should be a pointer for it to be useful -func (r *Request) Send(output any) error { +func (r *Request) SendWithToken(output any, token *string) error { err := retry.Do( // context.Background() will never cancel according to documentation // we do not want to cancel here because this retry mechanism is intended to overcome @@ -65,8 +69,7 @@ func (r *Request) Send(output any) error { // and fail due to flaky-ness, but if we want to cancel, there is no obvious bad side effect. context.Background(), func() (bool, error) { - - err := r.send(output) + err := r.send(output, token) if err != nil { return false, err } @@ -83,13 +86,13 @@ func (r *Request) Send(output any) error { return err } -func (r *Request) send(output any) error { +func (r *Request) send(output any, token *string) error { // clear prev response r.Response = nil r.Error = nil // build net/http.Request - req, err := r.build() + req, err := r.build(token) if err != nil { r.Error = err return err @@ -147,7 +150,7 @@ func (r *Request) OverrideApiToken(apiToken string) { } // build the net/http.Request -func (r *Request) build() (*http.Request, error) { +func (r *Request) build(token *string) (*http.Request, error) { bodyReader, err := toReader(r.body) if err != nil { @@ -162,7 +165,7 @@ func (r *Request) build() (*http.Request, error) { req = req.WithContext(r.ctx) } - r.addHeaders(req) + r.addHeaders(req, token) r.addQueryParams(req) return req, nil } @@ -177,8 +180,8 @@ func (r *Request) addQueryParams(req *http.Request) { req.URL.RawQuery = q.Encode() } -func (r *Request) addHeaders(req *http.Request) { - r.addAuthHeader(req) +func (r *Request) addHeaders(req *http.Request, token *string) { + r.addAuthHeader(req, token) r.addOtherHeader(req) r.addJsonContentTypeHeaderIfNotPresent(req) r.addUserAgentHeader(req) @@ -192,8 +195,8 @@ func (r *Request) addJsonContentTypeHeaderIfNotPresent(req *http.Request) { } } -func (r *Request) addAuthHeader(req *http.Request) { - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.config.ApiToken)) +func (r *Request) addAuthHeader(req *http.Request, token *string) { + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", *token)) } func (r *Request) addUserAgentHeader(req *http.Request) { diff --git a/client/msp/users/create_test.go b/client/msp/users/create_test.go index 819eb97a..4fd461c7 100644 --- a/client/msp/users/create_test.go +++ b/client/msp/users/create_test.go @@ -14,6 +14,7 @@ import ( "github.com/jarcoal/httpmock" "github.com/stretchr/testify/assert" netHttp "net/http" + "sort" "strconv" "testing" "time" @@ -38,6 +39,60 @@ func generateUsers(num int) []users.UserDetails { return createdUsers } +func TestGenerateApiToken(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + t.Run("generate API token successfully", func(t *testing.T) { + httpmock.Reset() + managedTenantUid := uuid.New().String() + userUid := uuid.New().String() + generateTokenApiTokenInput := users.MspGenerateApiTokenInput{ + UserUid: userUid, + TenantUid: managedTenantUid, + } + generateTokenApiOutput := users.MspGenerateApiTokenOutput{ + ApiToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTIzNDU2Nzg5LCJuYW1lIjoiSm9zZXBoIn0.OpOSSw7e485LOP5PrzScxHb7SR6sAOMRckfFwi4rp7o", + } + + httpmock.RegisterResponder( + netHttp.MethodPost, + fmt.Sprintf("/api/rest/v1/msp/tenants/%s/users/%s/token", managedTenantUid, userUid), + httpmock.NewJsonResponderOrPanic(200, generateTokenApiOutput), + ) + + actual, err := users.GenerateApiToken(context.Background(), + *http.MustNewWithConfig(baseUrl, "valid_token", 0, 0, time.Minute), + generateTokenApiTokenInput) + + assert.Nil(t, err) + assert.Equal(t, generateTokenApiOutput, *actual, "Token not returned as expected") + }) + + t.Run("fail to generate API token", func(t *testing.T) { + httpmock.Reset() + managedTenantUid := uuid.New().String() + userUid := uuid.New().String() + generateTokenApiTokenInput := users.MspGenerateApiTokenInput{ + UserUid: userUid, + TenantUid: managedTenantUid, + } + + httpmock.RegisterResponder( + netHttp.MethodPost, + fmt.Sprintf("/api/rest/v1/msp/tenants/%s/users/%s/token", managedTenantUid, userUid), + httpmock.NewJsonResponderOrPanic(500, nil), + ) + + actual, err := users.GenerateApiToken(context.Background(), + *http.MustNewWithConfig(baseUrl, "valid_token", 0, 0, time.Minute), + generateTokenApiTokenInput) + + assert.Nil(t, actual) + assert.NotNil(t, err) + }) +} + // the create test also tests read! func TestCreate(t *testing.T) { httpmock.Activate() @@ -117,6 +172,12 @@ func TestCreate(t *testing.T) { assert.NotNil(t, actual, "Created users should have not been nil") assert.Nil(t, err, "Created users operation should have not been an error") + sort.Slice(usersWithIds, func(i, j int) bool { + return usersWithIds[i].Uid < usersWithIds[j].Uid + }) + sort.Slice(*actual, func(i, j int) bool { + return (*actual)[i].Uid < (*actual)[j].Uid + }) assert.Equal(t, usersWithIds, *actual, "Created users operation should have been the same as the created tenant") }) diff --git a/client/msp/users/delete.go b/client/msp/users/delete.go index d8a13f02..d0523c8d 100644 --- a/client/msp/users/delete.go +++ b/client/msp/users/delete.go @@ -40,7 +40,7 @@ func RevokeApiToken(ctx context.Context, client http.Client, revokeInput MspRevo req := client.NewPost(ctx, revokeTokenUrl, nil) // overwrite token in header with API token for the user that we are revoking req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", revokeInput.ApiToken)) - if err := req.Send(&struct{}{}); err != nil { + if err := req.SendWithToken(&struct{}{}, &revokeInput.ApiToken); err != nil { return nil, err } diff --git a/client/msp/users/delete_test.go b/client/msp/users/delete_test.go index 0d66c39e..4a05748b 100644 --- a/client/msp/users/delete_test.go +++ b/client/msp/users/delete_test.go @@ -16,6 +16,29 @@ import ( "time" ) +func TestRevokeApiToken(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + t.Run("successfully revoke API token for user in MSP-managed tenant", func(t *testing.T) { + httpmock.Reset() + revokeInput := users.MspRevokeApiTokenInput{ + ApiToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTIzNDU2Nzg5LCJuYW1lIjoiSm9zZXBoIn0.OpOSSw7e485LOP5PrzScxHb7SR6sAOMRckfFwi4rp7o", + } + httpmock.RegisterResponder( + netHttp.MethodPost, + "/api/rest/v1/token/revoke", + httpmock.NewJsonResponderOrPanic(200, nil), + ) + response, err := users.RevokeApiToken(context.Background(), + *http.MustNewWithConfig(baseUrl, "valid_token", 0, 0, time.Minute), + revokeInput) + + assert.Nil(t, err) + assert.Nil(t, response) + }) +} + func TestDelete(t *testing.T) { httpmock.Activate() defer httpmock.DeactivateAndReset() diff --git a/provider/internal/msp/msp_tenant_user_api_token/resource_test.go b/provider/internal/msp/msp_tenant_user_api_token/resource_test.go new file mode 100644 index 00000000..1e10aa84 --- /dev/null +++ b/provider/internal/msp/msp_tenant_user_api_token/resource_test.go @@ -0,0 +1,73 @@ +package msp_tenant_user_api_token_test + +import ( + "fmt" + "github.com/CiscoDevnet/terraform-provider-cdo/internal/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "strings" + "testing" + "text/template" +) + +type Users struct { + Username string + Roles []string + ApiOnlyUser bool +} + +var testMspManagedTenantUsersResource = struct { + TenantUid string + Users []Users +}{ + Users: []Users{ + {Username: "api_only_user", Roles: []string{"ROLE_SUPER_ADMIN"}, ApiOnlyUser: true}, + }, + TenantUid: acctest.Env.MspTenantId(), +} + +// Join function to concatenate elements of a slice into a JSON array string. +func join(slice []string) string { + quoted := make([]string, len(slice)) + for i, s := range slice { + quoted[i] = fmt.Sprintf("%q", s) // Quotes each role to make it valid JSON + } + return strings.Join(quoted, ", ") // Joins with a comma +} + +const testMspManagedTenantUsersAndApiTokenTemplate = ` +resource "cdo_msp_managed_tenant_users" "test" { + tenant_uid = "{{.TenantUid}}" + users = [ + { + "username": "{{(index .Users 0).Username}}" + "roles": [{{ join (index .Users 0).Roles }}] + "api_only_user": "{{(index .Users 0).ApiOnlyUser}}" + } + ] +} + +resource "cdo_msp_managed_tenant_user_api_token" "test" { + tenant_uid = "{{.TenantUid}}" + user_uid = cdo_msp_managed_tenant_users.test.users[0].id +} +` + +var testMspManagedTenantUsersAndApiTokenResourceConfig = acctest.MustParseTemplateWithFuncMap(testMspManagedTenantUsersAndApiTokenTemplate, testMspManagedTenantUsersResource, template.FuncMap{ + "join": join, +}) + +func TestAccMspManagedTenantUserApiTokenResource(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: acctest.PreCheckFunc(t), + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Read testing + { + Config: acctest.MspProviderConfig() + testMspManagedTenantUsersAndApiTokenResourceConfig, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("cdo_msp_managed_tenant_user_api_token.test", "api_token"), + ), + }, + }, + }) +} From 3e624f9efde3e82a92c014b0b687dea521aaca98 Mon Sep 17 00:00:00 2001 From: Siddhu Warrier Date: Fri, 1 Nov 2024 22:44:38 +0000 Subject: [PATCH 05/10] docs(lh-86969): fix documentation for msp_managed_tenant_user_api_token --- docs/resources/msp_managed_tenant_user_api_token.md | 4 ++-- provider/internal/msp/msp_tenant_user_api_token/resource.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/resources/msp_managed_tenant_user_api_token.md b/docs/resources/msp_managed_tenant_user_api_token.md index e5663e0f..8d11b822 100644 --- a/docs/resources/msp_managed_tenant_user_api_token.md +++ b/docs/resources/msp_managed_tenant_user_api_token.md @@ -3,12 +3,12 @@ page_title: "cdo_msp_managed_tenant_user_api_token Resource - cdo" subcategory: "" description: |- - Provides a resource to generate an API token for a user in an MSP-managed tenant. + Provides a resource to manage an API token for a user in an MSP-managed tenant. --- # cdo_msp_managed_tenant_user_api_token (Resource) -Provides a resource to generate an API token for a user in an MSP-managed tenant. +Provides a resource to manage an API token for a user in an MSP-managed tenant. diff --git a/provider/internal/msp/msp_tenant_user_api_token/resource.go b/provider/internal/msp/msp_tenant_user_api_token/resource.go index 1fe63080..529ae6be 100644 --- a/provider/internal/msp/msp_tenant_user_api_token/resource.go +++ b/provider/internal/msp/msp_tenant_user_api_token/resource.go @@ -27,7 +27,7 @@ func (m *MspManagedTenantUserApiTokenResource) Metadata(ctx context.Context, req func (m *MspManagedTenantUserApiTokenResource) Schema(ctx context.Context, request resource.SchemaRequest, response *resource.SchemaResponse) { response.Schema = schema.Schema{ - MarkdownDescription: "Provides a resource to generate an API token for a user in an MSP-managed tenant.", + MarkdownDescription: "Provides a resource to manage an API token for a user in an MSP-managed tenant.", Attributes: map[string]schema.Attribute{ "tenant_uid": schema.StringAttribute{ MarkdownDescription: "Universally unique identifier of the tenant in which the API token for the user should be generated.", From ad9a9a358d6ad7485ff76f9c26df8276fec9b508 Mon Sep 17 00:00:00 2001 From: Siddhu Warrier Date: Fri, 1 Nov 2024 22:45:28 +0000 Subject: [PATCH 06/10] docs(lh-86969): fix capitalization of error message --- provider/internal/msp/msp_tenant_user_api_token/resource.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/provider/internal/msp/msp_tenant_user_api_token/resource.go b/provider/internal/msp/msp_tenant_user_api_token/resource.go index 529ae6be..290137f3 100644 --- a/provider/internal/msp/msp_tenant_user_api_token/resource.go +++ b/provider/internal/msp/msp_tenant_user_api_token/resource.go @@ -65,7 +65,7 @@ func (m *MspManagedTenantUserApiTokenResource) Create(ctx context.Context, reque TenantUid: planData.TenantUid.ValueString(), }) if err != nil { - response.Diagnostics.AddError(fmt.Sprintf("failed to generate API token for user %s in MSP-managed tenant %s", planData.UserUid, planData.TenantUid), err.Error()) + response.Diagnostics.AddError(fmt.Sprintf("Failed to generate API token for user %s in MSP-managed tenant %s", planData.UserUid, planData.TenantUid), err.Error()) return } From 863947acf2f749be58bce8b460a9cd4d78bb6a6b Mon Sep 17 00:00:00 2001 From: Siddhu Warrier Date: Fri, 1 Nov 2024 22:50:26 +0000 Subject: [PATCH 07/10] fix(lh-86969): remove fake JWT tokens in unit tests because it freaks GitGuardian out --- client/msp/users/create_test.go | 2 +- client/msp/users/delete_test.go | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/client/msp/users/create_test.go b/client/msp/users/create_test.go index 4fd461c7..8d8ae14d 100644 --- a/client/msp/users/create_test.go +++ b/client/msp/users/create_test.go @@ -52,7 +52,7 @@ func TestGenerateApiToken(t *testing.T) { TenantUid: managedTenantUid, } generateTokenApiOutput := users.MspGenerateApiTokenOutput{ - ApiToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTIzNDU2Nzg5LCJuYW1lIjoiSm9zZXBoIn0.OpOSSw7e485LOP5PrzScxHb7SR6sAOMRckfFwi4rp7o", + ApiToken: "fake-api-token", } httpmock.RegisterResponder( diff --git a/client/msp/users/delete_test.go b/client/msp/users/delete_test.go index 4a05748b..5e1f973e 100644 --- a/client/msp/users/delete_test.go +++ b/client/msp/users/delete_test.go @@ -23,7 +23,8 @@ func TestRevokeApiToken(t *testing.T) { t.Run("successfully revoke API token for user in MSP-managed tenant", func(t *testing.T) { httpmock.Reset() revokeInput := users.MspRevokeApiTokenInput{ - ApiToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTIzNDU2Nzg5LCJuYW1lIjoiSm9zZXBoIn0.OpOSSw7e485LOP5PrzScxHb7SR6sAOMRckfFwi4rp7o", + ApiToken: "fake-api-token" + + "", } httpmock.RegisterResponder( netHttp.MethodPost, From 5106a91e30234fb85517cd0193d66858cbb3917a Mon Sep 17 00:00:00 2001 From: Siddhu Warrier Date: Fri, 1 Nov 2024 22:54:42 +0000 Subject: [PATCH 08/10] fix(lh-86969): add gitleaks config to ignore false positives --- .gitleaks.yaml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .gitleaks.yaml diff --git a/.gitleaks.yaml b/.gitleaks.yaml new file mode 100644 index 00000000..488fa5d7 --- /dev/null +++ b/.gitleaks.yaml @@ -0,0 +1,6 @@ +rules: + - description: "Ignore ApiToken in tests" + regex: '(?i)(ApiToken:\s?"fake-api-token")' + allowlist: + - "ApiToken" + path: "^.*_test\\.go$" \ No newline at end of file From 17019bbcd588b233891e5cf1e218b0cea85f5c71 Mon Sep 17 00:00:00 2001 From: Siddhu Warrier Date: Fri, 1 Nov 2024 22:56:27 +0000 Subject: [PATCH 09/10] fix(lh-86969): use toml instead of yaml --- .gitleaks.toml | 5 +++++ .gitleaks.yaml | 6 ------ 2 files changed, 5 insertions(+), 6 deletions(-) create mode 100644 .gitleaks.toml delete mode 100644 .gitleaks.yaml diff --git a/.gitleaks.toml b/.gitleaks.toml new file mode 100644 index 00000000..174be5b9 --- /dev/null +++ b/.gitleaks.toml @@ -0,0 +1,5 @@ +[[rules]] + description = "Ignore ApiToken in tests" + regex = '''(?i)(ApiToken:\s?"fake-api-token")''' + allowlist = ["ApiToken"] + path = '''^.*_test\.go$''' diff --git a/.gitleaks.yaml b/.gitleaks.yaml deleted file mode 100644 index 488fa5d7..00000000 --- a/.gitleaks.yaml +++ /dev/null @@ -1,6 +0,0 @@ -rules: - - description: "Ignore ApiToken in tests" - regex: '(?i)(ApiToken:\s?"fake-api-token")' - allowlist: - - "ApiToken" - path: "^.*_test\\.go$" \ No newline at end of file From aa9b7175cd4431c2a7ea8d010b194b2563516fc6 Mon Sep 17 00:00:00 2001 From: Siddhu Warrier Date: Fri, 1 Nov 2024 22:59:27 +0000 Subject: [PATCH 10/10] try fix gitleaks config --- .gitleaks.toml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.gitleaks.toml b/.gitleaks.toml index 174be5b9..c8fdf98b 100644 --- a/.gitleaks.toml +++ b/.gitleaks.toml @@ -1,5 +1,10 @@ [[rules]] description = "Ignore ApiToken in tests" - regex = '''(?i)(ApiToken:\s?"fake-api-token")''' - allowlist = ["ApiToken"] + regex = '''(?i)ApiToken:\s?"fake-api-token"''' path = '''^.*_test\.go$''' + + [rules.allowlist] + description = "Allow fake ApiToken in test files" + commits = [] + files = [] + paths = ["^.*_test\\.go$"] \ No newline at end of file