Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(LH-69477): Add user and user_api_token resources #26

Merged
merged 18 commits into from
Aug 30, 2023
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
a75fddd
feat(LH-69477): Add resource and data source for a CDO user
siddhuwarrier Aug 29, 2023
b915f2f
docs(LH-69477): Add documentation
siddhuwarrier Aug 29, 2023
4b19913
docs(LH-69477): Add examples
siddhuwarrier Aug 29, 2023
becf543
fix(LH-69477): Record generated_username for user resource as API-onl…
siddhuwarrier Aug 29, 2023
3f40d7f
feat(LH-69477): Add API token resource
siddhuwarrier Aug 29, 2023
4cbf66b
fix(LH-69477): Record generated_username for user resource as API-onl…
siddhuwarrier Aug 29, 2023
69e77f2
Merge branch 'LH-69477-support-creating-cdo-users-in-terraform' into …
siddhuwarrier Aug 29, 2023
98a015f
feat(LH-69477): Add ability to revoke existing token
siddhuwarrier Aug 29, 2023
d3a9c77
Address comments and add docs
siddhuwarrier Aug 29, 2023
f0c86c4
fix(LH-69477): Fix broken test
siddhuwarrier Aug 29, 2023
5c79c54
test(LH-69477): Add additional tests
siddhuwarrier Aug 29, 2023
6be4d06
docs(LH-69447): Clean up readme
siddhuwarrier Aug 29, 2023
70d4013
test(LH-69477): Disable flaky acctest
siddhuwarrier Aug 30, 2023
4d48761
fix(LH-69447): Address Tal's comments
siddhuwarrier Aug 30, 2023
aad4b3e
refactor(LH-69477): Rename UserByUid to ReadOrUpdateUserByUid to make…
siddhuwarrier Aug 30, 2023
7d281b2
refactor(LH-69477): Add type aliases for CreateUserOutput, UpdateUser…
siddhuwarrier Aug 30, 2023
30ebea5
ci(LH-69477): Streamline CI
siddhuwarrier Aug 30, 2023
dd6a5a3
refactor(LH-69477): Address multiple pull-request comments
siddhuwarrier Aug 30, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ package client

import (
"context"
"net/http"

"github.com/CiscoDevnet/terraform-provider-cdo/go-client/connector"
"github.com/CiscoDevnet/terraform-provider-cdo/go-client/device/asa/asaconfig"
"github.com/CiscoDevnet/terraform-provider-cdo/go-client/device/genericssh"
"net/http"
"github.com/CiscoDevnet/terraform-provider-cdo/go-client/user"

"github.com/CiscoDevnet/terraform-provider-cdo/go-client/device"
"github.com/CiscoDevnet/terraform-provider-cdo/go-client/device/ios"
Expand Down Expand Up @@ -120,3 +122,31 @@ func (c *Client) UpdateGenericSSH(ctx context.Context, inp genericssh.UpdateInpu
func (c *Client) DeleteGenericSSH(ctx context.Context, inp genericssh.DeleteInput) (*genericssh.DeleteOutput, error) {
return genericssh.Delete(ctx, c.client, inp)
}

func (c *Client) ReadUserByUsername(ctx context.Context, inp user.ReadByUsernameInput) (*user.UserDetails, error) {
return user.ReadByUsername(ctx, c.client, inp)
}

func (c *Client) ReadUserByUid(ctx context.Context, inp user.ReadByUidInput) (*user.UserDetails, error) {
return user.ReadByUid(ctx, c.client, inp)
}

func (c *Client) CreateUser(ctx context.Context, inp user.CreateUserInput) (*user.UserDetails, error) {
return user.Create(ctx, c.client, inp)
}

func (c *Client) DeleteUser(ctx context.Context, inp user.DeleteUserInput) (*user.DeleteUserOutput, error) {
return user.Delete(ctx, c.client, inp)
}

func (c *Client) UpdateUser(ctx context.Context, inp user.UpdateUserInput) (*user.UserDetails, error) {
return user.Update(ctx, c.client, inp)
}

func (c *Client) GenerateApiToken(ctx context.Context, inp user.GenerateApiTokenInput) (*user.ApiTokenResponse, error) {
return user.GenerateApiToken(ctx, c.client, inp)
}

func (c *Client) RevokeApiToken(ctx context.Context, inp user.RevokeApiTokenInput) (*user.RevokeApiTokenOutput, error) {
return user.RevokeApiToken(ctx, c.client, inp)
}
34 changes: 29 additions & 5 deletions client/internal/http/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ func NewRequest(config cdo.Config, httpClient *http.Client, logger *log.Logger,
func (r *Request) Send(output any) error {
err := retry.Do(func() (bool, error) {

err := r.send(output)
err := r.send(output, "application/json")
if err != nil {
return false, err
}
Expand All @@ -72,13 +72,33 @@ func (r *Request) Send(output any) error {
return err
}

func (r *Request) send(output any) error {
func (r *Request) SendFormUrlEncoded(output any) error {
err := retry.Do(func() (bool, error) {

err := r.send(output, "application/x-www-form-urlencoded")
if err != nil {
return false, err
}
return true, nil

}, *retry.NewOptions(
r.logger,
r.config.Timeout,
r.config.Delay,
r.config.Retries,
false,
))

return err
}

func (r *Request) send(output any, contentType string) error {
// clear prev response
r.Response = nil
r.Error = nil

// build net/http.Request
req, err := r.build()
req, err := r.build(contentType)
if err != nil {
r.Error = err
return err
Expand Down Expand Up @@ -124,7 +144,7 @@ func (r *Request) send(output any) error {
}

// build the net/http.Request
func (r *Request) build() (*http.Request, error) {
func (r *Request) build(contentType string) (*http.Request, error) {

bodyReader, err := toReader(r.body)
if err != nil {
Expand All @@ -139,12 +159,16 @@ func (r *Request) build() (*http.Request, error) {
req = req.WithContext(r.ctx)
}
r.addAuthHeader(req)
r.addContentTypeHeader(req, contentType)
return req, nil
}

func (r *Request) addAuthHeader(req *http.Request) {
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.config.ApiToken))
req.Header.Add("Content-Type", "application/json")
}

func (r *Request) addContentTypeHeader(req *http.Request, contentType string) {
req.Header.Add("Content-Type", contentType)
}

// toReader try to convert anything to io.Reader.
Expand Down
21 changes: 21 additions & 0 deletions client/internal/url/url.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package url

import (
"fmt"

"github.com/CiscoDevnet/terraform-provider-cdo/go-client/model/devicetype"
)

Expand Down Expand Up @@ -76,3 +77,23 @@ func ReadSmartLicense(baseUrl string) string {
func ReadAccessPolicies(baseUrl string, domainUid string, limit int) string {
return fmt.Sprintf("%s/fmc/api/fmc_config/v1/domain/%s/policy/accesspolicies?limit=%d", baseUrl, domainUid, limit)
}

func CreateUser(baseUrl string, username string) string {
return fmt.Sprintf("%s/anubis/rest/v1/users/%s", baseUrl, username)
}

func ReadUserByUsername(baseUrl string, username string) string {
return fmt.Sprintf("%s/anubis/rest/v1/users?q=name:%s", baseUrl, username)
}

func UserByUid(baseUrl string, uid string) string {
return fmt.Sprintf("%s/anubis/rest/v1/users/%s", baseUrl, uid)
}

func GenerateApiToken(baseUrl string, username string) string {
return fmt.Sprintf("%s/anubis/rest/v1/oauth/token/%s", baseUrl, username)
}

func RevokeApiToken(baseUrl string, tokenId string) string {
return fmt.Sprintf("%s/anubis/rest/v1/oauth/revoke/%s", baseUrl, tokenId)
}
27 changes: 27 additions & 0 deletions client/user/create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package user

import (
"context"
"fmt"

"github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/http"
)

func Create(ctx context.Context, client http.Client, createInp CreateUserInput) (*UserDetails, error) {
client.Logger.Println(fmt.Sprintf("Creating user %s", createInp.Username))
req := NewCreateRequest(ctx, client, createInp)

var outp UserTenantAssociation
if err := req.SendFormUrlEncoded(&outp); err != nil {
return nil, err
}

// the user creation endpoint annoyingly returns an association, so we need to make a read request to get the actual user
readReq := NewReadByUidRequest(ctx, client, outp.Source.Uid)
var readOutp UserDetails
if readErr := readReq.Send(&readOutp); readErr != nil {
return nil, readErr
}

return &readOutp, nil
}
89 changes: 89 additions & 0 deletions client/user/create_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package user_test

import (
"context"
"fmt"
netHttp "net/http"
"testing"
"time"

"github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/http"
"github.com/CiscoDevnet/terraform-provider-cdo/go-client/user"
"github.com/jarcoal/httpmock"
"github.com/stretchr/testify/assert"
)

func TestCreate(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()

validUserTenantAssociation := user.UserTenantAssociation{
Uid: "association-uuid",
Source: user.Association{
Uid: "sample-uuid",
Namespace: "systemdb",
Type: "users",
},
}

t.Run("successfully create user", func(t *testing.T) {
httpmock.Reset()
expected := user.UserDetails{
Name: "[email protected]",
ApiOnlyUser: false,
UserRoles: []string{"ROLE_SUPER_ADMIN"},
}

httpmock.RegisterResponder(
netHttp.MethodPost,
fmt.Sprintf("/anubis/rest/v1/users/%s", expected.Name),
httpmock.NewJsonResponderOrPanic(200, validUserTenantAssociation),
)
httpmock.RegisterResponder(
netHttp.MethodGet,
"/anubis/rest/v1/users/"+validUserTenantAssociation.Source.Uid,
httpmock.NewJsonResponderOrPanic(200, expected),
)

actual, err := user.Create(context.Background(), *http.MustNewWithConfig(baseUrl, "valid_token", 0, 0, time.Minute), *user.NewCreateUserInput(expected.Name, expected.UserRoles[0], expected.ApiOnlyUser))

assert.NotNil(t, actual, "User details returned must not be nil")
assert.Equal(t, expected, *actual, "Actual user details do not match expected")
assert.Nil(t, err, "error should be nil")
})

t.Run("should error if failed to create user", func(t *testing.T) {
username := "[email protected]"
apiOnlyUser := false
userRoles := []string{"ROLE_READ_ONLY"}
httpmock.RegisterResponder(
netHttp.MethodPost,
fmt.Sprintf("/anubis/rest/v1/users/%s", username),
httpmock.NewJsonResponderOrPanic(500, nil),
)

actual, err := user.Create(context.Background(), *http.MustNewWithConfig(baseUrl, "valid_token", 0, 0, time.Minute), *user.NewCreateUserInput(username, userRoles[0], apiOnlyUser))
assert.Nil(t, actual, "Expected actual user not to be created")
assert.NotNil(t, err, "Expected error")
})

t.Run("should error if failed to read user details after creation", func(t *testing.T) {
username := "[email protected]"
apiOnlyUser := false
userRoles := []string{"ROLE_READ_ONLY"}
httpmock.RegisterResponder(
netHttp.MethodPost,
fmt.Sprintf("/anubis/rest/v1/users/%s", username),
httpmock.NewJsonResponderOrPanic(200, validUserTenantAssociation),
)
httpmock.RegisterResponder(
netHttp.MethodGet,
"/anubis/rest/v1/users/"+validUserTenantAssociation.Source.Uid,
httpmock.NewJsonResponderOrPanic(500, nil),
)

actual, err := user.Create(context.Background(), *http.MustNewWithConfig(baseUrl, "valid_token", 0, 0, time.Minute), *user.NewCreateUserInput(username, userRoles[0], apiOnlyUser))
assert.Nil(t, actual, "Expected actual user not to be created")
assert.NotNil(t, err, "Expected error")
})
}
22 changes: 22 additions & 0 deletions client/user/delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package user

import (
"context"

"github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/http"
"github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/url"
)

func Delete(ctx context.Context, client http.Client, deleteInp DeleteUserInput) (*DeleteUserOutput, error) {

url := url.UserByUid(client.BaseUrl(), deleteInp.Uid)

req := client.NewDelete(ctx, url)

var deleteOutp DeleteUserOutput
if err := req.Send(&deleteOutp); err != nil {
return nil, err
}

return &deleteOutp, nil
}
49 changes: 49 additions & 0 deletions client/user/delete_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package user_test

import (
"context"
netHttp "net/http"
"testing"
"time"

"github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/http"
"github.com/CiscoDevnet/terraform-provider-cdo/go-client/user"
"github.com/jarcoal/httpmock"
"github.com/stretchr/testify/assert"
)

func TestDelete(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()

t.Run("Should delete a user", func(t *testing.T) {
httpmock.Reset()
uid := "sample-user-uid"
httpmock.RegisterResponder(
netHttp.MethodDelete,
"/anubis/rest/v1/users/"+uid,
httpmock.NewJsonResponderOrPanic(200, nil),
)
deleteOutput, err := user.Delete(context.Background(), *http.MustNewWithConfig(baseUrl, "valid_token", 0, 0, time.Minute), user.DeleteUserInput{
Uid: uid,
})
assert.NotNil(t, deleteOutput, "Delete output should not be nil")
assert.Nil(t, err, "error should be nil")
})

t.Run("Should error if deletion of a user fails", func(t *testing.T) {
httpmock.Reset()
uid := "sample-user-uid"
httpmock.RegisterResponder(
netHttp.MethodDelete,
"/anubis/rest/v1/users/"+uid,
httpmock.NewJsonResponderOrPanic(500, nil),
)

deleteOutput, err := user.Delete(context.Background(), *http.MustNewWithConfig(baseUrl, "valid_token", 0, 0, time.Minute), user.DeleteUserInput{
Uid: uid,
})
assert.Nil(t, deleteOutput, "Delete output should be nil")
assert.NotNil(t, err, "error should not be nil")
})
}
12 changes: 12 additions & 0 deletions client/user/fixtures_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package user_test

const (
tenantUid = "99999999-9999-9999-9999-999999999999"
tenantName = "test-tenant-name"
accessToken = "test-access-token"
refreshToken = "test-refresh-token"
tokenType = "test-token-type"
scope = "test-scope"
baseUrl = "https://unittest.cdo.cisco.com"
host = "unittest.cdo.cisco.com"
)
20 changes: 20 additions & 0 deletions client/user/generate_api_token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package user

import (
"context"
"fmt"

"github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/http"
)

func GenerateApiToken(ctx context.Context, client http.Client, generateApiTokenInp GenerateApiTokenInput) (*ApiTokenResponse, error) {
client.Logger.Println(fmt.Sprintf("Generating API token for user %s", generateApiTokenInp.Name))
req := NewGenerateApiTokenRequest(ctx, client, generateApiTokenInp)

var outp ApiTokenResponse
if err := req.Send(&outp); err != nil {
return nil, err
}

return &outp, nil
}
Loading