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 +} diff --git a/user/mocks/Client.go b/user/mocks/Client.go new file mode 100644 index 0000000..3d11b7a --- /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.User, error) { + ret := _m.Called(ctx, username, password, attributes, groupIds, status, accessTokenNotValueAfter) + + 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.User) + } + + 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.User, error) { + ret := _m.Called(ctx, userId, full) + + 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.User) + } + } + + 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.User, error) { + ret := _m.Called(ctx, status, full) + + 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.User) + } + } + + 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.User, error) { + ret := _m.Called(ctx, userId, username, password, accessToken, accessTokenNotValueAfter, attributes, status) + + 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.User) + } + + 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..adf727e --- /dev/null +++ b/user/user.go @@ -0,0 +1,350 @@ +package user + +import ( + "bytes" + "context" + "encoding/json" + "errors" + gotruevault "github.com/FirstVisit/go-truevault" + "net/http" + "net/url" + "strconv" + "strings" + "time" +) + +// 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"` + 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 struct { + status string +} + +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 +} + +type crudResponse struct { + Result string `json:"result"` + TransactionID string `json:"transaction_id"` + User User `json:"user"` + Error gotruevault.Error `json:"error"` +} + +type getUserResponse struct { + Result string `json:"result"` + TransactionID string `json:"transaction_id"` + Users []User `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) ([]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 +} + +// Service implements the Client interface +type Service struct { + *gotruevault.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 +// 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 +// +// 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 *Service) Get(ctx context.Context, userId []string, full bool) ([]User, 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 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. +// +// 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. +// 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 User{}, 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, ",")) + } + + if status.String() != "" { + data.Set("status", status.String()) + } + + 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 User{}, err + } + + req, err := u.NewRequest(ctx, http.MethodPost, u.URLBuilder.CreateUserURL(), gotruevault.ContentTypeApplicationJSON, buf) + if err != nil { + return User{}, 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 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 *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)) + + 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 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 *Service) Update(ctx context.Context, userId, username, password, accessToken string, accessTokenNotValueAfter time.Time, attributes string, status Status) (User, error) { + if userId == "" { + return User{}, 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) + } + + if status.String() != "" { + data.Set("status", status.String()) + } + + buf := new(bytes.Buffer) + if err := json.NewEncoder(buf).Encode(data); err != nil { + return User{}, err + } + + req, err := u.NewRequest(ctx, http.MethodPost, u.URLBuilder.UpdateUserURL(userId), gotruevault.ContentTypeApplicationJSON, buf) + if err != nil { + return User{}, err + } + + var msg crudResponse + return msg.User, u.Do(req, &msg) +} + +// UpdatePassword Updates a given Service’s password. Requires the `U` activity on the `Service::USERID::Password` or +// `Service::USERID resource`. +// +// 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") + } + + 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 *Service) 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 *Service) 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 *Service) 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) +}