From 122116b0210aed9e7d5afc11d55f68e9dd2fdeae Mon Sep 17 00:00:00 2001 From: Sibley Date: Mon, 26 Oct 2020 16:26:53 -0600 Subject: [PATCH 1/4] add user client functionality --- client.go | 66 ++++++++++++++++++-- document/document.go | 8 +-- document/mocks/Document.go | 2 +- mocks/URLBuilder.go | 120 ++++++++++++++++++++++++++++++++++++- 4 files changed, 182 insertions(+), 14 deletions(-) diff --git a/client.go b/client.go index 63f9220..b4f5215 100644 --- a/client.go +++ b/client.go @@ -9,6 +9,8 @@ import ( "io" "log" "net/http" + "net/url" + "strings" ) const ( @@ -27,10 +29,24 @@ var ( ErrBadRequest = errors.New("error: bad request") ) +// Error API Response. Contains the error message as well as the type of error +type Error struct { + Message string `json:"message"` + Type string `json:"type"` +} + //URLBuilder is the interface for building URLs //go:generate mockery --name URLBuilder type URLBuilder interface { SearchDocumentURL(vaultID string) string + GetUserURL(userId []string) string + CreateUserURL() string + ListUserURL(queryParams url.Values) string + UpdateUserURL(userId string) string + UpdateUserPasswordURL(userId string) string + DeleteUserURL(userId string) string + CreateAccessTokenURL(userId string) string + CreateApiKeyURL(userId string) string } //DefaultURLBuilder implements URLBuilder interface @@ -41,6 +57,50 @@ func (t *DefaultURLBuilder) SearchDocumentURL(vaultID string) string { return fmt.Sprintf("https://api.truevault.com/v1/vaults/%s/search", vaultID) } +// GetUserURL returns the TrueVault `Get User` route for the specified user id(s) +func (t *DefaultURLBuilder) GetUserURL(userId []string) string { + return fmt.Sprintf("https://api.truevault.com/v2/users/"+strings.Join(userId, ",")) +} + +// CreateUserURL returns the TrueVault `Create User` route +func (t *DefaultURLBuilder) CreateUserURL() string { + return "https://api.truevault.com/v1/users" +} + +// UpdateUserURL returns the TrueVault `Update User` route +func (t *DefaultURLBuilder) UpdateUserURL(userId string) string { + return "https://api.truevault.com/v1/users/" + userId +} + +// UpdateUserPasswordURL returns the TrueVault `Update User Password` route +func (t *DefaultURLBuilder) UpdateUserPasswordURL(userId string) string { + return "https://api.truevault.com/v1/users/" + userId +} + +// DeleteUserURL returns the TrueVault `Delete User` route +func (t *DefaultURLBuilder) DeleteUserURL(userId string) string { + return "https://api.truevault.com/v1/users/" + userId +} + +// CreateAccessTokenURL returns the TrueVault `Create Access Token` route +func (t *DefaultURLBuilder) CreateAccessTokenURL(userId string) string { + return "https://api.truevault.com/v1/users/" + userId +} + +// CreateApiKeyURL returns the TrueVault `Create API Key` route +func (t *DefaultURLBuilder) CreateApiKeyURL(userId string) string { + return "https://api.truevault.com/v1/users/" + userId + "/api_key" +} + +// ListUserURL returns the TrueVault `List User` route +func (t *DefaultURLBuilder) ListUserURL(queryParams url.Values) string { + params := "?" + if queryParams != nil { + params += queryParams.Encode() + } + return fmt.Sprintf("https://api.truevault.com/v2/users/?%s", params) +} + //Client contains the base http requirements to make requests to TrueVault type Client struct { URLBuilder URLBuilder @@ -53,7 +113,7 @@ func New(h *http.Client, ub URLBuilder, accessTokenOrKey string) Client { return Client{ httpClient: h, URLBuilder: ub, - authorization: buildAuthorizationValue(accessTokenOrKey), + authorization: "Basic " + base64.StdEncoding.EncodeToString([]byte(accessTokenOrKey+":")), } } @@ -67,10 +127,6 @@ func (c *Client) WithNewAccessTokenOrKey(accessTokenOrKey string) Client { return New(c.httpClient, c.URLBuilder, accessTokenOrKey) } -func buildAuthorizationValue(key string) string { - return "Basic " + base64.StdEncoding.EncodeToString([]byte(key+":")) -} - // NewRequest builds an http.Request that contains the Authorization and Content-Type header func (c *Client) NewRequest(ctx context.Context, method, path, contentType string, body io.Reader) (*http.Request, error) { req, err := http.NewRequestWithContext(ctx, method, path, body) diff --git a/document/document.go b/document/document.go index 21f0402..0751fb4 100644 --- a/document/document.go +++ b/document/document.go @@ -67,21 +67,17 @@ func New(client gotruevault.Client) Document { // SearchDocument https://docs.truevault.com/documentsearch#search-documents func (r *TrueVaultDocument) SearchDocument(ctx context.Context, vaultID string, filter gotruevault.SearchOption) (SearchDocumentResult, error) { - var result SearchDocumentResult buf := new(bytes.Buffer) if err := json.NewEncoder(buf).Encode(filter); err != nil { return SearchDocumentResult{}, err } path := r.URLBuilder.SearchDocumentURL(vaultID) - req, err := r.NewRequest(ctx, http.MethodPost, path, gotruevault.ContentTypeApplicationJSON, buf) - if err != nil { return SearchDocumentResult{}, err } - err = r.Do(req, &result) - - return result, err + var result SearchDocumentResult + return result, r.Do(req, &result) } diff --git a/document/mocks/Document.go b/document/mocks/Document.go index d9da46c..9bdb1ed 100644 --- a/document/mocks/Document.go +++ b/document/mocks/Document.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.1.0. DO NOT EDIT. +// Code generated by mockery v1.0.0. DO NOT EDIT. package mocks diff --git a/mocks/URLBuilder.go b/mocks/URLBuilder.go index 18a83b0..68a6d8f 100644 --- a/mocks/URLBuilder.go +++ b/mocks/URLBuilder.go @@ -1,14 +1,102 @@ -// Code generated by mockery v2.1.0. DO NOT EDIT. +// Code generated by mockery v1.0.0. DO NOT EDIT. package mocks -import mock "github.com/stretchr/testify/mock" +import ( + url "net/url" + + mock "github.com/stretchr/testify/mock" +) // URLBuilder is an autogenerated mock type for the URLBuilder type type URLBuilder struct { mock.Mock } +// CreateAccessTokenURL provides a mock function with given fields: userId +func (_m *URLBuilder) CreateAccessTokenURL(userId string) string { + ret := _m.Called(userId) + + var r0 string + if rf, ok := ret.Get(0).(func(string) string); ok { + r0 = rf(userId) + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// CreateApiKeyURL provides a mock function with given fields: userId +func (_m *URLBuilder) CreateApiKeyURL(userId string) string { + ret := _m.Called(userId) + + var r0 string + if rf, ok := ret.Get(0).(func(string) string); ok { + r0 = rf(userId) + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// CreateUserURL provides a mock function with given fields: +func (_m *URLBuilder) CreateUserURL() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// DeleteUserURL provides a mock function with given fields: userId +func (_m *URLBuilder) DeleteUserURL(userId string) string { + ret := _m.Called(userId) + + var r0 string + if rf, ok := ret.Get(0).(func(string) string); ok { + r0 = rf(userId) + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// GetUserURL provides a mock function with given fields: userId +func (_m *URLBuilder) GetUserURL(userId []string) string { + ret := _m.Called(userId) + + var r0 string + if rf, ok := ret.Get(0).(func([]string) string); ok { + r0 = rf(userId) + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// ListUserURL provides a mock function with given fields: queryParams +func (_m *URLBuilder) ListUserURL(queryParams url.Values) string { + ret := _m.Called(queryParams) + + var r0 string + if rf, ok := ret.Get(0).(func(url.Values) string); ok { + r0 = rf(queryParams) + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + // SearchDocumentURL provides a mock function with given fields: vaultID func (_m *URLBuilder) SearchDocumentURL(vaultID string) string { ret := _m.Called(vaultID) @@ -22,3 +110,31 @@ func (_m *URLBuilder) SearchDocumentURL(vaultID string) string { return r0 } + +// UpdateUserPasswordURL provides a mock function with given fields: userId +func (_m *URLBuilder) UpdateUserPasswordURL(userId string) string { + ret := _m.Called(userId) + + var r0 string + if rf, ok := ret.Get(0).(func(string) string); ok { + r0 = rf(userId) + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// UpdateUserURL provides a mock function with given fields: userId +func (_m *URLBuilder) UpdateUserURL(userId string) string { + ret := _m.Called(userId) + + var r0 string + if rf, ok := ret.Get(0).(func(string) string); ok { + r0 = rf(userId) + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} From 8d7a3bfcfd053a2c3c5a0f708a6f269075c8531f Mon Sep 17 00:00:00 2001 From: Sibley Date: Mon, 26 Oct 2020 21:43:36 -0600 Subject: [PATCH 2/4] add user api functionality --- user/mocks/Client.go | 161 ++++++++++++++++++++ user/user.go | 347 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 508 insertions(+) create mode 100644 user/mocks/Client.go create mode 100644 user/user.go diff --git a/user/mocks/Client.go b/user/mocks/Client.go new file mode 100644 index 0000000..dbd5f0a --- /dev/null +++ b/user/mocks/Client.go @@ -0,0 +1,161 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + time "time" + + mock "github.com/stretchr/testify/mock" + + user "github.com/FirstVisit/go-truevault/user" +) + +// Client is an autogenerated mock type for the Client type +type Client struct { + mock.Mock +} + +// Create provides a mock function with given fields: ctx, username, password, attributes, groupIds, status, accessTokenNotValueAfter +func (_m *Client) Create(ctx context.Context, username string, password string, attributes string, groupIds []string, status user.Status, accessTokenNotValueAfter time.Time) (user.TvUser, error) { + ret := _m.Called(ctx, username, password, attributes, groupIds, status, accessTokenNotValueAfter) + + var r0 user.TvUser + if rf, ok := ret.Get(0).(func(context.Context, string, string, string, []string, user.Status, time.Time) user.TvUser); ok { + r0 = rf(ctx, username, password, attributes, groupIds, status, accessTokenNotValueAfter) + } else { + r0 = ret.Get(0).(user.TvUser) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string, string, string, []string, user.Status, time.Time) error); ok { + r1 = rf(ctx, username, password, attributes, groupIds, status, accessTokenNotValueAfter) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CreateAPIKey provides a mock function with given fields: ctx, userID +func (_m *Client) CreateAPIKey(ctx context.Context, userID string) error { + ret := _m.Called(ctx, userID) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, userID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateAccessToken provides a mock function with given fields: ctx, userId, notValidAfter +func (_m *Client) CreateAccessToken(ctx context.Context, userId string, notValidAfter time.Time) error { + ret := _m.Called(ctx, userId, notValidAfter) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, time.Time) error); ok { + r0 = rf(ctx, userId, notValidAfter) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Delete provides a mock function with given fields: ctx, userID +func (_m *Client) Delete(ctx context.Context, userID string) error { + ret := _m.Called(ctx, userID) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, userID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Get provides a mock function with given fields: ctx, userId, full +func (_m *Client) Get(ctx context.Context, userId []string, full bool) ([]user.TvUser, error) { + ret := _m.Called(ctx, userId, full) + + var r0 []user.TvUser + if rf, ok := ret.Get(0).(func(context.Context, []string, bool) []user.TvUser); ok { + r0 = rf(ctx, userId, full) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]user.TvUser) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, []string, bool) error); ok { + r1 = rf(ctx, userId, full) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// List provides a mock function with given fields: ctx, status, full +func (_m *Client) List(ctx context.Context, status user.Status, full bool) ([]user.TvUser, error) { + ret := _m.Called(ctx, status, full) + + var r0 []user.TvUser + if rf, ok := ret.Get(0).(func(context.Context, user.Status, bool) []user.TvUser); ok { + r0 = rf(ctx, status, full) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]user.TvUser) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, user.Status, bool) error); ok { + r1 = rf(ctx, status, full) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Update provides a mock function with given fields: ctx, userId, username, password, accessToken, accessTokenNotValueAfter, attributes, status +func (_m *Client) Update(ctx context.Context, userId string, username string, password string, accessToken string, accessTokenNotValueAfter time.Time, attributes string, status user.Status) (user.TvUser, error) { + ret := _m.Called(ctx, userId, username, password, accessToken, accessTokenNotValueAfter, attributes, status) + + var r0 user.TvUser + if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string, time.Time, string, user.Status) user.TvUser); ok { + r0 = rf(ctx, userId, username, password, accessToken, accessTokenNotValueAfter, attributes, status) + } else { + r0 = ret.Get(0).(user.TvUser) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string, string, string, string, time.Time, string, user.Status) error); ok { + r1 = rf(ctx, userId, username, password, accessToken, accessTokenNotValueAfter, attributes, status) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UpdatePassword provides a mock function with given fields: ctx, userId, password +func (_m *Client) UpdatePassword(ctx context.Context, userId string, password string) error { + ret := _m.Called(ctx, userId, password) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok { + r0 = rf(ctx, userId, password) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/user/user.go b/user/user.go new file mode 100644 index 0000000..d04f4bb --- /dev/null +++ b/user/user.go @@ -0,0 +1,347 @@ +package user + +import ( + "bytes" + "context" + "encoding/json" + "errors" + gotruevault "github.com/FirstVisit/go-truevault" + "net/http" + "net/url" + "strconv" + "strings" + "time" +) + +// TvUser contains the base access fields required for a TrueVault user +type TvUser struct { + AccessToken string `json:"access_token"` + AccountID string `json:"account_id"` + APIKey string `json:"api_key"` + ID string `json:"id"` + Status string `json:"status"` + UserID string `json:"user_id"` + Username string `json:"username"` + MFAEnrolled bool `json:"mfa_enrolled"` +} + +// Status indicates the state of the user +type Status string + +const ( + Activated Status = "ACTIVATED" + Pending Status = "PENDING" + Locked Status = "LOCKED" + Deactivated Status = "DEACTIVATED" +) + +// CRUDResponse contains the response from creating a new TrueVault User +type CRUDResponse struct { + Result string `json:"result"` + TransactionID string `json:"transaction_id"` + User TvUser `json:"user"` + Error gotruevault.Error `json:"error"` +} + +// GetUserResponse contains the response from creating a new TrueVault User +type GetUserResponse struct { + Result string `json:"result"` + TransactionID string `json:"transaction_id"` + Users []TvUser `json:"users"` +} + +type CreateAPIKeyResponse struct { + ApiKey string `json:"api_key"` + Result string `json:"result"` + TransactionID string `json:"transaction_id"` +} + +//go:generate mockery --name Client +type Client interface { + Get(ctx context.Context, userId []string, full bool) ([]TvUser, error) + Create(ctx context.Context, username, password, attributes string, groupIds []string, status Status, accessTokenNotValueAfter time.Time) (TvUser, error) + List(ctx context.Context, status Status, full bool) ([]TvUser, error) + Update(ctx context.Context, userId, username, password, accessToken string, accessTokenNotValueAfter time.Time, attributes string, status Status) (TvUser, error) + UpdatePassword(ctx context.Context, userId, password string) error + Delete(ctx context.Context, userID string) error + CreateAccessToken(ctx context.Context, userId string, notValidAfter time.Time) error + CreateAPIKey(ctx context.Context, userID string) error +} + +// User implements the Client interface +type User struct { + *gotruevault.Client +} + +// New creates a new User service +func New(client gotruevault.Client) User { + return User{&client} +} + +// Get returns information about one or more users. If any IDs aren't valid UUIDs, returns a 400. If any can’t be +// found or the user doesn't have permission to read them, returns a 404. Otherwise, returns 200. +// +// Note: When full=true, this endpoint consumes an Operation for every user returned, so a request with 50 ids will +// count as 50 Operations. When full=false, it consumes 1 operation regardless of how many users are returned +// +// @param userIds - string(req’d) - comma separated list of user IDs to retrieve. At most 100 ids can be fetched at a time. +// @param full – boolean(optional, default: ‘false’) - return User attributes and Group IDs. Note: If true, then this +// endpoint consumes an Operation for every user returned. If false, only a single Operation is used. +func (u *User) Get(ctx context.Context, userId []string, full bool) ([]TvUser, error) { + if userId == nil || len(userId) == 0 { + return nil, errors.New("user id required") + } + + q := make(url.Values) + q.Set("full", strconv.FormatBool(full)) + + req, err := u.NewRequest(ctx, http.MethodGet, u.URLBuilder.GetUserURL(userId), gotruevault.ContentTypeApplicationJSON, nil) + if err != nil { + return nil, err + } + + var msg GetUserResponse + return msg.Users, u.Do(req, &msg) +} + +// Create creates a new TrueVault User. The username given must be unique to ACTIVATED and LOCKED Users for an +// Account. Upon creation, both an API_KEY and an ACCESS_TOKEN will be automatically vended to the user. For security +// reasons, the API_KEY will only be shown upon creation or via the TrueVault Management Console for the account’s +// administrators. If group_ids is provided, the newly created user will be added to all given groups. The user making +// the request must have the C Group::GROUPID::GroupMembership::.* or U Group::GROUPID permission for all given groups. +// Please see authorization for more information regarding recommendations for API_KEY and ACCESS_TOKEN usage. +// +// @param username – string(req’d) - username for the User being created +// @param password – string(optional) - password for the User being created. If created without a password, the user +// can’t authenticate using the login endpoint, but it can still have an API key. This allows creating +// service accounts for backups or other server-to-TrueVault communication. +// @param attributes – b64 string(optional) - base64 encoded JSON document describing the User attributes +// @param groupIds – (optional) - list of group IDs where the new user will be placed +// @param status – (optional) - the user’s status, one of ACTIVATED (default), PENDING, or LOCKED +// @param accessTokenNotValueAfter – (optional) - expiration time of generated access token +func (u *User) Create(ctx context.Context, username, password, attributes string, groupIds []string, status Status, accessTokenNotValueAfter time.Time) (TvUser, error) { + if username == "" { + return TvUser{}, errors.New("username required to create user") + } + + data := url.Values{} + data.Set("username", username) + + if password != "" { + data.Set("password", password) + } + + if attributes != "" { + data.Set("attributes", attributes) + } + + if groupIds != nil { + data.Set("group_ids", strings.Join(groupIds, ",")) + } + + // TODO: Force empty string to be invalid. Should default (zero) to ACTIVATED + if status != "" { + data.Set("status", string(status)) + } + + if !accessTokenNotValueAfter.IsZero() { + data.Set("access_token_not_value_after", accessTokenNotValueAfter.String()) + } + + buf := new(bytes.Buffer) + if err := json.NewEncoder(buf).Encode(data); err != nil { + return TvUser{}, err + } + + req, err := u.NewRequest(ctx, http.MethodPost, u.URLBuilder.CreateUserURL(), gotruevault.ContentTypeApplicationJSON, buf) + if err != nil { + return TvUser{}, err + } + + var msg CRUDResponse + return msg.User, u.Do(req, &msg) +} + +// List returns all Users belonging to an Account. +// +// status – string(optional, default: ‘ACTIVATED’) - comma separated list of statuses (inclusive). Accepts any +// combination of ACTIVATED, DEACTIVATED, or LOCKED. +// full – boolean(optional, default: ‘false’) - return User attributes and Group IDs. Note: If true, then this endpoint +// consumes an Operation for every user returned. If false, only a single Operation is used. +func (u *User) List(ctx context.Context, status Status, full bool) ([]TvUser, error) { + q := make(url.Values) + q.Set("status", string(status)) + q.Set("full", strconv.FormatBool(full)) + + req, err := u.NewRequest(ctx, http.MethodGet, u.URLBuilder.ListUserURL(q), gotruevault.ContentTypeApplicationJSON, nil) + if err != nil { + return nil, err + } + + var msg GetUserResponse + return msg.Users, u.Do(req, &msg) +} + +// Update a given User’s properties. Strictly overwrites existing values. +// +// @Param userId – string(required) +// @Param full – boolean(optional, default: ‘false’) - return User attributes and Group IDs. Note: If true, then this +// endpoint consumes an Operation for every user returned. If false, only a single Operation is used. +func (u *User) Update(ctx context.Context, userId, username, password, accessToken string, accessTokenNotValueAfter time.Time, attributes string, status Status) (TvUser, error) { + if userId == "" { + return TvUser{}, errors.New("user id required to update user") + } + + data := url.Values{} + if username != "" { + data.Set("username", username) + } + + if password != "" { + data.Set("password", password) + } + + if accessToken != "" { + data.Set("access_token", accessToken) + } + + if !accessTokenNotValueAfter.IsZero() { + data.Set("access_token_not_value_after", accessTokenNotValueAfter.String()) + } + + if attributes != "" { + data.Set("attributes", attributes) + } + + // TODO: Force empty string to be invalid. Should default (zero) to ACTIVATED + if status != "" { + data.Set("status", string(status)) + } + + buf := new(bytes.Buffer) + if err := json.NewEncoder(buf).Encode(data); err != nil { + return TvUser{}, err + } + + req, err := u.NewRequest(ctx, http.MethodPost, u.URLBuilder.UpdateUserURL(userId), gotruevault.ContentTypeApplicationJSON, buf) + if err != nil { + return TvUser{}, err + } + + var msg CRUDResponse + return msg.User, u.Do(req, &msg) +} + +// UpdatePassword Updates a given User’s password. Requires the `U` activity on the `User::USERID::Password` or +// `User::USERID resource`. +// +// @Param userId – string(required) +// @Returns - nil on success +// - ErrorNotFound when user does not exist +func (u *User) UpdatePassword(ctx context.Context, userId, password string) error { + if userId == "" { + return errors.New("user id required") + } + + if password == "" { + return errors.New("password is required") + } + + data := url.Values{} + data.Set("password", password) + + buf := new(bytes.Buffer) + if err := json.NewEncoder(buf).Encode(data); err != nil { + return err + } + + req, err := u.NewRequest(ctx, http.MethodPost, u.URLBuilder.UpdateUserPasswordURL(userId), gotruevault.ContentTypeApplicationJSON, buf) + if err != nil { + return err + } + + var msg CRUDResponse + if err := u.Do(req, &msg); err != nil { + return err + } + + if msg.Error.Message != "" { + return errors.New(msg.Error.Message) + } + + return nil +} + +// Delete deactivates a user: frees the associated username, all ACCESS_TOKENs, and removes user_id from all Groups. +// @Warning: This endpoint does not delete any data permanently, unlike the Document and BLOB delete endpoints. If you +// need to completely purge a user’s data for policy or compliance reasons, first update the user’s attributes +// to be {}, then update their username to be a unique random string, then call this endpoint. +// @Warning: Once the user has been deactivated, it cannot be reactivated via a status update. +func (u *User) Delete(ctx context.Context, userID string) error { + if userID == "" { + return errors.New("user id required") + } + + req, err := u.NewRequest(ctx, http.MethodPost, u.URLBuilder.DeleteUserURL(userID), gotruevault.ContentTypeApplicationJSON, nil) + if err != nil { + return err + } + + var msg CRUDResponse + if err := u.Do(req, &msg); err != nil { + return err + } + + if msg.Error.Message != "" { + return errors.New(msg.Error.Message) + } + + return nil +} + +// CreateAccessToken Vends a new `ACCESS_TOKEN` for user_id. +func (u *User) CreateAccessToken(ctx context.Context, userId string, notValidAfter time.Time) error { + if userId == "" { + return errors.New("user id required") + } + + data := url.Values{} + data.Set("not_valid_after", notValidAfter.String()) + + buf := new(bytes.Buffer) + if err := json.NewEncoder(buf).Encode(data); err != nil { + return err + } + + req, err := u.NewRequest(ctx, http.MethodPost, u.URLBuilder.CreateAccessTokenURL(userId), gotruevault.ContentTypeApplicationJSON, buf) + if err != nil { + return err + } + + var msg CRUDResponse + if err := u.Do(req, &msg); err != nil { + return err + } + + if msg.Error.Message != "" { + return errors.New(msg.Error.Message) + } + + return nil +} + +// CreateAPIKey replaces the current `API_KEY` for user_id. Companion to `ACCESS_TOKEN` method. Must have `U` group +// permissions for the user. +func (u *User) CreateApiKey(ctx context.Context, userID string) (string, error) { + if userID == "" { + return "", errors.New("user id required") + } + + req, err := u.NewRequest(ctx, http.MethodPost, u.URLBuilder.CreateApiKeyURL(userID), gotruevault.ContentTypeApplicationJSON, nil) + if err != nil { + return "", err + } + + var msg CreateAPIKeyResponse + return msg.ApiKey, u.Do(req, &msg) +} From b730f2f4faa7ec0608d52c145511d0aebde2de0e Mon Sep 17 00:00:00 2001 From: Sibley Date: Mon, 26 Oct 2020 22:00:37 -0600 Subject: [PATCH 3/4] Add user status check to ensure it is valid --- user/user.go | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/user/user.go b/user/user.go index d04f4bb..6e6f017 100644 --- a/user/user.go +++ b/user/user.go @@ -26,15 +26,25 @@ type TvUser struct { } // Status indicates the state of the user -type Status string +type Status struct { + status string +} -const ( - Activated Status = "ACTIVATED" - Pending Status = "PENDING" - Locked Status = "LOCKED" - Deactivated Status = "DEACTIVATED" +var ( + // Activated the user is active in TV + Activated = Status{status: "ACTIVATED"} + // Pending the user is pending in TV + Pending = Status{status: "PENDING"} + // Locked the user is locked in TV + Locked = Status{status: "LOCKED"} + // Deactivated the user is deactivated in TV + Deactivated = Status{status: "DEACTIVATED"} ) +func (u *Status) String() string { + return u.status +} + // CRUDResponse contains the response from creating a new TrueVault User type CRUDResponse struct { Result string `json:"result"` @@ -139,9 +149,8 @@ func (u *User) Create(ctx context.Context, username, password, attributes string data.Set("group_ids", strings.Join(groupIds, ",")) } - // TODO: Force empty string to be invalid. Should default (zero) to ACTIVATED - if status != "" { - data.Set("status", string(status)) + if status.String() != "" { + data.Set("status", status.String()) } if !accessTokenNotValueAfter.IsZero() { @@ -170,7 +179,7 @@ func (u *User) Create(ctx context.Context, username, password, attributes string // consumes an Operation for every user returned. If false, only a single Operation is used. func (u *User) List(ctx context.Context, status Status, full bool) ([]TvUser, error) { q := make(url.Values) - q.Set("status", string(status)) + q.Set("status", status.String()) q.Set("full", strconv.FormatBool(full)) req, err := u.NewRequest(ctx, http.MethodGet, u.URLBuilder.ListUserURL(q), gotruevault.ContentTypeApplicationJSON, nil) @@ -213,9 +222,8 @@ func (u *User) Update(ctx context.Context, userId, username, password, accessTok data.Set("attributes", attributes) } - // TODO: Force empty string to be invalid. Should default (zero) to ACTIVATED - if status != "" { - data.Set("status", string(status)) + if status.String() != "" { + data.Set("status", status.String()) } buf := new(bytes.Buffer) From fb9a6272b97ad0ba8c6a0a9274d3603dc8335a1f Mon Sep 17 00:00:00 2001 From: Sibley Date: Mon, 26 Oct 2020 22:08:29 -0600 Subject: [PATCH 4/4] rename and tidy comments --- user/mocks/Client.go | 32 +++++------ user/user.go | 123 +++++++++++++++++++++---------------------- 2 files changed, 75 insertions(+), 80 deletions(-) diff --git a/user/mocks/Client.go b/user/mocks/Client.go index dbd5f0a..3d11b7a 100644 --- a/user/mocks/Client.go +++ b/user/mocks/Client.go @@ -17,14 +17,14 @@ type Client struct { } // Create provides a mock function with given fields: ctx, username, password, attributes, groupIds, status, accessTokenNotValueAfter -func (_m *Client) Create(ctx context.Context, username string, password string, attributes string, groupIds []string, status user.Status, accessTokenNotValueAfter time.Time) (user.TvUser, error) { +func (_m *Client) Create(ctx context.Context, username string, password string, attributes string, groupIds []string, status user.Status, accessTokenNotValueAfter time.Time) (user.User, error) { ret := _m.Called(ctx, username, password, attributes, groupIds, status, accessTokenNotValueAfter) - var r0 user.TvUser - if rf, ok := ret.Get(0).(func(context.Context, string, string, string, []string, user.Status, time.Time) user.TvUser); ok { + var r0 user.User + if rf, ok := ret.Get(0).(func(context.Context, string, string, string, []string, user.Status, time.Time) user.User); ok { r0 = rf(ctx, username, password, attributes, groupIds, status, accessTokenNotValueAfter) } else { - r0 = ret.Get(0).(user.TvUser) + r0 = ret.Get(0).(user.User) } var r1 error @@ -80,15 +80,15 @@ func (_m *Client) Delete(ctx context.Context, userID string) error { } // Get provides a mock function with given fields: ctx, userId, full -func (_m *Client) Get(ctx context.Context, userId []string, full bool) ([]user.TvUser, error) { +func (_m *Client) Get(ctx context.Context, userId []string, full bool) ([]user.User, error) { ret := _m.Called(ctx, userId, full) - var r0 []user.TvUser - if rf, ok := ret.Get(0).(func(context.Context, []string, bool) []user.TvUser); ok { + var r0 []user.User + if rf, ok := ret.Get(0).(func(context.Context, []string, bool) []user.User); ok { r0 = rf(ctx, userId, full) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).([]user.TvUser) + r0 = ret.Get(0).([]user.User) } } @@ -103,15 +103,15 @@ func (_m *Client) Get(ctx context.Context, userId []string, full bool) ([]user.T } // List provides a mock function with given fields: ctx, status, full -func (_m *Client) List(ctx context.Context, status user.Status, full bool) ([]user.TvUser, error) { +func (_m *Client) List(ctx context.Context, status user.Status, full bool) ([]user.User, error) { ret := _m.Called(ctx, status, full) - var r0 []user.TvUser - if rf, ok := ret.Get(0).(func(context.Context, user.Status, bool) []user.TvUser); ok { + var r0 []user.User + if rf, ok := ret.Get(0).(func(context.Context, user.Status, bool) []user.User); ok { r0 = rf(ctx, status, full) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).([]user.TvUser) + r0 = ret.Get(0).([]user.User) } } @@ -126,14 +126,14 @@ func (_m *Client) List(ctx context.Context, status user.Status, full bool) ([]us } // Update provides a mock function with given fields: ctx, userId, username, password, accessToken, accessTokenNotValueAfter, attributes, status -func (_m *Client) Update(ctx context.Context, userId string, username string, password string, accessToken string, accessTokenNotValueAfter time.Time, attributes string, status user.Status) (user.TvUser, error) { +func (_m *Client) Update(ctx context.Context, userId string, username string, password string, accessToken string, accessTokenNotValueAfter time.Time, attributes string, status user.Status) (user.User, error) { ret := _m.Called(ctx, userId, username, password, accessToken, accessTokenNotValueAfter, attributes, status) - var r0 user.TvUser - if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string, time.Time, string, user.Status) user.TvUser); ok { + var r0 user.User + if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string, time.Time, string, user.Status) user.User); ok { r0 = rf(ctx, userId, username, password, accessToken, accessTokenNotValueAfter, attributes, status) } else { - r0 = ret.Get(0).(user.TvUser) + r0 = ret.Get(0).(user.User) } var r1 error diff --git a/user/user.go b/user/user.go index 6e6f017..adf727e 100644 --- a/user/user.go +++ b/user/user.go @@ -13,8 +13,8 @@ import ( "time" ) -// TvUser contains the base access fields required for a TrueVault user -type TvUser struct { +// User contains the base access fields required for a TrueVault user +type User struct { AccessToken string `json:"access_token"` AccountID string `json:"account_id"` APIKey string `json:"api_key"` @@ -45,22 +45,20 @@ func (u *Status) String() string { return u.status } -// CRUDResponse contains the response from creating a new TrueVault User -type CRUDResponse struct { +type crudResponse struct { Result string `json:"result"` TransactionID string `json:"transaction_id"` - User TvUser `json:"user"` + User User `json:"user"` Error gotruevault.Error `json:"error"` } -// GetUserResponse contains the response from creating a new TrueVault User -type GetUserResponse struct { - Result string `json:"result"` - TransactionID string `json:"transaction_id"` - Users []TvUser `json:"users"` +type getUserResponse struct { + Result string `json:"result"` + TransactionID string `json:"transaction_id"` + Users []User `json:"users"` } -type CreateAPIKeyResponse struct { +type createAPIKeyResponse struct { ApiKey string `json:"api_key"` Result string `json:"result"` TransactionID string `json:"transaction_id"` @@ -68,24 +66,24 @@ type CreateAPIKeyResponse struct { //go:generate mockery --name Client type Client interface { - Get(ctx context.Context, userId []string, full bool) ([]TvUser, error) - Create(ctx context.Context, username, password, attributes string, groupIds []string, status Status, accessTokenNotValueAfter time.Time) (TvUser, error) - List(ctx context.Context, status Status, full bool) ([]TvUser, error) - Update(ctx context.Context, userId, username, password, accessToken string, accessTokenNotValueAfter time.Time, attributes string, status Status) (TvUser, error) + Get(ctx context.Context, userId []string, full bool) ([]User, error) + Create(ctx context.Context, username, password, attributes string, groupIds []string, status Status, accessTokenNotValueAfter time.Time) (User, error) + List(ctx context.Context, status Status, full bool) ([]User, error) + Update(ctx context.Context, userId, username, password, accessToken string, accessTokenNotValueAfter time.Time, attributes string, status Status) (User, error) UpdatePassword(ctx context.Context, userId, password string) error Delete(ctx context.Context, userID string) error CreateAccessToken(ctx context.Context, userId string, notValidAfter time.Time) error CreateAPIKey(ctx context.Context, userID string) error } -// User implements the Client interface -type User struct { +// Service implements the Client interface +type Service struct { *gotruevault.Client } -// New creates a new User service -func New(client gotruevault.Client) User { - return User{&client} +// New creates a new Service service +func New(client gotruevault.Client) Service { + return Service{&client} } // Get returns information about one or more users. If any IDs aren't valid UUIDs, returns a 400. If any can’t be @@ -94,10 +92,10 @@ func New(client gotruevault.Client) User { // Note: When full=true, this endpoint consumes an Operation for every user returned, so a request with 50 ids will // count as 50 Operations. When full=false, it consumes 1 operation regardless of how many users are returned // -// @param userIds - string(req’d) - comma separated list of user IDs to retrieve. At most 100 ids can be fetched at a time. -// @param full – boolean(optional, default: ‘false’) - return User attributes and Group IDs. Note: If true, then this +// userIds - string(req’d) - comma separated list of user IDs to retrieve. At most 100 ids can be fetched at a time. +// full – boolean(optional, default: ‘false’) - return Service attributes and Group IDs. Note: If true, then this // endpoint consumes an Operation for every user returned. If false, only a single Operation is used. -func (u *User) Get(ctx context.Context, userId []string, full bool) ([]TvUser, error) { +func (u *Service) Get(ctx context.Context, userId []string, full bool) ([]User, error) { if userId == nil || len(userId) == 0 { return nil, errors.New("user id required") } @@ -110,28 +108,28 @@ func (u *User) Get(ctx context.Context, userId []string, full bool) ([]TvUser, e return nil, err } - var msg GetUserResponse + var msg getUserResponse return msg.Users, u.Do(req, &msg) } -// Create creates a new TrueVault User. The username given must be unique to ACTIVATED and LOCKED Users for an +// Create creates a new TrueVault Service. The username given must be unique to ACTIVATED and LOCKED Users for an // Account. Upon creation, both an API_KEY and an ACCESS_TOKEN will be automatically vended to the user. For security // reasons, the API_KEY will only be shown upon creation or via the TrueVault Management Console for the account’s // administrators. If group_ids is provided, the newly created user will be added to all given groups. The user making // the request must have the C Group::GROUPID::GroupMembership::.* or U Group::GROUPID permission for all given groups. // Please see authorization for more information regarding recommendations for API_KEY and ACCESS_TOKEN usage. // -// @param username – string(req’d) - username for the User being created -// @param password – string(optional) - password for the User being created. If created without a password, the user +// username – string(req’d) - username for the Service being created +// password – string(optional) - password for the Service being created. If created without a password, the user // can’t authenticate using the login endpoint, but it can still have an API key. This allows creating // service accounts for backups or other server-to-TrueVault communication. -// @param attributes – b64 string(optional) - base64 encoded JSON document describing the User attributes -// @param groupIds – (optional) - list of group IDs where the new user will be placed -// @param status – (optional) - the user’s status, one of ACTIVATED (default), PENDING, or LOCKED -// @param accessTokenNotValueAfter – (optional) - expiration time of generated access token -func (u *User) Create(ctx context.Context, username, password, attributes string, groupIds []string, status Status, accessTokenNotValueAfter time.Time) (TvUser, error) { +// attributes – b64 string(optional) - base64 encoded JSON document describing the Service attributes +// groupIds – (optional) - list of group IDs where the new user will be placed +// status – (optional) - the user’s status, one of ACTIVATED (default), PENDING, or LOCKED +// accessTokenNotValueAfter – (optional) - expiration time of generated access token +func (u *Service) Create(ctx context.Context, username, password, attributes string, groupIds []string, status Status, accessTokenNotValueAfter time.Time) (User, error) { if username == "" { - return TvUser{}, errors.New("username required to create user") + return User{}, errors.New("username required to create user") } data := url.Values{} @@ -159,25 +157,24 @@ func (u *User) Create(ctx context.Context, username, password, attributes string buf := new(bytes.Buffer) if err := json.NewEncoder(buf).Encode(data); err != nil { - return TvUser{}, err + return User{}, err } req, err := u.NewRequest(ctx, http.MethodPost, u.URLBuilder.CreateUserURL(), gotruevault.ContentTypeApplicationJSON, buf) if err != nil { - return TvUser{}, err + return User{}, err } - var msg CRUDResponse + var msg crudResponse return msg.User, u.Do(req, &msg) } // List returns all Users belonging to an Account. -// // status – string(optional, default: ‘ACTIVATED’) - comma separated list of statuses (inclusive). Accepts any // combination of ACTIVATED, DEACTIVATED, or LOCKED. -// full – boolean(optional, default: ‘false’) - return User attributes and Group IDs. Note: If true, then this endpoint +// full – boolean(optional, default: ‘false’) - return Service attributes and Group IDs. Note: If true, then this endpoint // consumes an Operation for every user returned. If false, only a single Operation is used. -func (u *User) List(ctx context.Context, status Status, full bool) ([]TvUser, error) { +func (u *Service) List(ctx context.Context, status Status, full bool) ([]User, error) { q := make(url.Values) q.Set("status", status.String()) q.Set("full", strconv.FormatBool(full)) @@ -187,18 +184,17 @@ func (u *User) List(ctx context.Context, status Status, full bool) ([]TvUser, er return nil, err } - var msg GetUserResponse + var msg getUserResponse return msg.Users, u.Do(req, &msg) } -// Update a given User’s properties. Strictly overwrites existing values. -// -// @Param userId – string(required) -// @Param full – boolean(optional, default: ‘false’) - return User attributes and Group IDs. Note: If true, then this +// Update a given Service’s properties. Strictly overwrites existing values. +// userId – string(required) +// full – boolean(optional, default: ‘false’) - return Service attributes and Group IDs. Note: If true, then this // endpoint consumes an Operation for every user returned. If false, only a single Operation is used. -func (u *User) Update(ctx context.Context, userId, username, password, accessToken string, accessTokenNotValueAfter time.Time, attributes string, status Status) (TvUser, error) { +func (u *Service) Update(ctx context.Context, userId, username, password, accessToken string, accessTokenNotValueAfter time.Time, attributes string, status Status) (User, error) { if userId == "" { - return TvUser{}, errors.New("user id required to update user") + return User{}, errors.New("user id required to update user") } data := url.Values{} @@ -228,25 +224,24 @@ func (u *User) Update(ctx context.Context, userId, username, password, accessTok buf := new(bytes.Buffer) if err := json.NewEncoder(buf).Encode(data); err != nil { - return TvUser{}, err + return User{}, err } req, err := u.NewRequest(ctx, http.MethodPost, u.URLBuilder.UpdateUserURL(userId), gotruevault.ContentTypeApplicationJSON, buf) if err != nil { - return TvUser{}, err + return User{}, err } - var msg CRUDResponse + var msg crudResponse return msg.User, u.Do(req, &msg) } -// UpdatePassword Updates a given User’s password. Requires the `U` activity on the `User::USERID::Password` or -// `User::USERID resource`. +// UpdatePassword Updates a given Service’s password. Requires the `U` activity on the `Service::USERID::Password` or +// `Service::USERID resource`. // -// @Param userId – string(required) -// @Returns - nil on success -// - ErrorNotFound when user does not exist -func (u *User) UpdatePassword(ctx context.Context, userId, password string) error { +// userId – string(required) +// returns - nil on success otherwise, ErrorNotFound when user does not exist +func (u *Service) UpdatePassword(ctx context.Context, userId, password string) error { if userId == "" { return errors.New("user id required") } @@ -268,7 +263,7 @@ func (u *User) UpdatePassword(ctx context.Context, userId, password string) erro return err } - var msg CRUDResponse + var msg crudResponse if err := u.Do(req, &msg); err != nil { return err } @@ -281,11 +276,11 @@ func (u *User) UpdatePassword(ctx context.Context, userId, password string) erro } // Delete deactivates a user: frees the associated username, all ACCESS_TOKENs, and removes user_id from all Groups. -// @Warning: This endpoint does not delete any data permanently, unlike the Document and BLOB delete endpoints. If you +// warning - This endpoint does not delete any data permanently, unlike the Document and BLOB delete endpoints. If you // need to completely purge a user’s data for policy or compliance reasons, first update the user’s attributes // to be {}, then update their username to be a unique random string, then call this endpoint. -// @Warning: Once the user has been deactivated, it cannot be reactivated via a status update. -func (u *User) Delete(ctx context.Context, userID string) error { +// warning - Once the user has been deactivated, it cannot be reactivated via a status update. +func (u *Service) Delete(ctx context.Context, userID string) error { if userID == "" { return errors.New("user id required") } @@ -295,7 +290,7 @@ func (u *User) Delete(ctx context.Context, userID string) error { return err } - var msg CRUDResponse + var msg crudResponse if err := u.Do(req, &msg); err != nil { return err } @@ -308,7 +303,7 @@ func (u *User) Delete(ctx context.Context, userID string) error { } // CreateAccessToken Vends a new `ACCESS_TOKEN` for user_id. -func (u *User) CreateAccessToken(ctx context.Context, userId string, notValidAfter time.Time) error { +func (u *Service) CreateAccessToken(ctx context.Context, userId string, notValidAfter time.Time) error { if userId == "" { return errors.New("user id required") } @@ -326,7 +321,7 @@ func (u *User) CreateAccessToken(ctx context.Context, userId string, notValidAft return err } - var msg CRUDResponse + var msg crudResponse if err := u.Do(req, &msg); err != nil { return err } @@ -340,7 +335,7 @@ func (u *User) CreateAccessToken(ctx context.Context, userId string, notValidAft // CreateAPIKey replaces the current `API_KEY` for user_id. Companion to `ACCESS_TOKEN` method. Must have `U` group // permissions for the user. -func (u *User) CreateApiKey(ctx context.Context, userID string) (string, error) { +func (u *Service) CreateApiKey(ctx context.Context, userID string) (string, error) { if userID == "" { return "", errors.New("user id required") } @@ -350,6 +345,6 @@ func (u *User) CreateApiKey(ctx context.Context, userID string) (string, error) return "", err } - var msg CreateAPIKeyResponse + var msg createAPIKeyResponse return msg.ApiKey, u.Do(req, &msg) }