diff --git a/VERSION b/VERSION index 54d1a4f..a803cc2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.13.0 +0.14.0 diff --git a/authentication.go b/authentication.go index c0363fe..9f53ffc 100644 --- a/authentication.go +++ b/authentication.go @@ -71,7 +71,10 @@ func NewClient(appKey string, appSecret string, baseUri string) (Client, error) } func (c *Client) login() error { - loginBody := fmt.Sprintf("AppKey=%s&AppSecret=%s", c.credential.appKey, c.credential.appSecret) + form := url.Values{} + form.Set("AppKey", c.credential.appKey) + form.Set("AppSecret", c.credential.appSecret) + loginBody := form.Encode() reqUrl, err := url.JoinPath(c.baseUri, loginEndpoint) if err != nil { diff --git a/dvls.go b/dvls.go index 67d3ed5..c1ad2d6 100644 --- a/dvls.go +++ b/dvls.go @@ -2,6 +2,7 @@ package dvls import ( "encoding/json" + "errors" "fmt" "io" "net/http" @@ -16,8 +17,10 @@ type Response struct { } type RequestError struct { - Url string - Err error + Url string + StatusCode int + Body []byte + Err error } const defaultContentType string = "application/json" @@ -28,9 +31,23 @@ type RequestOptions struct { } func (e RequestError) Error() string { + if e.StatusCode != 0 { + return fmt.Sprintf("error while submitting request on url %s (status %d). error: %s", e.Url, e.StatusCode, e.Err.Error()) + } + return fmt.Sprintf("error while submitting request on url %s. error: %s", e.Url, e.Err.Error()) } +// IsNotFound reports whether the error is a DVLS RequestError with an HTTP 404 status code. +func IsNotFound(err error) bool { + var reqErr *RequestError + if errors.As(err, &reqErr) { + return reqErr.StatusCode == http.StatusNotFound + } + + return false +} + // Request returns a Response that contains the HTTP response body in bytes, the result code and result message. func (c *Client) Request(url string, reqMethod string, reqBody io.Reader, options ...RequestOptions) (Response, error) { islogged, err := c.isLogged() @@ -74,9 +91,12 @@ func (c *Client) rawRequest(url string, reqMethod string, contentType string, re if err != nil { return Response{}, &RequestError{Err: fmt.Errorf("error while submitting request. error: %w", err), Url: url} } + defer resp.Body.Close() if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { - return Response{}, &RequestError{Err: fmt.Errorf("unexpected status code %d", resp.StatusCode), Url: url} + body, _ := io.ReadAll(resp.Body) + + return Response{}, &RequestError{Err: fmt.Errorf("unexpected status code %d", resp.StatusCode), Url: url, StatusCode: resp.StatusCode, Body: body} } var response Response @@ -84,7 +104,6 @@ func (c *Client) rawRequest(url string, reqMethod string, contentType string, re if err != nil { return Response{}, &RequestError{Err: fmt.Errorf("failed to read response body. error: %w", err), Url: url} } - defer resp.Body.Close() if !opts.RawBody && len(response.Response) > 0 { err = json.Unmarshal(response.Response, &response) diff --git a/entry_credential.go b/entry_credential.go index e2aec5a..af78e53 100644 --- a/entry_credential.go +++ b/entry_credential.go @@ -109,6 +109,125 @@ func (e *Entry) GetCredentialPrivateKeyData() (*EntryCredentialPrivateKeyData, b return data, ok } +// ToCredentialMap flattens a credential entry into a map of fields keyed by a stable name. +// It always includes "entry-id" and "entry-name" and then subtype-specific keys. +func (e *Entry) ToCredentialMap() (map[string]string, error) { + if e.GetType() != EntryCredentialType { + return nil, fmt.Errorf("unsupported entry type (%s). Only %s is supported", e.GetType(), EntryCredentialType) + } + + secretMap := map[string]string{ + "entry-id": e.Id, + "entry-name": e.Name, + } + + switch e.SubType { + case EntryCredentialSubTypeDefault: + if data, ok := e.GetCredentialDefaultData(); ok { + if data.Username != "" { + secretMap["username"] = data.Username + } + if data.Password != "" { + secretMap["password"] = data.Password + } + if data.Domain != "" { + secretMap["domain"] = data.Domain + } + } + + case EntryCredentialSubTypeAccessCode: + if data, ok := e.GetCredentialAccessCodeData(); ok { + if data.Password != "" { + secretMap["password"] = data.Password + } + } + + case EntryCredentialSubTypeApiKey: + if data, ok := e.GetCredentialApiKeyData(); ok { + if data.ApiId != "" { + secretMap["api-id"] = data.ApiId + } + if data.ApiKey != "" { + secretMap["api-key"] = data.ApiKey + } + if data.TenantId != "" { + secretMap["tenant-id"] = data.TenantId + } + } + + case EntryCredentialSubTypeAzureServicePrincipal: + if data, ok := e.GetCredentialAzureServicePrincipalData(); ok { + if data.ClientId != "" { + secretMap["client-id"] = data.ClientId + } + if data.ClientSecret != "" { + secretMap["client-secret"] = data.ClientSecret + } + if data.TenantId != "" { + secretMap["tenant-id"] = data.TenantId + } + } + + case EntryCredentialSubTypeConnectionString: + if data, ok := e.GetCredentialConnectionStringData(); ok { + if data.ConnectionString != "" { + secretMap["connection-string"] = data.ConnectionString + } + } + + case EntryCredentialSubTypePrivateKey: + if data, ok := e.GetCredentialPrivateKeyData(); ok { + if data.Username != "" { + secretMap["username"] = data.Username + } + if data.Password != "" { + secretMap["password"] = data.Password + } + if data.PrivateKey != "" { + secretMap["private-key"] = data.PrivateKey + } + if data.PublicKey != "" { + secretMap["public-key"] = data.PublicKey + } + if data.Passphrase != "" { + secretMap["passphrase"] = data.Passphrase + } + } + + default: + return nil, fmt.Errorf("unsupported credential subtype (%s)", e.SubType) + } + + return secretMap, nil +} + +// SetCredentialSecret mutates the Entry data to update the secret value for supported subtypes. +// It preserves existing fields and only updates the password/secret field. +func (e *Entry) SetCredentialSecret(secret string) error { + if e.GetType() != EntryCredentialType { + return fmt.Errorf("unsupported entry type (%s). Only %s is supported", e.GetType(), EntryCredentialType) + } + + switch e.SubType { + case EntryCredentialSubTypeDefault: + if data, ok := e.GetCredentialDefaultData(); ok { + data.Password = secret + } else { + e.Data = &EntryCredentialDefaultData{Password: secret} + } + case EntryCredentialSubTypeAccessCode: + if data, ok := e.GetCredentialAccessCodeData(); ok { + data.Password = secret + } else { + e.Data = &EntryCredentialAccessCodeData{Password: secret} + } + default: + return fmt.Errorf("cannot set secret for credential subtype (%s)", e.SubType) + } + + return nil +} + // validateEntry checks if an Entry has the required fields and valid type/subtype. func (c *EntryCredentialService) validateEntry(entry *Entry) error { if entry.VaultId == "" { diff --git a/server.go b/server.go index 7477233..5375250 100644 --- a/server.go +++ b/server.go @@ -87,22 +87,29 @@ func (z *ServerTime) UnmarshalJSON(d []byte) error { return nil } - dateParsed, err := time.Parse(serverTimeLayout, s) - if err != nil { - return err + for _, layout := range serverTimeLayouts { + if dateParsed, err := time.Parse(layout, s); err == nil { + z.Time = dateParsed + return nil + } } - z.Time = dateParsed - return nil + return fmt.Errorf("cannot parse server time %q", s) } const ( serverPublicInfoEndpoint string = "api/public-instance-information" serverPrivateInfoEndpoint string = "api/private-instance-information" serverTimezonesEndpoint string = "/api/configuration/timezones" - serverTimeLayout string = "2006-01-02T15:04:05" ) +var serverTimeLayouts = []string{ + time.RFC3339Nano, + "2006-01-02T15:04:05.9999999Z07:00", + "2006-01-02T15:04:05.9999999Z", + "2006-01-02T15:04:05", +} + // GetPublicServerInfo returns Server that contains public information on the DVLS instance. func (c *Client) GetPublicServerInfo() (Server, error) { var server Server