diff --git a/go.mod b/go.mod index 6ec4dde2..11a018b1 100644 --- a/go.mod +++ b/go.mod @@ -291,6 +291,6 @@ replace certwarden-backend/pkg/storage => /pkg/storage replace certwarden-backend/pkg/storage/sqlite3 => /pkg/storage/sqlite3 -replace certwarden-backend/pkg/test_helpers => /pkg/test_helpers +replace certwarden-backend/pkg/helpers_test => /pkg/helpers_test replace certwarden-backend/pkg/validation => /pkg/validation diff --git a/pkg/domain/acme_accounts/handlers_post.go b/pkg/domain/acme_accounts/handlers_post.go index 7943f2c7..a5e5b83d 100644 --- a/pkg/domain/acme_accounts/handlers_post.go +++ b/pkg/domain/acme_accounts/handlers_post.go @@ -20,16 +20,16 @@ import ( // NewPayload is the payload struct for creating a new account type NewPayload struct { - Name *string `json:"name"` - Description *string `json:"description"` - AcmeServerID *int `json:"acme_server_id"` - PrivateKeyID *int `json:"private_key_id"` - Status string `json:"-"` - Email *string `json:"email"` - AcceptedTos *bool `json:"accepted_tos"` - CreatedAt int `json:"-"` - UpdatedAt int `json:"-"` - Kid string `json:"-"` + Name *string `json:"name"` + Description *string `json:"description"` + AcmeServerID *int `json:"acme_server_id"` + PrivateKeyID *int `json:"private_key_id"` + Status string `json:"-"` + Email *string `json:"email"` + AcceptedTos *bool `json:"accepted_tos"` + CreatedAt time.Time `json:"-"` + UpdatedAt time.Time `json:"-"` + Kid string `json:"-"` } // PostNewAccount is the handler to save a new account to storage. No ACME @@ -89,8 +89,9 @@ func (service *Service) PostNewAccount(w http.ResponseWriter, r *http.Request) * // add additional details to the payload before saving payload.Status = "unknown" - payload.CreatedAt = int(time.Now().Unix()) - payload.UpdatedAt = payload.CreatedAt + t := time.Now() + payload.CreatedAt = t + payload.UpdatedAt = t payload.Kid = "" // Save new account details to storage. diff --git a/pkg/domain/acme_servers/handlers_post.go b/pkg/domain/acme_servers/handlers_post.go index b5d93ca0..e66a9ee0 100644 --- a/pkg/domain/acme_servers/handlers_post.go +++ b/pkg/domain/acme_servers/handlers_post.go @@ -12,12 +12,12 @@ import ( // NewPayload is used to post a new Server type NewPayload struct { - Name *string `json:"name"` - Description *string `json:"description"` - DirectoryURL *string `json:"directory_url"` - IsStaging *bool `json:"is_staging"` - CreatedAt int `json:"-"` - UpdatedAt int `json:"-"` + Name *string `json:"name"` + Description *string `json:"description"` + DirectoryURL *string `json:"directory_url"` + IsStaging *bool `json:"is_staging"` + CreatedAt time.Time `json:"-"` + UpdatedAt time.Time `json:"-"` } // PostNewServer creates a new server, saves it to storage, and starts an *acme.Service @@ -62,8 +62,9 @@ func (service *Service) PostNewServer(w http.ResponseWriter, r *http.Request) *o // end validation // add additional details to the payload before saving - payload.CreatedAt = int(time.Now().Unix()) - payload.UpdatedAt = payload.CreatedAt + t := time.Now() + payload.CreatedAt = t + payload.UpdatedAt = t // save new key to storage, which also returns the new server newServer, err := service.storage.PostNewServer(payload) diff --git a/pkg/domain/acme_servers/handlers_put.go b/pkg/domain/acme_servers/handlers_put.go index 07b35b7a..a31b501e 100644 --- a/pkg/domain/acme_servers/handlers_put.go +++ b/pkg/domain/acme_servers/handlers_put.go @@ -15,12 +15,12 @@ import ( // UpdatePayload is the struct for editing an existing Server's // information (only certain fields are editable) type UpdatePayload struct { - ID int `json:"-"` - Name *string `json:"name"` - Description *string `json:"description"` - DirectoryURL *string `json:"directory_url"` - IsStaging *bool `json:"is_staging"` - UpdatedAt int `json:"-"` + ID int `json:"-"` + Name *string `json:"name"` + Description *string `json:"description"` + DirectoryURL *string `json:"directory_url"` + IsStaging *bool `json:"is_staging"` + UpdatedAt time.Time `json:"-"` } // PutServerUpdate updates a Server that already exists in storage. @@ -66,7 +66,7 @@ func (service *Service) PutServerUpdate(w http.ResponseWriter, r *http.Request) // end validation // add additional details to the payload before saving - payload.UpdatedAt = int(time.Now().Unix()) + payload.UpdatedAt = time.Now() // save updated key info to storage updatedServer, err := service.storage.PutServerUpdate(payload) diff --git a/pkg/domain/certificates/certificate.go b/pkg/domain/certificates/certificate.go index aba02854..e636ac8c 100644 --- a/pkg/domain/certificates/certificate.go +++ b/pkg/domain/certificates/certificate.go @@ -12,8 +12,8 @@ type Certificate struct { ID int Name string Description string - CertificateKey private_keys.Key - CertificateAccount acme_accounts.Account + Key private_keys.Key + Account acme_accounts.Account Subject string SubjectAltNames []string Organization string @@ -39,15 +39,15 @@ type Certificate struct { // certificateSummaryResponse is a JSON response containing only // fields desired for the summary type certificateSummaryResponse struct { - ID int `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - CertificateKey certificateKeySummaryResponse `json:"private_key"` - CertificateAccount certificateAccountSummaryResponse `json:"acme_account"` - Subject string `json:"subject"` - SubjectAltNames []string `json:"subject_alts"` - ApiKeyViaUrl bool `json:"api_key_via_url"` - LastAccess int64 `json:"last_access"` + ID int `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Key certificateKeySummaryResponse `json:"private_key"` + Account certificateAccountSummaryResponse `json:"acme_account"` + Subject string `json:"subject"` + SubjectAltNames []string `json:"subject_alts"` + ApiKeyViaUrl bool `json:"api_key_via_url"` + LastAccess int64 `json:"last_access"` } type certificateKeySummaryResponse struct { @@ -73,18 +73,18 @@ func (cert Certificate) summaryResponse() certificateSummaryResponse { ID: cert.ID, Name: cert.Name, Description: cert.Description, - CertificateKey: certificateKeySummaryResponse{ - ID: cert.CertificateKey.ID, - Name: cert.CertificateKey.Name, - Algorithm: cert.CertificateKey.Algorithm, + Key: certificateKeySummaryResponse{ + ID: cert.Key.ID, + Name: cert.Key.Name, + Algorithm: cert.Key.Algorithm, }, - CertificateAccount: certificateAccountSummaryResponse{ - ID: cert.CertificateAccount.ID, - Name: cert.CertificateAccount.Name, + Account: certificateAccountSummaryResponse{ + ID: cert.Account.ID, + Name: cert.Account.Name, CertAccountServer: certificateAccountServerSummaryResponse{ - ID: cert.CertificateAccount.AcmeServer.ID, - Name: cert.CertificateAccount.AcmeServer.Name, - IsStaging: cert.CertificateAccount.AcmeServer.IsStaging, + ID: cert.Account.AcmeServer.ID, + Name: cert.Account.AcmeServer.Name, + IsStaging: cert.Account.AcmeServer.IsStaging, }, }, Subject: cert.Subject, @@ -98,32 +98,25 @@ func (cert Certificate) summaryResponse() certificateSummaryResponse { // fields that can be returned as JSON type certificateDetailedResponse struct { certificateSummaryResponse - Organization string `json:"organization"` - OrganizationalUnit string `json:"organizational_unit"` - Country string `json:"country"` - State string `json:"state"` - City string `json:"city"` - CSRExtraExtensions []CertExtensionJSON `json:"csr_extra_extensions"` - PreferredRootCN string `json:"preferred_root_cn"` - Profile string `json:"profile"` - CreatedAt int64 `json:"created_at"` - UpdatedAt int64 `json:"updated_at"` - ApiKey string `json:"api_key"` - ApiKeyNew string `json:"api_key_new,omitempty"` - PostProcessingCommand string `json:"post_processing_command"` - PostProcessingEnvironment []string `json:"post_processing_environment"` - PostProcessingClientAddress string `json:"post_processing_client_address"` - PostProcessingClientKeyB64 string `json:"post_processing_client_key"` + Organization string `json:"organization"` + OrganizationalUnit string `json:"organizational_unit"` + Country string `json:"country"` + State string `json:"state"` + City string `json:"city"` + CSRExtraExtensions []CertExtension `json:"csr_extra_extensions"` + PreferredRootCN string `json:"preferred_root_cn"` + Profile string `json:"profile"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` + ApiKey string `json:"api_key"` + ApiKeyNew string `json:"api_key_new,omitempty"` + PostProcessingCommand string `json:"post_processing_command"` + PostProcessingEnvironment []string `json:"post_processing_environment"` + PostProcessingClientAddress string `json:"post_processing_client_address"` + PostProcessingClientKeyB64 string `json:"post_processing_client_key"` } func (cert Certificate) detailedResponse() certificateDetailedResponse { - // convert extensions to json output obj - extraExtensions := []CertExtensionJSON{} - for i := range cert.CSRExtraExtensions { - oneExt := cert.CSRExtraExtensions[i].toJSONObj() - extraExtensions = append(extraExtensions, oneExt) - } - return certificateDetailedResponse{ certificateSummaryResponse: cert.summaryResponse(), Organization: cert.Organization, @@ -131,7 +124,7 @@ func (cert Certificate) detailedResponse() certificateDetailedResponse { Country: cert.Country, State: cert.State, City: cert.City, - CSRExtraExtensions: extraExtensions, + CSRExtraExtensions: cert.CSRExtraExtensions, PreferredRootCN: cert.PreferredRootCN, Profile: cert.Profile, CreatedAt: cert.CreatedAt.Unix(), diff --git a/pkg/domain/certificates/certificate_extra_extn.go b/pkg/domain/certificates/certificate_extra_extn.go index 2eb9a4c6..477cf7f1 100644 --- a/pkg/domain/certificates/certificate_extra_extn.go +++ b/pkg/domain/certificates/certificate_extra_extn.go @@ -4,16 +4,13 @@ import ( "crypto/x509/pkix" "encoding/asn1" "encoding/hex" + "encoding/json" "errors" + "fmt" "strconv" "strings" ) -var ( - errCertExtOIDBadFormat = errors.New("certificates extension: OID string invalid (must be in dot notation)") - errCertExtValueBad = errors.New("certificates extension: Value invalid (must be hex string, hex string with colons, or hex string with spaces)") -) - // CertExtension us a pkix.Extension with an additional field for // a description type CertExtension struct { @@ -21,84 +18,142 @@ type CertExtension struct { Description string } -// CertExtensionJSON is the object to use in the API (both input and output) -// to represent the custom CertificateExtension -type CertExtensionJSON struct { - Description string `json:"description"` - OID string `json:"oid"` - Critical bool `json:"critical"` - ValueHexString string `json:"value_hex"` +// String prints a log friendly version of the Certificate Extension (useful for testing) +func (cext CertExtension) String() string { + return fmt.Sprintf("CertExtension{Description: %s, Id: %s, Critical: %t, Value: %x}", cext.Description, cext.Id.String(), cext.Critical, cext.Value) } -// toJSONObj returns the JSON object of the custom CertExtension -func (ce CertExtension) toJSONObj() CertExtensionJSON { - return CertExtensionJSON{ - Description: ce.Description, - OID: ce.Id.String(), - Critical: ce.Critical, - ValueHexString: hex.EncodeToString(ce.Value), +// UnmarshalJSON implements the unmarshalling interface for CertExtension +// json should be in the shape: +// +// { +// Description string `json:"description"` +// OID string `json:"oid"` +// Critical bool `json:"critical"` +// ValueHexString string `json:"value_hex"` +// } +func (cext *CertExtension) UnmarshalJSON(b []byte) error { + // generic unmarshal + m := map[string]interface{}{} + err := json.Unmarshal(b, &m) + if err != nil { + return err } -} -// toCertExtension validates the CertExtensionJSON and then returns -// the CertExtension object; if any fields fail to validate, an error -// is returned instead -func (cej CertExtensionJSON) ToCertExtension() (CertExtension, error) { - ce := CertExtension{} + result := CertExtension{} - // Description - no validation needed - ce.Description = cej.Description + // description + desc, ok := m["description"] + if !ok { + return errors.New("'description' missing") + } + result.Description, ok = desc.(string) + if !ok { + return errors.New("'description' is not string type") + } + + // oid - must convert into asn1.ObjectIdentifier (must be in dot notation) + desc, ok = m["oid"] + if !ok { + return errors.New("'oid' missing") + } + oidStr, ok := desc.(string) + if !ok { + return errors.New("'oid' is not string type") + } - // OID - must convert into asn1.ObjectIdentifier (must be in dot notation) - var err error - oidParts := strings.Split(cej.OID, ".") + oidParts := strings.Split(oidStr, ".") id := make(asn1.ObjectIdentifier, len(oidParts)) for i := range oidParts { id[i], err = strconv.Atoi(oidParts[i]) if err != nil { - return CertExtension{}, errCertExtOIDBadFormat + return errors.New("invalid oid format") } } - ce.Id = id + result.Id = id - // Cricial - no validation needed - ce.Critical = cej.Critical + // critical + desc, ok = m["critical"] + if !ok { + return errors.New("'critical' missing") + } + result.Critical, ok = desc.(bool) + if !ok { + return errors.New("'critical' is not bool type") + } + + // value - must convert from hex string to []byte + // allow no delimiter, ':' (colon), or ' ' (space) as delimiter + desc, ok = m["value_hex"] + if !ok { + return errors.New("'value_hex' missing") + } + valueHex, ok := desc.(string) + if !ok { + return errors.New("'value_hex' is not string type") + } - // ValueByteString - must be a valid hex byte string; will try a couple of parsing - // options (allow bytes to be separated by colons or spaces) - valueParts := []string{} - if strings.Contains(cej.ValueHexString, ":") { + valueHexParts := []string{} + // check for and deal with ':' or ' ' separation + if strings.Contains(valueHex, ":") { // has colons - valueParts = strings.Split(cej.ValueHexString, ":") - } else if strings.Contains(cej.ValueHexString, " ") { + valueHexParts = strings.Split(valueHex, ":") + } else if strings.Contains(valueHex, " ") { // has spaces - valueParts = strings.Split(cej.ValueHexString, " ") + valueHexParts = strings.Split(valueHex, " ") } // if we made value parts, build hex without separator string from them, if we did // not, use original hex value valueHexNoSep := "" - if len(valueParts) > 0 { - for i := range valueParts { + if len(valueHexParts) > 0 { + for i := range valueHexParts { // each byte must be explicityly two chars long - if len(valueParts[i]) != 2 { + if len(valueHexParts[i]) != 2 { // fail if not - return CertExtension{}, errCertExtValueBad + return fmt.Errorf("invalid value byte '%s' (not 2 chars)", valueHexParts[i]) } // add byte to the no seperation string - valueHexNoSep += valueParts[i] + valueHexNoSep += valueHexParts[i] } } else { // no separator was found, use as-is - valueHexNoSep = cej.ValueHexString + valueHexNoSep = valueHex } // decode hex string - ce.Value, err = hex.DecodeString(valueHexNoSep) + result.Value, err = hex.DecodeString(valueHexNoSep) if err != nil { - return CertExtension{}, errCertExtValueBad + return fmt.Errorf("failed to decode hex '%s'", valueHexNoSep) + } + + // good to go + *cext = result + return nil +} + +// MarshalJSON implements the marshalling interface for CertExtension +// and the output json will be in the shape: +// +// { +// Description string `json:"description"` +// OID string `json:"oid"` +// Critical bool `json:"critical"` +// ValueHexString string `json:"value_hex"` +// } +func (cext *CertExtension) MarshalJSON() ([]byte, error) { + out := struct { + Description string `json:"description"` + OID string `json:"oid"` + Critical bool `json:"critical"` + ValueHexString string `json:"value_hex"` + }{ + Description: cext.Description, + OID: cext.Id.String(), + Critical: cext.Critical, + ValueHexString: hex.EncodeToString(cext.Value), } - return ce, nil + return json.Marshal(out) } diff --git a/pkg/domain/certificates/csr.go b/pkg/domain/certificates/csr.go index ded45d92..660b3633 100644 --- a/pkg/domain/certificates/csr.go +++ b/pkg/domain/certificates/csr.go @@ -56,7 +56,7 @@ func (cert *Certificate) MakeCsrDer() (csr []byte, err error) { // CSR template to create CSR from template := x509.CertificateRequest{ - SignatureAlgorithm: cert.CertificateKey.Algorithm.CsrSigningAlg(), + SignatureAlgorithm: cert.Key.Algorithm.CsrSigningAlg(), Subject: subj, DNSNames: append([]string{cert.Subject}, cert.SubjectAltNames...), // unused: EmailAddresses, IPAddresses, URIs, Attributes (deprecated) @@ -64,7 +64,7 @@ func (cert *Certificate) MakeCsrDer() (csr []byte, err error) { } // cert's private key for signing - certKey, err := key_crypto.PemStringToKey(cert.CertificateKey.Pem, cert.CertificateKey.Algorithm) + certKey, err := key_crypto.PemStringToKey(cert.Key.Pem, cert.Key.Algorithm) if err != nil { return nil, err } diff --git a/pkg/domain/certificates/handlers_delete.go b/pkg/domain/certificates/handlers_delete.go index ba40b51c..9084e8b3 100644 --- a/pkg/domain/certificates/handlers_delete.go +++ b/pkg/domain/certificates/handlers_delete.go @@ -76,7 +76,7 @@ func (service *Service) RemoveOldApiKey(w http.ResponseWriter, r *http.Request) // update storage // set current api key from new key - err = service.storage.PutCertApiKey(certId, cert.ApiKeyNew, int(time.Now().Unix())) + err = service.storage.PutCertApiKey(certId, cert.ApiKeyNew, time.Now()) if err != nil { service.logger.Error(err) return output.JsonErrStorageGeneric(err) @@ -84,7 +84,7 @@ func (service *Service) RemoveOldApiKey(w http.ResponseWriter, r *http.Request) cert.ApiKey = cert.ApiKeyNew // set new key to blank - err = service.storage.PutCertNewApiKey(certId, "", int(time.Now().Unix())) + err = service.storage.PutCertApiKeyNew(certId, "", time.Now()) if err != nil { service.logger.Error(err) return output.JsonErrStorageGeneric(err) @@ -126,7 +126,7 @@ func (service *Service) DisableClientKey(w http.ResponseWriter, r *http.Request) // validation -- end // update storage - err = service.storage.PutCertClientKey(certId, "", int(time.Now().Unix())) + err = service.storage.PutCertClientKey(certId, "", time.Now()) if err != nil { service.logger.Error(err) return output.JsonErrStorageGeneric(err) diff --git a/pkg/domain/certificates/handlers_post.go b/pkg/domain/certificates/handlers_post.go index afbf9c66..136e1fda 100644 --- a/pkg/domain/certificates/handlers_post.go +++ b/pkg/domain/certificates/handlers_post.go @@ -18,29 +18,29 @@ import ( // NewPayload is the struct for creating a new certificate type NewPayload struct { - Name *string `json:"name"` - Description *string `json:"description"` - PrivateKeyID *int `json:"private_key_id"` - NewKeyAlgorithmValue *string `json:"algorithm_value"` - AcmeAccountID *int `json:"acme_account_id"` - Subject *string `json:"subject"` - SubjectAltNames []string `json:"subject_alts"` - Organization *string `json:"organization"` - OrganizationalUnit *string `json:"organizational_unit"` - Country *string `json:"country"` - State *string `json:"state"` - City *string `json:"city"` - CSRExtraExtensions []CertExtensionJSON `json:"csr_extra_extensions"` - PreferredRootCN *string `json:"preferred_root_cn"` - PostProcessingCommand *string `json:"post_processing_command"` - PostProcessingEnvironment []string `json:"post_processing_environment"` - PostProcessingClientAddress *string `json:"post_processing_client_address"` - PostProcessingClientKeyB64 string `json:"-"` - Profile *string `json:"profile"` - ApiKey string `json:"-"` - ApiKeyViaUrl bool `json:"-"` - CreatedAt int `json:"-"` - UpdatedAt int `json:"-"` + Name *string `json:"name"` + Description *string `json:"description"` + PrivateKeyID *int `json:"private_key_id"` + NewKeyAlgorithmValue *string `json:"algorithm_value"` + AcmeAccountID *int `json:"acme_account_id"` + Subject *string `json:"subject"` + SubjectAltNames []string `json:"subject_alts"` + Organization *string `json:"organization"` + OrganizationalUnit *string `json:"organizational_unit"` + Country *string `json:"country"` + State *string `json:"state"` + City *string `json:"city"` + CSRExtraExtensions []CertExtension `json:"csr_extra_extensions"` + PreferredRootCN *string `json:"preferred_root_cn"` + PostProcessingCommand *string `json:"post_processing_command"` + PostProcessingEnvironment []string `json:"post_processing_environment"` + PostProcessingClientAddress *string `json:"post_processing_client_address"` + PostProcessingClientKeyB64 *string `json:"post_processing_client_key"` + Profile *string `json:"profile"` + ApiKey string `json:"-"` + ApiKeyViaUrl bool `json:"-"` + CreatedAt time.Time `json:"-"` + UpdatedAt time.Time `json:"-"` } // PostNewCert creates a new certificate object in storage. No actual encryption certificate @@ -165,14 +165,7 @@ func (service *Service) PostNewCert(w http.ResponseWriter, r *http.Request) *out payload.City = new(string) } - // CSR Extra Extensions - check each extra extension for proper formatting - for i := range payload.CSRExtraExtensions { - _, err = payload.CSRExtraExtensions[i].ToCertExtension() - if err != nil { - service.logger.Debug(err) - return output.JsonErrValidationFailed(err) - } - } + // CSR Extra Extensions - error checking is now in the custom unmarshal function if payload.PreferredRootCN == nil { payload.PreferredRootCN = new(string) @@ -195,28 +188,37 @@ func (service *Service) PostNewCert(w http.ResponseWriter, r *http.Request) *out return output.JsonErrValidationFailed(ErrClientAddressBad) } } + // post processing aes key (if specified) + if payload.PostProcessingClientKeyB64 != nil { + valid := clientKeyB64Valid(*payload.PostProcessingClientKeyB64) + if !valid { + service.logger.Debug(ErrPostProcessingClientKeyB64Bad) + return output.JsonErrValidationFailed(ErrPostProcessingClientKeyB64Bad) + } + } // end validation // if new private key was generated, save it to storage + createdAtAndUpdatedAt := time.Now() if generatedKeyPem != "" { + apiKey, err := randomness.GenerateApiKey() + if err != nil { + service.logger.Error(err) + return output.JsonErrInternal(err) + } + // create new key payload newKeyPayload := private_keys.NewPayload{ Name: payload.Name, Description: payload.Description, AlgorithmValue: payload.NewKeyAlgorithmValue, PemContent: &generatedKeyPem, - ApiKeyDisabled: new(bool), + ApiKeyDisabled: new(false), ApiKeyViaUrl: payload.ApiKeyViaUrl, + ApiKey: apiKey, + CreatedAt: createdAtAndUpdatedAt, + UpdatedAt: createdAtAndUpdatedAt, } - // set additional new key payload fields - newKeyPayload.ApiKey, err = randomness.GenerateApiKey() - if err != nil { - service.logger.Error(err) - return output.JsonErrInternal(err) - } - *newKeyPayload.ApiKeyDisabled = false - newKeyPayload.CreatedAt = int(time.Now().Unix()) - newKeyPayload.UpdatedAt = payload.CreatedAt // save new key to storage, and set the cert key id based on returned key's id newKey, err := service.storage.PostNewKey(newKeyPayload) @@ -234,15 +236,23 @@ func (service *Service) PostNewCert(w http.ResponseWriter, r *http.Request) *out return output.JsonErrInternal(err) } payload.ApiKeyViaUrl = false - payload.CreatedAt = int(time.Now().Unix()) - payload.UpdatedAt = payload.CreatedAt - // if client address specified, generate key to save (b64 raw url encoded) - if payload.PostProcessingClientAddress != nil && *payload.PostProcessingClientAddress != "" { - payload.PostProcessingClientKeyB64, err = randomness.GenerateAES256KeyAsBase64RawUrl() - if err != nil { - err = fmt.Errorf("failed to generate client key for certificate (%s)", err) - service.logger.Error(err) - return output.JsonErrInternal(err) + payload.CreatedAt = createdAtAndUpdatedAt + payload.UpdatedAt = createdAtAndUpdatedAt + + // if client address specified but no aes key, generate key to save (b64 raw url encoded) + if payload.PostProcessingClientKeyB64 == nil { + // empty if no processing address + payload.PostProcessingClientKeyB64 = new("") + + // processing address & user didnt specify an aes key -- generate one + if payload.PostProcessingClientAddress != nil && *payload.PostProcessingClientAddress != "" { + key, err := randomness.GenerateAES256KeyAsBase64RawUrl() + if err != nil { + err = fmt.Errorf("failed to generate client key for certificate (%s)", err) + service.logger.Error(err) + return output.JsonErrInternal(err) + } + payload.PostProcessingClientKeyB64 = &key } } @@ -301,7 +311,7 @@ func (service *Service) StageNewApiKey(w http.ResponseWriter, r *http.Request) * } // update storage - err = service.storage.PutCertNewApiKey(certId, newApiKey, int(time.Now().Unix())) + err = service.storage.PutCertApiKeyNew(certId, newApiKey, time.Now()) if err != nil { service.logger.Error(err) return output.JsonErrStorageGeneric(err) @@ -351,7 +361,7 @@ func (service *Service) MakeNewClientKey(w http.ResponseWriter, r *http.Request) } // update storage - err = service.storage.PutCertClientKey(certId, clientKey, int(time.Now().Unix())) + err = service.storage.PutCertClientKey(certId, clientKey, time.Now()) if err != nil { service.logger.Error(err) return output.JsonErrStorageGeneric(err) diff --git a/pkg/domain/certificates/handlers_put.go b/pkg/domain/certificates/handlers_put.go index eea37680..02f41479 100644 --- a/pkg/domain/certificates/handlers_put.go +++ b/pkg/domain/certificates/handlers_put.go @@ -12,36 +12,37 @@ import ( "github.com/julienschmidt/httprouter" ) -// DetailsUpdatePayload is the struct for editing an existing cert. A number of +// UpdatePayload is the struct for editing an existing cert. A number of // fields can be updated by the client on the fly (without ACME interaction). -type DetailsUpdatePayload struct { - ID int `json:"-"` - Name *string `json:"name"` - Description *string `json:"description"` - PrivateKeyId *int `json:"private_key_id"` - SubjectAltNames []string `json:"subject_alts"` - Organization *string `json:"organization"` - OrganizationalUnit *string `json:"organizational_unit"` - Country *string `json:"country"` - State *string `json:"state"` - City *string `json:"city"` - CSRExtraExtensions []CertExtensionJSON `json:"csr_extra_extensions"` - PreferredRootCN *string `json:"preferred_root_cn"` - PostProcessingCommand *string `json:"post_processing_command"` - PostProcessingEnvironment []string `json:"post_processing_environment"` - PostProcessingClientAddress *string `json:"post_processing_client_address"` - Profile *string `json:"profile"` - ApiKey *string `json:"api_key"` - ApiKeyNew *string `json:"api_key_new"` - ApiKeyViaUrl *bool `json:"api_key_via_url"` - UpdatedAt int `json:"-"` +type UpdatePayload struct { + ID int `json:"-"` + Name *string `json:"name"` + Description *string `json:"description"` + PrivateKeyId *int `json:"private_key_id"` + SubjectAltNames []string `json:"subject_alts"` + Organization *string `json:"organization"` + OrganizationalUnit *string `json:"organizational_unit"` + Country *string `json:"country"` + State *string `json:"state"` + City *string `json:"city"` + CSRExtraExtensions []CertExtension `json:"csr_extra_extensions"` + PreferredRootCN *string `json:"preferred_root_cn"` + PostProcessingCommand *string `json:"post_processing_command"` + PostProcessingEnvironment []string `json:"post_processing_environment"` + PostProcessingClientAddress *string `json:"post_processing_client_address"` + PostProcessingClientKeyB64 *string `json:"post_processing_client_key"` + Profile *string `json:"profile"` + ApiKey *string `json:"api_key"` + ApiKeyNew *string `json:"api_key_new"` + ApiKeyViaUrl *bool `json:"api_key_via_url"` + UpdatedAt time.Time `json:"-"` } // PutDetailsCert is a handler that sets various details about a cert and saves // them to storage. These are all details that should be editable any time. func (service *Service) PutDetailsCert(w http.ResponseWriter, r *http.Request) *output.JsonError { // payload decoding - var payload DetailsUpdatePayload + var payload UpdatePayload err := json.NewDecoder(r.Body).Decode(&payload) if err != nil { service.logger.Debug(err) @@ -92,7 +93,7 @@ func (service *Service) PutDetailsCert(w http.ResponseWriter, r *http.Request) * // profile Extension -- validate if specified if payload.Profile != nil && *payload.Profile != "" { // specified, validate against acme service - acmeService, err := service.acmeServerService.AcmeService(cert.CertificateAccount.AcmeServer.ID) + acmeService, err := service.acmeServerService.AcmeService(cert.Account.AcmeServer.ID) if err != nil { err = fmt.Errorf("failed to retrieve acme service (%s)", err) service.logger.Error(err) @@ -114,23 +115,13 @@ func (service *Service) PutDetailsCert(w http.ResponseWriter, r *http.Request) * service.logger.Debug(ErrApiKeyNewBad) return output.JsonErrValidationFailed(ErrApiKeyNewBad) } - // TODO: Do any validation of CSR components? - // CSR Extra Extensions - check each extra extension for proper formatting - for i := range payload.CSRExtraExtensions { - _, err = payload.CSRExtraExtensions[i].ToCertExtension() - if err != nil { - service.logger.Debug(err) - return output.JsonErrValidationFailed(err) - } - } + // CSR Extra Extensions - error checking is now in the custom unmarshal function // post processing command & env are optional but nothing to validate // post processing address - if payload.PostProcessingClientAddress == nil { - payload.PostProcessingClientAddress = new(string) - } else if *payload.PostProcessingClientAddress != "" { + if payload.PostProcessingClientAddress != nil && *payload.PostProcessingClientAddress != "" { valid := validation.DomainAndPortValid(*payload.PostProcessingClientAddress) if !valid { service.logger.Debug(ErrClientAddressBad) @@ -138,14 +129,23 @@ func (service *Service) PutDetailsCert(w http.ResponseWriter, r *http.Request) * } } + // post processing aes key (if specified) + if payload.PostProcessingClientKeyB64 != nil { + valid := clientKeyB64Valid(*payload.PostProcessingClientKeyB64) + if !valid { + service.logger.Debug(ErrPostProcessingClientKeyB64Bad) + return output.JsonErrValidationFailed(ErrPostProcessingClientKeyB64Bad) + } + } + // end validation // add additional details to the payload before saving - payload.UpdatedAt = int(time.Now().Unix()) + payload.UpdatedAt = time.Now() // save account name and desc to storage, which also returns the account id with new // name and description - updatedCert, err := service.storage.PutDetailsCert(payload) + updatedCert, err := service.storage.PutCertUpdate(payload) if err != nil { service.logger.Error(err) return output.JsonErrStorageGeneric(err) diff --git a/pkg/domain/certificates/services.go b/pkg/domain/certificates/services.go index 33d1d6c6..431637bc 100644 --- a/pkg/domain/certificates/services.go +++ b/pkg/domain/certificates/services.go @@ -7,6 +7,7 @@ import ( "certwarden-backend/pkg/output" "certwarden-backend/pkg/pagination_sort" "errors" + "time" "go.uber.org/zap" ) @@ -26,17 +27,17 @@ type App interface { // Storage interface for storage functions type Storage interface { GetAllCerts(q pagination_sort.Query) (certs []Certificate, totalRowCount int, err error) - GetOneCertById(id int) (cert Certificate, err error) - GetOneCertByName(name string) (cert Certificate, err error) + GetOneCertById(id int) (Certificate, error) + GetOneCertByName(name string) (Certificate, error) PostNewCert(payload NewPayload) (Certificate, error) - PutDetailsCert(payload DetailsUpdatePayload) (Certificate, error) - PutCertApiKey(certId int, apiKey string, updateTimeUnix int) (err error) - PutCertNewApiKey(certId int, newApiKey string, updateTimeUnix int) (err error) - PutCertClientKey(certId int, newClientKeyB64 string, updateTimeUnix int) (err error) + PutCertUpdate(payload UpdatePayload) (Certificate, error) + PutCertApiKey(certId int, apiKey string, updatedAt time.Time) error + PutCertApiKeyNew(certId int, apiKeyNew string, updatedAt time.Time) error + PutCertClientKey(certId int, newClientKeyB64 string, updatedAt time.Time) error - DeleteCert(id int) (err error) + DeleteCert(id int) error PostNewKey(private_keys.NewPayload) (private_keys.Key, error) } diff --git a/pkg/domain/certificates/validation.go b/pkg/domain/certificates/validation.go index 947e1786..921854a7 100644 --- a/pkg/domain/certificates/validation.go +++ b/pkg/domain/certificates/validation.go @@ -3,7 +3,9 @@ package certificates import ( "certwarden-backend/pkg/output" "certwarden-backend/pkg/validation" + "crypto/aes" "database/sql" + "encoding/base64" "errors" "fmt" ) @@ -28,6 +30,9 @@ var ( // domain ErrDomainBad = errors.New("domain or subject name not valid") ErrClientAddressBad = errors.New("client address is not valid") + + // aes key + ErrPostProcessingClientKeyB64Bad = errors.New("post processing aes key not valid") ) // GetCertificate returns the Certificate for the specified id. @@ -98,7 +103,7 @@ func (service *Service) privateKeyIdValid(keyId int, certId *int) bool { } // if certificate's key id matches keyId, valid - if cert.CertificateKey.ID == keyId { + if cert.Key.ID == keyId { return true } @@ -124,3 +129,20 @@ func subjectAltsValid(alts []string) bool { return true } + +// clientKeyB64Valid ensures the string is a base64 RawURL encoded AES key +func clientKeyB64Valid(b64Key string) bool { + // decode AES key + aesKey, err := base64.RawURLEncoding.DecodeString(b64Key) + if err != nil { + return false + } + + // ensure key is a proper size and can create a cipher + _, err = aes.NewCipher(aesKey) + if err != nil { + return false + } + + return true +} diff --git a/pkg/domain/download/fetch_key.go b/pkg/domain/download/fetch_key.go index 934eed39..f68517b4 100644 --- a/pkg/domain/download/fetch_key.go +++ b/pkg/domain/download/fetch_key.go @@ -54,7 +54,7 @@ func (service *Service) getKey(keyName string, apiKey string, apiKeyViaUrl bool) } // before return, update key last access, dont fail our though if this step fails, just log error - err = service.storage.PutKeyLastAccess(key.ID, time.Now().Unix()) + err = service.storage.PutKeyLastAccess(key.ID, time.Now()) if err != nil { service.logger.Errorf("download: failed to update key (id: %d) last access time (%s)", key.ID, err) } diff --git a/pkg/domain/download/fetch_order.go b/pkg/domain/download/fetch_order.go index 0a45cdd7..ae0ba07a 100644 --- a/pkg/domain/download/fetch_order.go +++ b/pkg/domain/download/fetch_order.go @@ -73,7 +73,7 @@ func (service *Service) getCertNewestValidOrder(certName string, apiKeyOrKeys st order.FinalizedKey.Pem = "" // before return, update cert last access, dont fail our though if this step fails, just log error - err = service.storage.PutCertLastAccess(order.Certificate.ID, time.Now().Unix()) + err = service.storage.PutCertLastAccess(order.Certificate.ID, time.Now()) if err != nil { service.logger.Errorf("download: failed to update cert (id: %d) last access time (%s)", order.Certificate.ID, err) } @@ -122,11 +122,11 @@ func (service *Service) getCertNewestValidOrder(certName string, apiKeyOrKeys st // before return, update cert AND KEY last access, dont fail our though if this step fails, just log error nowT := time.Now() - err = service.storage.PutCertLastAccess(order.Certificate.ID, nowT.Unix()) + err = service.storage.PutCertLastAccess(order.Certificate.ID, nowT) if err != nil { service.logger.Errorf("download: failed to update cert (id: %d) last access time (%s)", order.Certificate.ID, err) } - err = service.storage.PutKeyLastAccess(order.FinalizedKey.ID, nowT.Unix()) + err = service.storage.PutKeyLastAccess(order.FinalizedKey.ID, nowT) if err != nil { service.logger.Errorf("download: failed to update key (id: %d) last access time (%s)", order.FinalizedKey.ID, err) } diff --git a/pkg/domain/download/out_pfx_test.go b/pkg/domain/download/out_pfx_test.go index 131c659f..f5efe6c3 100644 --- a/pkg/domain/download/out_pfx_test.go +++ b/pkg/domain/download/out_pfx_test.go @@ -2,8 +2,8 @@ package download_test import ( "certwarden-backend/pkg/domain/download" + "certwarden-backend/pkg/helpers_test" "certwarden-backend/pkg/output" - "certwarden-backend/pkg/test_helpers" "context" "errors" "net/http" @@ -42,17 +42,17 @@ func onePfxTest(t *testing.T, handler func(w http.ResponseWriter, r *http.Reques jsonErr := handler(w, r) if !errors.Is(jsonErr, expectedJsonErr) { - t.Errorf("%s: name '%s' with header api-key '%s' and url api-key '%s' returned error '%s' but expected '%s'", test_helpers.GetFunctionName(handler), - certName, test_helpers.StringPointerToVal(apiKeyHeader), test_helpers.StringPointerToVal(apiKeyURL), jsonErr, expectedJsonErr) + t.Errorf("%s: name '%s' with header api-key '%s' and url api-key '%s' returned error '%s' but expected '%s'", helpers_test.GetFunctionName(handler), + certName, helpers_test.StringPointerToVal(apiKeyHeader), helpers_test.StringPointerToVal(apiKeyURL), jsonErr, expectedJsonErr) } body := w.Body.String() if jsonErr != nil && body != "" { - t.Errorf("%s: name '%s' with header api-key '%s' and url api-key '%s' returned body data but expected none", test_helpers.GetFunctionName(handler), - certName, test_helpers.StringPointerToVal(apiKeyHeader), test_helpers.StringPointerToVal(apiKeyURL)) + t.Errorf("%s: name '%s' with header api-key '%s' and url api-key '%s' returned body data but expected none", helpers_test.GetFunctionName(handler), + certName, helpers_test.StringPointerToVal(apiKeyHeader), helpers_test.StringPointerToVal(apiKeyURL)) } else if jsonErr == nil && body == "" { - t.Errorf("%s: name '%s' with header api-key '%s' and url api-key '%s' returned empty body but data was expected", test_helpers.GetFunctionName(handler), - certName, test_helpers.StringPointerToVal(apiKeyHeader), test_helpers.StringPointerToVal(apiKeyURL)) + t.Errorf("%s: name '%s' with header api-key '%s' and url api-key '%s' returned empty body but data was expected", helpers_test.GetFunctionName(handler), + certName, helpers_test.StringPointerToVal(apiKeyHeader), helpers_test.StringPointerToVal(apiKeyURL)) } } diff --git a/pkg/domain/download/service.go b/pkg/domain/download/service.go index 843c7d9d..8fa4b147 100644 --- a/pkg/domain/download/service.go +++ b/pkg/domain/download/service.go @@ -5,6 +5,7 @@ import ( "certwarden-backend/pkg/domain/private_keys" "certwarden-backend/pkg/output" "errors" + "time" "go.uber.org/zap" ) @@ -24,8 +25,8 @@ type Storage interface { GetCertNewestValidOrderByName(certName string) (order orders.Order, err error) - PutKeyLastAccess(keyId int, unixLastAccessTime int64) (err error) - PutCertLastAccess(certId int, unixLastAccessTime int64) (err error) + PutKeyLastAccess(keyId int, lastAccess time.Time) (err error) + PutCertLastAccess(certId int, lastAccess time.Time) (err error) } // Keys service struct diff --git a/pkg/domain/download/service_mock_test.go b/pkg/domain/download/service_mock_test.go index 95c93c4a..6e88130d 100644 --- a/pkg/domain/download/service_mock_test.go +++ b/pkg/domain/download/service_mock_test.go @@ -5,14 +5,15 @@ import ( "certwarden-backend/pkg/domain/download" "certwarden-backend/pkg/domain/orders" "certwarden-backend/pkg/domain/private_keys" + "certwarden-backend/pkg/helpers_test" "certwarden-backend/pkg/output" - "certwarden-backend/pkg/test_helpers" "context" "database/sql" "errors" "net/http" "net/http/httptest" "testing" + "time" "github.com/julienschmidt/httprouter" "go.uber.org/zap" @@ -421,10 +422,10 @@ j1f1P6e7Khe0uXD3N+r34piMQT0WX0po2rf16x0i return orders.Order{}, sql.ErrNoRows } -func (fs *fakeStorage) PutKeyLastAccess(keyId int, unixLastAccessTime int64) (err error) { +func (fs *fakeStorage) PutKeyLastAccess(keyId int, lastAccess time.Time) (err error) { return errors.New("not implemented") } -func (fs *fakeStorage) PutCertLastAccess(certId int, unixLastAccessTime int64) (err error) { +func (fs *fakeStorage) PutCertLastAccess(certId int, lastAccess time.Time) (err error) { return errors.New("not implemented") } @@ -490,13 +491,13 @@ func oneTest(t *testing.T, handler func(w http.ResponseWriter, r *http.Request) jsonErr := handler(w, r) if !errors.Is(jsonErr, expectedJsonErr) { - t.Errorf("%s: name '%s' with header api-key '%s' and url api-key '%s' returned error '%s' but expected '%s'", test_helpers.GetFunctionName(handler), - certName, test_helpers.StringPointerToVal(apiKeyHeader), test_helpers.StringPointerToVal(apiKeyURL), jsonErr, expectedJsonErr) + t.Errorf("%s: name '%s' with header api-key '%s' and url api-key '%s' returned error '%s' but expected '%s'", helpers_test.GetFunctionName(handler), + certName, helpers_test.StringPointerToVal(apiKeyHeader), helpers_test.StringPointerToVal(apiKeyURL), jsonErr, expectedJsonErr) } body := w.Body.String() if body != expectedBody { - t.Errorf("%s: name '%s' with header api-key '%s' and url api-key '%s' returned body '%s' but expected body '%s'", test_helpers.GetFunctionName(handler), - certName, test_helpers.StringPointerToVal(apiKeyHeader), test_helpers.StringPointerToVal(apiKeyURL), body, expectedBody) + t.Errorf("%s: name '%s' with header api-key '%s' and url api-key '%s' returned body '%s' but expected body '%s'", helpers_test.GetFunctionName(handler), + certName, helpers_test.StringPointerToVal(apiKeyHeader), helpers_test.StringPointerToVal(apiKeyURL), body, expectedBody) } } diff --git a/pkg/domain/orders/auto_ordering_exp.go b/pkg/domain/orders/auto_ordering_exp.go index efe5e60c..3a062900 100644 --- a/pkg/domain/orders/auto_ordering_exp.go +++ b/pkg/domain/orders/auto_ordering_exp.go @@ -30,7 +30,7 @@ func (service *Service) orderExpiringCerts() { defer wg.Done() // Get relevant ACME Server service - acmeService, err := service.acmeServerService.AcmeService(orders[i].Certificate.CertificateAccount.AcmeServer.ID) + acmeService, err := service.acmeServerService.AcmeService(orders[i].Certificate.Account.AcmeServer.ID) if err != nil { service.logger.Errorf("orders: auto order failed to get acme service for order %d (%s)", orders[i].ID, err) return // done, failed diff --git a/pkg/domain/orders/fulfilling_do.go b/pkg/domain/orders/fulfilling_do.go index b2d89889..5472cecc 100644 --- a/pkg/domain/orders/fulfilling_do.go +++ b/pkg/domain/orders/fulfilling_do.go @@ -25,14 +25,14 @@ func (j *orderFulfillJob) Do(workerID int) { // update certificate timestamp after fulfiller is done defer func() { - err = j.service.storage.UpdateCertUpdatedTime(order.Certificate.ID) + err = j.service.storage.PutCertUpdatedAt(order.Certificate.ID, time.Now()) if err != nil { j.service.logger.Errorf("orders: fulfilling worker %d: update cert time error: %s", workerID, err) } }() // get account key - key, err := order.Certificate.CertificateAccount.AcmeAccountKey() + key, err := order.Certificate.Account.AcmeAccountKey() if err != nil { j.service.logger.Errorf("orders: fulfilling worker %d: get account key error: %s", workerID, err) return // done, failed @@ -49,7 +49,7 @@ func (j *orderFulfillJob) Do(workerID int) { var acmeOrder acme.Order // acmeService to avoid repeated logic - acmeService, err := j.service.acmeServerService.AcmeService(order.Certificate.CertificateAccount.AcmeServer.ID) + acmeService, err := j.service.acmeServerService.AcmeService(order.Certificate.Account.AcmeServer.ID) if err != nil { j.service.logger.Errorf("orders: fulfilling worker %d: select acme service error: %s", workerID, err) return // done, failed @@ -104,7 +104,7 @@ fulfillLoop: // save finalized_key_id in storage (if finalize ACME cmd below fails, this will save any key change // upon next attempt to finalize with ACME; therefore this should always occur BEFORE the ACME finalize // command) - err = j.service.storage.UpdateFinalizedKey(order.ID, order.Certificate.CertificateKey.ID) + err = j.service.storage.UpdateFinalizedKey(order.ID, order.Certificate.Key.ID) if err != nil { j.service.logger.Errorf("orders: fulfilling worker %d: update finalized key error: %s", workerID, err) return // done, failed diff --git a/pkg/domain/orders/handlers_post.go b/pkg/domain/orders/handlers_post.go index e50e2c33..092509b0 100644 --- a/pkg/domain/orders/handlers_post.go +++ b/pkg/domain/orders/handlers_post.go @@ -7,6 +7,7 @@ import ( "errors" "net/http" "strconv" + "time" "github.com/julienschmidt/httprouter" ) @@ -160,14 +161,14 @@ func (service *Service) RevokeOrder(w http.ResponseWriter, r *http.Request) *out // end validation // get account key - key, err := order.Certificate.CertificateAccount.AcmeAccountKey() + key, err := order.Certificate.Account.AcmeAccountKey() if err != nil { service.logger.Error(err) return output.JsonErrInternal(err) } // revoke the certificate with ACME - acmeService, err := service.acmeServerService.AcmeService(order.Certificate.CertificateAccount.AcmeServer.ID) + acmeService, err := service.acmeServerService.AcmeService(order.Certificate.Account.AcmeServer.ID) if err != nil { service.logger.Error(err) return output.JsonErrInternal(err) @@ -191,7 +192,7 @@ func (service *Service) RevokeOrder(w http.ResponseWriter, r *http.Request) *out } // update certificate timestamp - err = service.storage.UpdateCertUpdatedTime(certId) + err = service.storage.PutCertUpdatedAt(certId, time.Now()) if err != nil { service.logger.Error(err) // no return diff --git a/pkg/domain/orders/order.go b/pkg/domain/orders/order.go index 3342e4af..cfb8decf 100644 --- a/pkg/domain/orders/order.go +++ b/pkg/domain/orders/order.go @@ -59,22 +59,22 @@ type orderSummaryResponse struct { } type orderCertificateSummaryResponse struct { - ID int `json:"id"` - Name string `json:"name"` - CertificateAccount orderCertificateAccountSummaryResponse `json:"acme_account"` - Subject string `json:"subject"` - SubjectAltNames []string `json:"subject_alts"` - ApiKeyViaUrl bool `json:"api_key_via_url"` - LastAccess int64 `json:"last_access"` + ID int `json:"id"` + Name string `json:"name"` + Account orderAccountSummaryResponse `json:"acme_account"` + Subject string `json:"subject"` + SubjectAltNames []string `json:"subject_alts"` + ApiKeyViaUrl bool `json:"api_key_via_url"` + LastAccess int64 `json:"last_access"` } -type orderCertificateAccountSummaryResponse struct { - ID int `json:"id"` - Name string `json:"name"` - OrderCertAccountServer orderCertificateAccountServerSummaryResponse `json:"acme_server"` +type orderAccountSummaryResponse struct { + ID int `json:"id"` + Name string `json:"name"` + OrderCertAccountServer orderAccountServerSummaryResponse `json:"acme_server"` } -type orderCertificateAccountServerSummaryResponse struct { +type orderAccountServerSummaryResponse struct { ID int `json:"id"` Name string `json:"name"` IsStaging bool `json:"is_staging"` @@ -118,13 +118,13 @@ func (order Order) summaryResponse(service *Service) orderSummaryResponse { Certificate: orderCertificateSummaryResponse{ ID: order.Certificate.ID, Name: order.Certificate.Name, - CertificateAccount: orderCertificateAccountSummaryResponse{ - ID: order.Certificate.CertificateAccount.ID, - Name: order.Certificate.CertificateAccount.Name, - OrderCertAccountServer: orderCertificateAccountServerSummaryResponse{ - ID: order.Certificate.CertificateAccount.AcmeServer.ID, - Name: order.Certificate.CertificateAccount.AcmeServer.Name, - IsStaging: order.Certificate.CertificateAccount.AcmeServer.IsStaging, + Account: orderAccountSummaryResponse{ + ID: order.Certificate.Account.ID, + Name: order.Certificate.Account.Name, + OrderCertAccountServer: orderAccountServerSummaryResponse{ + ID: order.Certificate.Account.AcmeServer.ID, + Name: order.Certificate.Account.AcmeServer.Name, + IsStaging: order.Certificate.Account.AcmeServer.IsStaging, }, }, Subject: order.Certificate.Subject, @@ -248,7 +248,7 @@ func (service *Service) NewOrderPayload(cert certificates.Certificate) acme.NewO // ACME ARI Extension: try to include the `replaces` field replaces := func() *string { - acmeServ, err := service.acmeServerService.AcmeService(cert.CertificateAccount.AcmeServer.ID) + acmeServ, err := service.acmeServerService.AcmeService(cert.Account.AcmeServer.ID) if err != nil { service.logger.Errorf("orders: new order cant populated `replaces`, failed to get acme service for cert %d (%s)", cert.ID, err) return nil diff --git a/pkg/domain/orders/order_acme_create.go b/pkg/domain/orders/order_acme_create.go index d60fe21c..129f9926 100644 --- a/pkg/domain/orders/order_acme_create.go +++ b/pkg/domain/orders/order_acme_create.go @@ -4,6 +4,7 @@ import ( "certwarden-backend/pkg/output" "database/sql" "errors" + "time" ) // placeNewOrderAndFulfill creates a new ACME order for the specified Certificate ID, @@ -21,14 +22,14 @@ func (service *Service) placeNewOrderAndFulfill(certId int, highPriority bool) ( } // get account key - key, err := cert.CertificateAccount.AcmeAccountKey() + key, err := cert.Account.AcmeAccountKey() if err != nil { service.logger.Error(err) return Order{}, output.JsonErrInternal(err) } // send the new-order to ACME - acmeService, err := service.acmeServerService.AcmeService(cert.CertificateAccount.AcmeServer.ID) + acmeService, err := service.acmeServerService.AcmeService(cert.Account.AcmeServer.ID) if err != nil { service.logger.Error(err) return Order{}, output.JsonErrInternal(err) @@ -59,7 +60,7 @@ func (service *Service) placeNewOrderAndFulfill(certId int, highPriority bool) ( } // update certificate timestamp - err = service.storage.UpdateCertUpdatedTime(cert.ID) + err = service.storage.PutCertUpdatedAt(cert.ID, time.Now()) if err != nil { service.logger.Error(err) // no return diff --git a/pkg/domain/orders/order_acme_payloads.go b/pkg/domain/orders/order_acme_payloads.go index 578644d7..7ff45e38 100644 --- a/pkg/domain/orders/order_acme_payloads.go +++ b/pkg/domain/orders/order_acme_payloads.go @@ -37,7 +37,7 @@ func makeNewOrderAcmePayload(cert certificates.Certificate, acmeResponse acme.Or payload := NewOrderAcmePayload{ CertId: cert.ID, - AccountId: cert.CertificateAccount.ID, + AccountId: cert.Account.ID, Status: acmeResponse.Status, KnownRevoked: false, Expires: acmeResponse.Expires, diff --git a/pkg/domain/orders/service.go b/pkg/domain/orders/service.go index 9378e778..201db312 100644 --- a/pkg/domain/orders/service.go +++ b/pkg/domain/orders/service.go @@ -12,6 +12,7 @@ import ( "net/http" "os/exec" "sync" + "time" "github.com/scaleway/scaleway-sdk-go/logger" "go.uber.org/zap" @@ -60,7 +61,7 @@ type Storage interface { GetNewestIncompleteCertOrderId(certId int) (orderId int, err error) // certs - UpdateCertUpdatedTime(certId int) (err error) + PutCertUpdatedAt(certId int, updatedAt time.Time) (err error) } // service struct diff --git a/pkg/domain/private_keys/handlers_delete.go b/pkg/domain/private_keys/handlers_delete.go index a717cbb0..8571868f 100644 --- a/pkg/domain/private_keys/handlers_delete.go +++ b/pkg/domain/private_keys/handlers_delete.go @@ -89,7 +89,7 @@ func (service *Service) RemoveOldApiKey(w http.ResponseWriter, r *http.Request) // update storage // set current api key from new key - err = service.storage.PutKeyApiKey(keyId, key.ApiKeyNew, int(time.Now().Unix())) + err = service.storage.PutKeyApiKey(keyId, key.ApiKeyNew, time.Now()) if err != nil { service.logger.Error(err) return output.JsonErrStorageGeneric(err) @@ -97,7 +97,7 @@ func (service *Service) RemoveOldApiKey(w http.ResponseWriter, r *http.Request) key.ApiKey = key.ApiKeyNew // set new key to blank - err = service.storage.PutKeyNewApiKey(keyId, "", int(time.Now().Unix())) + err = service.storage.PutKeyApiKeyNew(keyId, "", time.Now()) if err != nil { service.logger.Error(err) return output.JsonErrStorageGeneric(err) diff --git a/pkg/domain/private_keys/handlers_post.go b/pkg/domain/private_keys/handlers_post.go index 0e491b9c..bccb74cb 100644 --- a/pkg/domain/private_keys/handlers_post.go +++ b/pkg/domain/private_keys/handlers_post.go @@ -15,15 +15,15 @@ import ( // PostPayload is a struct for posting a new key type NewPayload struct { - Name *string `json:"name"` - Description *string `json:"description"` - AlgorithmValue *string `json:"algorithm_value"` - PemContent *string `json:"pem"` - ApiKey string `json:"-"` - ApiKeyDisabled *bool `json:"api_key_disabled"` - ApiKeyViaUrl bool `json:"-"` - CreatedAt int `json:"-"` - UpdatedAt int `json:"-"` + Name *string `json:"name"` + Description *string `json:"description"` + AlgorithmValue *string `json:"algorithm_value"` + PemContent *string `json:"pem"` + ApiKey string `json:"-"` + ApiKeyDisabled *bool `json:"api_key_disabled"` + ApiKeyViaUrl bool `json:"-"` + CreatedAt time.Time `json:"-"` + UpdatedAt time.Time `json:"-"` } // PostNewKey creates a new private key and saves it to storage @@ -95,8 +95,9 @@ func (service *Service) PostNewKey(w http.ResponseWriter, r *http.Request) *outp return output.JsonErrInternal(err) } payload.ApiKeyViaUrl = false - payload.CreatedAt = int(time.Now().Unix()) - payload.UpdatedAt = payload.CreatedAt + t := time.Now() + payload.CreatedAt = t + payload.UpdatedAt = t // save new key to storage, which also returns the new key id newKey, err := service.storage.PostNewKey(payload) @@ -154,7 +155,7 @@ func (service *Service) StageNewApiKey(w http.ResponseWriter, r *http.Request) * } // update storage - err = service.storage.PutKeyNewApiKey(keyId, newApiKey, int(time.Now().Unix())) + err = service.storage.PutKeyApiKeyNew(keyId, newApiKey, time.Now()) if err != nil { service.logger.Error(err) return output.JsonErrStorageGeneric(err) diff --git a/pkg/domain/private_keys/handlers_put.go b/pkg/domain/private_keys/handlers_put.go index 0fde09ae..72d1a38c 100644 --- a/pkg/domain/private_keys/handlers_put.go +++ b/pkg/domain/private_keys/handlers_put.go @@ -13,14 +13,14 @@ import ( // UpdatePayload is the struct for editing an existing Key's // information (only certain fields are editable) type UpdatePayload struct { - ID int `json:"-"` - Name *string `json:"name"` - Description *string `json:"description"` - ApiKey *string `json:"api_key"` - ApiKeyNew *string `json:"api_key_new"` - ApiKeyDisabled *bool `json:"api_key_disabled"` - ApiKeyViaUrl *bool `json:"api_key_via_url"` - UpdatedAt int `json:"-"` + ID int `json:"-"` + Name *string `json:"name"` + Description *string `json:"description"` + ApiKey *string `json:"api_key"` + ApiKeyNew *string `json:"api_key_new"` + ApiKeyDisabled *bool `json:"api_key_disabled"` + ApiKeyViaUrl *bool `json:"api_key_via_url"` + UpdatedAt time.Time `json:"-"` } // PutKeyUpdate updates a Key that already exists in storage. @@ -67,7 +67,7 @@ func (service *Service) PutKeyUpdate(w http.ResponseWriter, r *http.Request) *ou // end validation // add additional details to the payload before saving - payload.UpdatedAt = int(time.Now().Unix()) + payload.UpdatedAt = time.Now() // save updated key info to storage updatedKey, err := service.storage.PutKeyUpdate(payload) diff --git a/pkg/domain/private_keys/service.go b/pkg/domain/private_keys/service.go index ac3ae7f7..1c8bd9ba 100644 --- a/pkg/domain/private_keys/service.go +++ b/pkg/domain/private_keys/service.go @@ -4,6 +4,7 @@ import ( "certwarden-backend/pkg/output" "certwarden-backend/pkg/pagination_sort" "errors" + "time" "go.uber.org/zap" ) @@ -26,8 +27,8 @@ type Storage interface { PostNewKey(NewPayload) (Key, error) PutKeyUpdate(UpdatePayload) (Key, error) - PutKeyApiKey(keyId int, apiKey string, updateTimeUnix int) (err error) - PutKeyNewApiKey(keyId int, newApiKey string, updateTimeUnix int) error + PutKeyApiKey(keyId int, apiKey string, updatedAt time.Time) error + PutKeyApiKeyNew(keyId int, apiKeyNew string, updatedAt time.Time) error DeleteKey(int) error diff --git a/pkg/helpers_test/err_is.go b/pkg/helpers_test/err_is.go new file mode 100644 index 00000000..4a909957 --- /dev/null +++ b/pkg/helpers_test/err_is.go @@ -0,0 +1,49 @@ +package helpers_test + +import ( + "errors" + "strings" +) + +// testErrorStringComp is a special error type to check error text for a matching +// value (as opposed to a strict type match); this is useful when the exact type +// is not importable +type testErrorStringComp struct { + Inner error +} + +func (e testErrorStringComp) Error() string { + return e.Inner.Error() +} + +func (e testErrorStringComp) Unwrap() error { + return e.Inner +} + +// NewTestErrorStringComp wraps the provided error text in a special error type that +// will be parsed and compared when the custom ErrorsIs is called +func NewTestErrorStringComp(errText string) testErrorStringComp { + return testErrorStringComp{Inner: errors.New(errText)} +} + +// ErrorsIs +func ErrorsIs(err error, target error) bool { + // check if target is our special error type and if not, just do a normal errors.Is() + tError, isTestErrStringCmp := errors.AsType[testErrorStringComp](target) + if !isTestErrStringCmp { + return errors.Is(err, target) + } + + // if one is nil but not the other, they are not the same, return false early + // to avoid calls to nil value + if (err == nil && target != nil) || + (err != nil && target == nil) { + return false + } + + // comparison is case-insensitive + return strings.Contains( + strings.ToLower(err.Error()), + strings.ToLower(tError.Unwrap().Error()), + ) +} diff --git a/pkg/helpers_test/err_is_test.go b/pkg/helpers_test/err_is_test.go new file mode 100644 index 00000000..1be758a4 --- /dev/null +++ b/pkg/helpers_test/err_is_test.go @@ -0,0 +1,103 @@ +package helpers_test_test + +import ( + "certwarden-backend/pkg/acme" + "certwarden-backend/pkg/helpers_test" + "database/sql" + "errors" + "fmt" + "testing" +) + +func TestErrorsIs(t *testing.T) { + testCases := []struct { + err error + target error + isTheSame bool + }{ + { + err: nil, + target: nil, + isTheSame: true, + }, + { + err: nil, + target: helpers_test.NewTestErrorStringComp("an error"), + isTheSame: false, + }, + { + err: helpers_test.NewTestErrorStringComp("an error"), + target: nil, + isTheSame: false, + }, + { + err: sql.ErrNoRows, + target: acme.ErrChallengeMalformed, + isTheSame: false, + }, + { + err: errors.New("an error 1"), + target: errors.New("another error 2"), + isTheSame: false, + }, + { + err: sql.ErrNoRows, + target: helpers_test.NewTestErrorStringComp("an error"), + isTheSame: false, + }, + { + err: acme.ErrChallengeMalformed, + target: helpers_test.NewTestErrorStringComp("an error"), + isTheSame: false, + }, + { + err: errors.New("some error"), + target: helpers_test.NewTestErrorStringComp("uh oh, some error"), + isTheSame: false, + }, + { + err: errors.New("some error"), + target: helpers_test.NewTestErrorStringComp("some error, uh oh"), + isTheSame: false, + }, + { + err: helpers_test.NewTestErrorStringComp("uh oh, some error"), + target: errors.New("some error"), + isTheSame: false, + }, + { + err: helpers_test.NewTestErrorStringComp("some error, uh oh"), + target: errors.New("some error"), + isTheSame: false, + }, + { + err: errors.New("uh oh, some error"), + target: helpers_test.NewTestErrorStringComp("some error"), + isTheSame: true, + }, + { + err: errors.New("some error, uh oh"), + target: helpers_test.NewTestErrorStringComp("some error"), + isTheSame: true, + }, + { + err: errors.New("uh oh, some error"), + target: helpers_test.NewTestErrorStringComp("SOME errOR"), + isTheSame: true, + }, + { + err: errors.New("some error, uh oh"), + target: helpers_test.NewTestErrorStringComp("SOME errOR"), + isTheSame: true, + }, + } + + for i, tc := range testCases { + t.Run(fmt.Sprintf("#%d:", i), func(t *testing.T) { + res := helpers_test.ErrorsIs(tc.err, tc.target) + if res != tc.isTheSame { + t.Errorf("err '%s' with target '%s' expected '%t' but got '%t'", helpers_test.ErrorToVal(tc.err), helpers_test.ErrorToVal(tc.target), tc.isTheSame, res) + } + }) + } +} diff --git a/pkg/test_helpers/log_values.go b/pkg/helpers_test/log_values.go similarity index 97% rename from pkg/test_helpers/log_values.go rename to pkg/helpers_test/log_values.go index d3b3f00a..8c1ba5e1 100644 --- a/pkg/test_helpers/log_values.go +++ b/pkg/helpers_test/log_values.go @@ -1,4 +1,4 @@ -package test_helpers +package helpers_test import ( "reflect" diff --git a/pkg/test_helpers/log_values_test.go b/pkg/helpers_test/log_values_test.go similarity index 74% rename from pkg/test_helpers/log_values_test.go rename to pkg/helpers_test/log_values_test.go index a4ec12ba..3de50402 100644 --- a/pkg/test_helpers/log_values_test.go +++ b/pkg/helpers_test/log_values_test.go @@ -1,7 +1,7 @@ -package test_helpers_test +package helpers_test_test import ( - "certwarden-backend/pkg/test_helpers" + "certwarden-backend/pkg/helpers_test" "errors" "testing" ) @@ -11,12 +11,12 @@ func someFunctionA() error { } func TestGetFunctionName(t *testing.T) { - fName := test_helpers.GetFunctionName(someFunctionA) + fName := helpers_test.GetFunctionName(someFunctionA) if fName != "someFunctionA" { t.Errorf("getfunctionname expected 'someFunctionA', but got '%s'", fName) } - fName = test_helpers.GetFunctionName(test_helpers.ErrorToVal) + fName = helpers_test.GetFunctionName(helpers_test.ErrorToVal) if fName != "ErrorToVal" { t.Errorf("getfunctionname expected 'ErrorToVal', but got '%s'", fName) } @@ -24,7 +24,7 @@ func TestGetFunctionName(t *testing.T) { // nil input var f *func() error f = nil - fName = test_helpers.GetFunctionName(f) + fName = helpers_test.GetFunctionName(f) if fName != "" { t.Errorf("getfunctionname expected '', but got '%s'", fName) } @@ -32,20 +32,20 @@ func TestGetFunctionName(t *testing.T) { func TestStringPointerToVal(t *testing.T) { s := "test-1" - result := test_helpers.StringPointerToVal(&s) + result := helpers_test.StringPointerToVal(&s) if result != "test-1" { t.Errorf("stringpointertoval expected 'test-1', but got '%s'", result) } s = "some other test, again" - result = test_helpers.StringPointerToVal(&s) + result = helpers_test.StringPointerToVal(&s) if result != "some other test, again" { t.Errorf("stringpointertoval expected 'some other test, again', but got '%s'", result) } // nil var ptr *string - result = test_helpers.StringPointerToVal(ptr) + result = helpers_test.StringPointerToVal(ptr) if result != "" { t.Errorf("stringpointertoval expected '', but got '%s'", result) } @@ -53,20 +53,20 @@ func TestStringPointerToVal(t *testing.T) { func TestErrorToVal(t *testing.T) { e := errors.New("test-2") - result := test_helpers.ErrorToVal(e) + result := helpers_test.ErrorToVal(e) if result != "test-2" { t.Errorf("errortoval expected 'test-2', but got '%s'", result) } e = errors.New("some other test 2, again") - result = test_helpers.ErrorToVal(e) + result = helpers_test.ErrorToVal(e) if result != "some other test 2, again" { t.Errorf("errortoval expected 'some other test 2, again', but got '%s'", result) } // nil e = nil - result = test_helpers.ErrorToVal(e) + result = helpers_test.ErrorToVal(e) if result != "" { t.Errorf("errortoval expected '', but got '%s'", result) } diff --git a/pkg/storage/accounts_delete.go b/pkg/storage/accounts_delete.go index 75023e7f..a0358478 100644 --- a/pkg/storage/accounts_delete.go +++ b/pkg/storage/accounts_delete.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "errors" + "fmt" ) // AcmeAccountInUse returns true if the specified accountId matches @@ -12,6 +13,12 @@ func (store *Storage) AcmeAccountInUse(accountId int) (inUse bool, err error) { ctx, cancel := context.WithTimeout(store.shutdownContext, store.timeout) defer cancel() + tx, err := store.db.BeginTx(ctx, nil) + if err != nil { + return false, err + } + defer tx.Rollback() + // check server exists query := ` SELECT id @@ -19,7 +26,7 @@ func (store *Storage) AcmeAccountInUse(accountId int) (inUse bool, err error) { WHERE id = $1 ` - row := store.db.QueryRowContext(ctx, query, accountId) + row := tx.QueryRowContext(ctx, query, accountId) _discardVar := -2 err = row.Scan(&_discardVar) if err != nil { @@ -34,73 +41,55 @@ func (store *Storage) AcmeAccountInUse(accountId int) (inUse bool, err error) { WHERE acme_account_id = $1 ` - row = store.db.QueryRowContext(ctx, query, accountId) + row = tx.QueryRowContext(ctx, query, accountId) err = row.Scan(&_discardVar) if !errors.Is(err, sql.ErrNoRows) { return true, err } + err = tx.Commit() + if err != nil { + return false, err + } + return false, nil } // DeleteAccount deletes an account from the database func (store *Storage) DeleteAcmeAccount(id int) error { - ctx, cancel := context.WithTimeout(store.shutdownContext, store.timeout) - defer cancel() - - tx, err := store.db.BeginTx(ctx, nil) + // check that delete is safe + inUse, err := store.AcmeAccountInUse(id) if err != nil { return err } - defer tx.Rollback() - - // check acct exists - // if scan in succeeds, key exists - query := ` - SELECT id - FROM acme_accounts - WHERE id = $1 - ` - - row := tx.QueryRowContext(ctx, query, id) - temp := -2 - row.Scan(&temp) - if temp == -2 { - return sql.ErrNoRows - } - - // check not in use in certs - // if scan in succeeds, record exists in certificates - query = ` - SELECT id - FROM certificates - WHERE acme_account_id = $1 - ` - - row = tx.QueryRowContext(ctx, query, id) - temp = -2 - row.Scan(&temp) - if temp != -2 { + if inUse { return ErrInUse } + ctx, cancel := context.WithTimeout(store.shutdownContext, store.timeout) + defer cancel() + // delete - query = ` + query := ` DELETE FROM acme_accounts WHERE id = $1 ` - _, err = tx.ExecContext(ctx, query, id) + res, err := store.db.ExecContext(ctx, query, id) if err != nil { return err } - err = tx.Commit() + // verify update actually happened + rowsAffected, err := res.RowsAffected() if err != nil { return err } + if rowsAffected != 1 { + return errors.Join(fmt.Errorf("expected 1 row update, but got '%d'", rowsAffected), ErrWrongUpdateRowCount) + } return nil } diff --git a/pkg/storage/accounts_delete_test.go b/pkg/storage/accounts_delete_test.go index 4ea84f95..916c3233 100644 --- a/pkg/storage/accounts_delete_test.go +++ b/pkg/storage/accounts_delete_test.go @@ -2,10 +2,9 @@ package storage_test import ( "certwarden-backend/pkg/domain/acme_accounts" + "certwarden-backend/pkg/helpers_test" "certwarden-backend/pkg/storage" - "certwarden-backend/pkg/test_helpers" "database/sql" - "errors" "fmt" "testing" ) @@ -38,8 +37,8 @@ func TestAcmeAccountInUse(t *testing.T) { for _, tc := range testCases { t.Run(fmt.Sprintf("id: %d", tc.acctID), func(t *testing.T) { inUse, err := storage.AcmeAccountInUse(tc.acctID) - if !errors.Is(err, tc.expectedErr) { - t.Errorf("expected error '%s' but got '%s'", test_helpers.ErrorToVal(tc.expectedErr), test_helpers.ErrorToVal(err)) + if !helpers_test.ErrorsIs(err, tc.expectedErr) { + t.Errorf("expected error '%s' but got '%s'", helpers_test.ErrorToVal(tc.expectedErr), helpers_test.ErrorToVal(err)) } if inUse != tc.expectedInUse { @@ -74,13 +73,13 @@ func TestDeleteAcmeAccount(t *testing.T) { for _, tc := range testCases { t.Run(fmt.Sprintf("id: %d", tc.acctID), func(t *testing.T) { err := storage.DeleteAcmeAccount(tc.acctID) - if !errors.Is(err, tc.expectedDelErr) { - t.Errorf("expected delete error '%s' but got '%s'", test_helpers.ErrorToVal(tc.expectedDelErr), test_helpers.ErrorToVal(err)) + if !helpers_test.ErrorsIs(err, tc.expectedDelErr) { + t.Errorf("expected delete error '%s' but got '%s'", helpers_test.ErrorToVal(tc.expectedDelErr), helpers_test.ErrorToVal(err)) } acct, err := storage.GetOneAcmeAccountById(tc.acctID) - if !errors.Is(err, tc.expectedGetErr) { - t.Errorf("expected get error '%s' but got '%s'", test_helpers.ErrorToVal(tc.expectedGetErr), test_helpers.ErrorToVal(err)) + if !helpers_test.ErrorsIs(err, tc.expectedGetErr) { + t.Errorf("expected get error '%s' but got '%s'", helpers_test.ErrorToVal(tc.expectedGetErr), helpers_test.ErrorToVal(err)) } CompareAcmeAccount(t, acct, tc.expectedGetResult) diff --git a/pkg/storage/accounts_get_test.go b/pkg/storage/accounts_get_test.go index f4c7b29d..15122c85 100644 --- a/pkg/storage/accounts_get_test.go +++ b/pkg/storage/accounts_get_test.go @@ -2,10 +2,9 @@ package storage_test import ( "certwarden-backend/pkg/domain/acme_accounts" + "certwarden-backend/pkg/helpers_test" "certwarden-backend/pkg/pagination_sort" - "certwarden-backend/pkg/test_helpers" "database/sql" - "errors" "fmt" "testing" "time" @@ -83,7 +82,6 @@ var ( } ) -// TODO func TestGetAllAcmeAccounts(t *testing.T) { testCases := []struct { q pagination_sort.Query @@ -93,10 +91,10 @@ func TestGetAllAcmeAccounts(t *testing.T) { expectedAcctAtIndx acme_accounts.Account }{ {pagination_sort.Query{}, 7, 7, 3, acmeAcct23}, - {QueryBuilderForTest(1, 1, "id", true), 7, 1, 0, acmeAcct2}, - {QueryBuilderForTest(2, 1, "servername", false), 7, 2, 1, acmeAcct2}, - {QueryBuilderForTest(2, 4, "servername", false), 7, 2, 0, acmeAcct23}, - {QueryBuilderForTest(6, 1, "keyname", true), 7, 6, 5, acmeAcct1}, + {queryBuilderForTest(1, 1, "id", true), 7, 1, 0, acmeAcct2}, + {queryBuilderForTest(2, 1, "servername", false), 7, 2, 1, acmeAcct2}, + {queryBuilderForTest(2, 4, "servername", false), 7, 2, 0, acmeAcct23}, + {queryBuilderForTest(6, 1, "keyname", true), 7, 6, 5, acmeAcct1}, } // create testing service @@ -109,7 +107,7 @@ func TestGetAllAcmeAccounts(t *testing.T) { t.Run(fmt.Sprintf("#%d (%s)", i, tc.expectedAcctAtIndx.Name), func(t *testing.T) { accts, totalCt, err := storage.GetAllAcmeAccounts(tc.q) if err != nil { - t.Errorf("get all keys failed") + t.Errorf("get all failed") return } @@ -150,8 +148,8 @@ func TestGetOneAccountById(t *testing.T) { for i, tc := range testCases { t.Run(fmt.Sprintf("#%d (id: %d)", i, tc.id), func(t *testing.T) { acct, err := storage.GetOneAcmeAccountById(tc.id) - if !errors.Is(err, tc.expectedErr) { - t.Errorf("expected error '%s' but got '%s'", test_helpers.ErrorToVal(tc.expectedErr), test_helpers.ErrorToVal(err)) + if !helpers_test.ErrorsIs(err, tc.expectedErr) { + t.Errorf("expected error '%s' but got '%s'", helpers_test.ErrorToVal(tc.expectedErr), helpers_test.ErrorToVal(err)) } CompareAcmeAccount(t, acct, tc.expectedAcct) @@ -167,7 +165,7 @@ func TestGetOneAccountByName(t *testing.T) { }{ {"", sql.ErrNoRows, acme_accounts.Account{}}, {"fake-name", sql.ErrNoRows, acme_accounts.Account{}}, - {"LE_Staging_Account", nil, acmeAcct1}, + {"le_staging_account", nil, acmeAcct1}, // case is wrong {"LE_Production_Account", nil, acmeAcct2}, {"Google_Cloud_Staging2", nil, acmeAcct23}, } @@ -181,8 +179,8 @@ func TestGetOneAccountByName(t *testing.T) { for i, tc := range testCases { t.Run(fmt.Sprintf("#%d (name: %s)", i, tc.name), func(t *testing.T) { acct, err := storage.GetOneAcmeAccountByName(tc.name) - if !errors.Is(err, tc.expectedErr) { - t.Errorf("expected error '%s' but got '%s'", test_helpers.ErrorToVal(tc.expectedErr), test_helpers.ErrorToVal(err)) + if !helpers_test.ErrorsIs(err, tc.expectedErr) { + t.Errorf("expected error '%s' but got '%s'", helpers_test.ErrorToVal(tc.expectedErr), helpers_test.ErrorToVal(err)) } CompareAcmeAccount(t, acct, tc.expectedAcct) diff --git a/pkg/storage/accounts_post.go b/pkg/storage/accounts_post.go index b540a1a7..db601679 100644 --- a/pkg/storage/accounts_post.go +++ b/pkg/storage/accounts_post.go @@ -30,8 +30,8 @@ func (store *Storage) PostNewAcmeAccount(payload acme_accounts.NewPayload) (acme payload.Status, payload.Email, payload.AcceptedTos, - payload.CreatedAt, - payload.UpdatedAt, + payload.CreatedAt.Unix(), + payload.UpdatedAt.Unix(), payload.Kid, ).Scan(&id) diff --git a/pkg/storage/accounts_post_test.go b/pkg/storage/accounts_post_test.go index 40413628..d673d52c 100644 --- a/pkg/storage/accounts_post_test.go +++ b/pkg/storage/accounts_post_test.go @@ -2,9 +2,8 @@ package storage_test import ( "certwarden-backend/pkg/domain/acme_accounts" - "certwarden-backend/pkg/test_helpers" + "certwarden-backend/pkg/helpers_test" "database/sql" - "errors" "fmt" "testing" "time" @@ -26,8 +25,8 @@ func TestPostNewAcmeAccount(t *testing.T) { Status: "a status", Email: new("anemail@example.com"), AcceptedTos: new(false), - CreatedAt: 1788837479, - UpdatedAt: 1788838000, + CreatedAt: time.Unix(1788837479, 0), + UpdatedAt: time.Unix(1788838000, 0), Kid: "https://fake.example.com/1234", }, nil, @@ -55,11 +54,11 @@ func TestPostNewAcmeAccount(t *testing.T) { Status: "status", Email: new("email@example.com"), AcceptedTos: new(true), - CreatedAt: 1888837479, - UpdatedAt: 1888838000, + CreatedAt: time.Unix(1888837479, 0), + UpdatedAt: time.Unix(1888838000, 0), Kid: "https://fake.example.com/123456", }, - test_helpers.ErrAnyType, + helpers_test.NewTestErrorStringComp("UNIQUE constraint failed"), acme_accounts.Account{}, sql.ErrNoRows, }, @@ -72,11 +71,11 @@ func TestPostNewAcmeAccount(t *testing.T) { Status: "status", Email: new("fake2@example.com"), AcceptedTos: new(true), - CreatedAt: 1888837479, - UpdatedAt: 1888838000, + CreatedAt: time.Unix(1888837479, 0), + UpdatedAt: time.Unix(1888838000, 0), Kid: "https://fake.example.com/123456", }, - test_helpers.ErrAnyType, + helpers_test.NewTestErrorStringComp("NOT NULL constraint failed"), acme_accounts.Account{}, sql.ErrNoRows, }, @@ -89,11 +88,11 @@ func TestPostNewAcmeAccount(t *testing.T) { Status: "status", // Email: AcceptedTos: new(true), - CreatedAt: 1888837479, - UpdatedAt: 1888838000, + CreatedAt: time.Unix(1888837479, 0), + UpdatedAt: time.Unix(1888838000, 0), Kid: "https://fake.example.com/123456", }, - test_helpers.ErrAnyType, + helpers_test.NewTestErrorStringComp("NOT NULL constraint failed"), acme_accounts.Account{}, sql.ErrNoRows, }, @@ -106,17 +105,17 @@ func TestPostNewAcmeAccount(t *testing.T) { } for _, tc := range testCases { - t.Run(fmt.Sprintf("post name: %s", test_helpers.StringPointerToVal(tc.newPayload.Name)), func(t *testing.T) { + t.Run(fmt.Sprintf("post name: %s", helpers_test.StringPointerToVal(tc.newPayload.Name)), func(t *testing.T) { acct, err := storage.PostNewAcmeAccount(tc.newPayload) - if !test_helpers.ErrorsIs(err, tc.expectedPostErr) { - t.Errorf("expected post error '%s' but got '%s'", test_helpers.ErrorToVal(tc.expectedPostErr), test_helpers.ErrorToVal(err)) + if !helpers_test.ErrorsIs(err, tc.expectedPostErr) { + t.Errorf("expected post error '%s' but got '%s'", helpers_test.ErrorToVal(tc.expectedPostErr), helpers_test.ErrorToVal(err)) } CompareAcmeAccount(t, acct, tc.expectedNew) acct, err = storage.GetOneAcmeAccountByName(acct.Name) - if !errors.Is(err, tc.expectedGetErr) { - t.Errorf("expected get error '%s' but got '%s'", test_helpers.ErrorToVal(tc.expectedGetErr), test_helpers.ErrorToVal(err)) + if !helpers_test.ErrorsIs(err, tc.expectedGetErr) { + t.Errorf("expected get error '%s' but got '%s'", helpers_test.ErrorToVal(tc.expectedGetErr), helpers_test.ErrorToVal(err)) } CompareAcmeAccount(t, acct, tc.expectedNew) diff --git a/pkg/storage/accounts_put_test.go b/pkg/storage/accounts_put_test.go index d5d12c4b..1c251da3 100644 --- a/pkg/storage/accounts_put_test.go +++ b/pkg/storage/accounts_put_test.go @@ -2,10 +2,9 @@ package storage_test import ( "certwarden-backend/pkg/domain/acme_accounts" + "certwarden-backend/pkg/helpers_test" "certwarden-backend/pkg/storage" - "certwarden-backend/pkg/test_helpers" "database/sql" - "errors" "fmt" "testing" "time" @@ -155,7 +154,7 @@ func TestPutAcmeAccountUpdate(t *testing.T) { UpdatedAt: time.Unix(107800777, 0), }, acme_accounts.Account{}, - test_helpers.ErrAnyType, + helpers_test.NewTestErrorStringComp("UNIQUE constraint failed"), 16, acmeAcct16, nil, @@ -247,15 +246,15 @@ func TestPutAcmeAccountUpdate(t *testing.T) { for i, tc := range testCases { t.Run(fmt.Sprintf("#%d (id: %d)", i, tc.payload.ID), func(t *testing.T) { acct, err := storage.PutAcmeAccountUpdate(tc.payload) - if !test_helpers.ErrorsIs(err, tc.expectedPutErr) { - t.Errorf("expected put error '%s' but got '%s'", test_helpers.ErrorToVal(tc.expectedPutErr), test_helpers.ErrorToVal(err)) + if !helpers_test.ErrorsIs(err, tc.expectedPutErr) { + t.Errorf("expected put error '%s' but got '%s'", helpers_test.ErrorToVal(tc.expectedPutErr), helpers_test.ErrorToVal(err)) } CompareAcmeAccount(t, acct, tc.expectedPutResult) acct, err = storage.GetOneAcmeAccountById(tc.getId) - if !errors.Is(err, tc.expectedGetErr) { - t.Errorf("expected get error '%s' but got '%s'", test_helpers.ErrorToVal(tc.expectedGetErr), test_helpers.ErrorToVal(err)) + if !helpers_test.ErrorsIs(err, tc.expectedGetErr) { + t.Errorf("expected get error '%s' but got '%s'", helpers_test.ErrorToVal(tc.expectedGetErr), helpers_test.ErrorToVal(err)) } CompareAcmeAccount(t, acct, tc.expectedGetResult) @@ -337,7 +336,7 @@ func TestPutAcmeAccountNewKey(t *testing.T) { UpdatedAt: time.Unix(1088000751, 0), }, acme_accounts.Account{}, - test_helpers.ErrAnyType, + helpers_test.NewTestErrorStringComp("FOREIGN KEY constraint failed"), 1, acmeAcct1, nil, @@ -349,7 +348,7 @@ func TestPutAcmeAccountNewKey(t *testing.T) { UpdatedAt: time.Unix(1088000752, 0), }, acme_accounts.Account{}, - test_helpers.ErrAnyType, + helpers_test.NewTestErrorStringComp("FOREIGN KEY constraint failed"), 1, acmeAcct1, nil, @@ -361,7 +360,7 @@ func TestPutAcmeAccountNewKey(t *testing.T) { UpdatedAt: time.Unix(1088000753, 0), }, acme_accounts.Account{}, - test_helpers.ErrAnyType, + helpers_test.NewTestErrorStringComp("UNIQUE constraint failed"), 23, acmeAcct23, nil, @@ -377,15 +376,15 @@ func TestPutAcmeAccountNewKey(t *testing.T) { for i, tc := range testCases { t.Run(fmt.Sprintf("#%d (id: %d)", i, tc.payload.ID), func(t *testing.T) { acct, err := storage.PutAcmeAccountNewKey(tc.payload) - if !test_helpers.ErrorsIs(err, tc.expectedPutErr) { - t.Errorf("expected put error '%s' but got '%s'", test_helpers.ErrorToVal(tc.expectedPutErr), test_helpers.ErrorToVal(err)) + if !helpers_test.ErrorsIs(err, tc.expectedPutErr) { + t.Errorf("expected put error '%s' but got '%s'", helpers_test.ErrorToVal(tc.expectedPutErr), helpers_test.ErrorToVal(err)) } CompareAcmeAccount(t, acct, tc.expectedPutResult) acct, err = storage.GetOneAcmeAccountById(tc.getId) - if !errors.Is(err, tc.expectedGetErr) { - t.Errorf("expected get error '%s' but got '%s'", test_helpers.ErrorToVal(tc.expectedGetErr), test_helpers.ErrorToVal(err)) + if !helpers_test.ErrorsIs(err, tc.expectedGetErr) { + t.Errorf("expected get error '%s' but got '%s'", helpers_test.ErrorToVal(tc.expectedGetErr), helpers_test.ErrorToVal(err)) } CompareAcmeAccount(t, acct, tc.expectedGetResult) diff --git a/pkg/storage/acme_servers_delete.go b/pkg/storage/acme_servers_delete.go index cf416d89..e1a8e62a 100644 --- a/pkg/storage/acme_servers_delete.go +++ b/pkg/storage/acme_servers_delete.go @@ -12,6 +12,12 @@ func (store *Storage) ServerInUse(serverId int) (inUse bool, err error) { ctx, cancel := context.WithTimeout(store.shutdownContext, store.timeout) defer cancel() + tx, err := store.db.BeginTx(ctx, nil) + if err != nil { + return false, err + } + defer tx.Rollback() + // check server exists query := ` SELECT id @@ -19,7 +25,7 @@ func (store *Storage) ServerInUse(serverId int) (inUse bool, err error) { WHERE id = $1 ` - row := store.db.QueryRowContext(ctx, query, serverId) + row := tx.QueryRowContext(ctx, query, serverId) _discardVar := -2 err = row.Scan(&_discardVar) if err != nil { @@ -34,20 +40,22 @@ func (store *Storage) ServerInUse(serverId int) (inUse bool, err error) { WHERE acme_server_id = $1 ` - row = store.db.QueryRowContext(ctx, query, serverId) + row = tx.QueryRowContext(ctx, query, serverId) err = row.Scan(&_discardVar) if !errors.Is(err, sql.ErrNoRows) { return true, err } + err = tx.Commit() + if err != nil { + return false, err + } + return false, nil } // DeleteServer deletes an acme server from the database func (store *Storage) DeleteServer(serverId int) error { - ctx, cancel := context.WithTimeout(store.shutdownContext, store.timeout) - defer cancel() - // check that delete is safe inUse, err := store.ServerInUse(serverId) if err != nil { @@ -57,6 +65,9 @@ func (store *Storage) DeleteServer(serverId int) error { return ErrInUse } + ctx, cancel := context.WithTimeout(store.shutdownContext, store.timeout) + defer cancel() + // delete query := ` DELETE FROM diff --git a/pkg/storage/acme_servers_delete_test.go b/pkg/storage/acme_servers_delete_test.go index 19870c79..8fdba97c 100644 --- a/pkg/storage/acme_servers_delete_test.go +++ b/pkg/storage/acme_servers_delete_test.go @@ -2,10 +2,9 @@ package storage_test import ( "certwarden-backend/pkg/domain/acme_servers" + "certwarden-backend/pkg/helpers_test" "certwarden-backend/pkg/storage" - "certwarden-backend/pkg/test_helpers" "database/sql" - "errors" "fmt" "testing" ) @@ -34,8 +33,8 @@ func TestServerInUse(t *testing.T) { for _, tc := range testCases { t.Run(fmt.Sprintf("server id: %d", tc.serverID), func(t *testing.T) { inUse, err := storage.ServerInUse(tc.serverID) - if !errors.Is(err, tc.expectedErr) { - t.Errorf("expected error '%s' but got '%s'", test_helpers.ErrorToVal(tc.expectedErr), test_helpers.ErrorToVal(err)) + if !helpers_test.ErrorsIs(err, tc.expectedErr) { + t.Errorf("expected error '%s' but got '%s'", helpers_test.ErrorToVal(tc.expectedErr), helpers_test.ErrorToVal(err)) } if inUse != tc.expectedInUse { @@ -70,13 +69,13 @@ func TestDeleteServer(t *testing.T) { for _, tc := range testCases { t.Run(fmt.Sprintf("server id: %d", tc.serverID), func(t *testing.T) { err := storage.DeleteServer(tc.serverID) - if !errors.Is(err, tc.expectedDelErr) { - t.Errorf("expected delete error '%s' but got '%s'", test_helpers.ErrorToVal(tc.expectedDelErr), test_helpers.ErrorToVal(err)) + if !helpers_test.ErrorsIs(err, tc.expectedDelErr) { + t.Errorf("expected delete error '%s' but got '%s'", helpers_test.ErrorToVal(tc.expectedDelErr), helpers_test.ErrorToVal(err)) } server, err := storage.GetOneServerById(tc.serverID) - if !errors.Is(err, tc.expectedGetErr) { - t.Errorf("expected get error '%s' but got '%s'", test_helpers.ErrorToVal(tc.expectedGetErr), test_helpers.ErrorToVal(err)) + if !helpers_test.ErrorsIs(err, tc.expectedGetErr) { + t.Errorf("expected get error '%s' but got '%s'", helpers_test.ErrorToVal(tc.expectedGetErr), helpers_test.ErrorToVal(err)) } CompareAcmeServer(t, server, tc.expectedGetResult) diff --git a/pkg/storage/acme_servers_get_test.go b/pkg/storage/acme_servers_get_test.go index 77b70f50..19f0881a 100644 --- a/pkg/storage/acme_servers_get_test.go +++ b/pkg/storage/acme_servers_get_test.go @@ -2,10 +2,9 @@ package storage_test import ( "certwarden-backend/pkg/domain/acme_servers" + "certwarden-backend/pkg/helpers_test" "certwarden-backend/pkg/pagination_sort" - "certwarden-backend/pkg/test_helpers" "database/sql" - "errors" "fmt" "testing" "time" @@ -62,8 +61,8 @@ func TestGetAllAcmeServers(t *testing.T) { expectedServerAtIndx acme_servers.Server }{ {pagination_sort.Query{}, 5, 5, 3, acmeServer0}, - {QueryBuilderForTest(1, 1, "id", true), 5, 1, 0, acmeServer1}, - {QueryBuilderForTest(2, 1, "updated_at", false), 5, 2, 1, acmeServer4}, + {queryBuilderForTest(1, 1, "id", true), 5, 1, 0, acmeServer1}, + {queryBuilderForTest(2, 1, "updated_at", false), 5, 2, 1, acmeServer4}, } // create testing service @@ -76,7 +75,7 @@ func TestGetAllAcmeServers(t *testing.T) { t.Run(fmt.Sprintf("#%d (%s)", i, tc.expectedServerAtIndx.Name), func(t *testing.T) { servers, totalCt, err := storage.GetAllAcmeServers(tc.q) if err != nil { - t.Errorf("get all keys failed") + t.Errorf("get all failed") return } @@ -84,12 +83,12 @@ func TestGetAllAcmeServers(t *testing.T) { t.Errorf("incorrect total count, expected '%d' but got '%d'", tc.expectedTotalCt, totalCt) } if len(servers) != tc.expectedResultLen { - t.Errorf("incorrect servers length, expected '%d' but got '%d'", tc.expectedResultLen, len(servers)) + t.Errorf("incorrect result length, expected '%d' but got '%d'", tc.expectedResultLen, len(servers)) } if tc.testIndx <= len(servers)-1 { CompareAcmeServer(t, servers[tc.testIndx], tc.expectedServerAtIndx) } else { - t.Errorf("couldnt test server at index '%d' because length of server array was only '%d'", tc.testIndx, len(servers)) + t.Errorf("couldnt test result at index '%d' because length of result array was only '%d'", tc.testIndx, len(servers)) } }) } @@ -117,8 +116,8 @@ func TestGetOneServerById(t *testing.T) { for i, tc := range testCases { t.Run(fmt.Sprintf("#%d (id: %d)", i, tc.id), func(t *testing.T) { serv, err := storage.GetOneServerById(tc.id) - if !errors.Is(err, tc.expectedErr) { - t.Errorf("expected error '%s' but got '%s'", test_helpers.ErrorToVal(tc.expectedErr), test_helpers.ErrorToVal(err)) + if !helpers_test.ErrorsIs(err, tc.expectedErr) { + t.Errorf("expected error '%s' but got '%s'", helpers_test.ErrorToVal(tc.expectedErr), helpers_test.ErrorToVal(err)) } CompareAcmeServer(t, serv, tc.expectedServer) @@ -134,7 +133,7 @@ func TestGetOneServerByName(t *testing.T) { }{ {"fake-bad-name", sql.ErrNoRows, acme_servers.Server{}}, {"", sql.ErrNoRows, acme_servers.Server{}}, - {"Lets_Encrypt", nil, acmeServer0}, + {"lets_encrypt", nil, acmeServer0}, // case is wrong {"Lets_Encrypt_Staging", nil, acmeServer1}, {"Google_Prod", nil, acmeServer4}, } @@ -148,8 +147,8 @@ func TestGetOneServerByName(t *testing.T) { for i, tc := range testCases { t.Run(fmt.Sprintf("#%d (name: %s)", i, tc.name), func(t *testing.T) { serv, err := storage.GetOneServerByName(tc.name) - if !errors.Is(err, tc.expectedErr) { - t.Errorf("expected error '%s' but got '%s'", test_helpers.ErrorToVal(tc.expectedErr), test_helpers.ErrorToVal(err)) + if !helpers_test.ErrorsIs(err, tc.expectedErr) { + t.Errorf("expected error '%s' but got '%s'", helpers_test.ErrorToVal(tc.expectedErr), helpers_test.ErrorToVal(err)) } CompareAcmeServer(t, serv, tc.expectedServer) diff --git a/pkg/storage/acme_servers_post.go b/pkg/storage/acme_servers_post.go index 45c371a4..af0c21e1 100644 --- a/pkg/storage/acme_servers_post.go +++ b/pkg/storage/acme_servers_post.go @@ -24,8 +24,8 @@ func (store *Storage) PostNewServer(payload acme_servers.NewPayload) (acme_serve payload.Description, payload.DirectoryURL, payload.IsStaging, - payload.CreatedAt, - payload.UpdatedAt, + payload.CreatedAt.Unix(), + payload.UpdatedAt.Unix(), ).Scan(&acmeServerId) if err != nil { diff --git a/pkg/storage/acme_servers_post_test.go b/pkg/storage/acme_servers_post_test.go index cb4fc24d..ca9bc597 100644 --- a/pkg/storage/acme_servers_post_test.go +++ b/pkg/storage/acme_servers_post_test.go @@ -2,9 +2,8 @@ package storage_test import ( "certwarden-backend/pkg/domain/acme_servers" - "certwarden-backend/pkg/test_helpers" + "certwarden-backend/pkg/helpers_test" "database/sql" - "errors" "fmt" "testing" "time" @@ -23,8 +22,8 @@ func TestPostNewServer(t *testing.T) { Description: new("some service"), DirectoryURL: new("https://example.com/directory"), IsStaging: new(true), - CreatedAt: 1780337479, - UpdatedAt: 1780338000, + CreatedAt: time.Unix(1780337479, 0), + UpdatedAt: time.Unix(1780338000, 0), }, nil, acme_servers.Server{ @@ -44,10 +43,10 @@ func TestPostNewServer(t *testing.T) { Description: new("some service wont work"), DirectoryURL: new("https://example2.com/directory"), IsStaging: new(true), - CreatedAt: 1780337449, - UpdatedAt: 1780338040, + CreatedAt: time.Unix(1780337449, 0), + UpdatedAt: time.Unix(1780338040, 0), }, - test_helpers.ErrAnyType, + helpers_test.NewTestErrorStringComp("UNIQUE constraint failed"), acme_servers.Server{}, sql.ErrNoRows, }, @@ -57,10 +56,10 @@ func TestPostNewServer(t *testing.T) { Description: new("wont work"), // DirectoryURL IsStaging: new(false), - CreatedAt: 1880337449, - UpdatedAt: 1880338040, + CreatedAt: time.Unix(1880337449, 0), + UpdatedAt: time.Unix(1880338040, 0), }, - test_helpers.ErrAnyType, + helpers_test.NewTestErrorStringComp("NOT NULL constraint failed"), acme_servers.Server{}, sql.ErrNoRows, }, @@ -73,17 +72,17 @@ func TestPostNewServer(t *testing.T) { } for _, tc := range testCases { - t.Run(fmt.Sprintf("post name: %s", test_helpers.StringPointerToVal(tc.newPayload.Name)), func(t *testing.T) { + t.Run(fmt.Sprintf("post name: %s", helpers_test.StringPointerToVal(tc.newPayload.Name)), func(t *testing.T) { server, err := storage.PostNewServer(tc.newPayload) - if !test_helpers.ErrorsIs(err, tc.expectedPostErr) { - t.Errorf("expected post error '%s' but got '%s'", test_helpers.ErrorToVal(tc.expectedPostErr), test_helpers.ErrorToVal(err)) + if !helpers_test.ErrorsIs(err, tc.expectedPostErr) { + t.Errorf("expected post error '%s' but got '%s'", helpers_test.ErrorToVal(tc.expectedPostErr), helpers_test.ErrorToVal(err)) } CompareAcmeServer(t, server, tc.expectedNew) server, err = storage.GetOneServerByName(server.Name) - if !errors.Is(err, tc.expectedGetErr) { - t.Errorf("expected get error '%s' but got '%s'", test_helpers.ErrorToVal(tc.expectedGetErr), test_helpers.ErrorToVal(err)) + if !helpers_test.ErrorsIs(err, tc.expectedGetErr) { + t.Errorf("expected get error '%s' but got '%s'", helpers_test.ErrorToVal(tc.expectedGetErr), helpers_test.ErrorToVal(err)) } CompareAcmeServer(t, server, tc.expectedNew) diff --git a/pkg/storage/acme_servers_put.go b/pkg/storage/acme_servers_put.go index bba7ab16..b8076607 100644 --- a/pkg/storage/acme_servers_put.go +++ b/pkg/storage/acme_servers_put.go @@ -31,7 +31,7 @@ func (store *Storage) PutServerUpdate(payload acme_servers.UpdatePayload) (acme_ payload.Description, payload.DirectoryURL, payload.IsStaging, - payload.UpdatedAt, + payload.UpdatedAt.Unix(), payload.ID, ) if err != nil { diff --git a/pkg/storage/acme_servers_put_test.go b/pkg/storage/acme_servers_put_test.go index f32dc050..bf7b2447 100644 --- a/pkg/storage/acme_servers_put_test.go +++ b/pkg/storage/acme_servers_put_test.go @@ -2,10 +2,9 @@ package storage_test import ( "certwarden-backend/pkg/domain/acme_servers" + "certwarden-backend/pkg/helpers_test" "certwarden-backend/pkg/storage" - "certwarden-backend/pkg/test_helpers" "database/sql" - "errors" "fmt" "testing" "time" @@ -47,7 +46,7 @@ func TestPutServerUpdate(t *testing.T) { Description: new("new desc"), DirectoryURL: new("https://example-new.com/directory"), IsStaging: new(false), - UpdatedAt: 1733265750, + UpdatedAt: time.Unix(1733265750, 0), }, acme_servers.Server{ ID: 1, @@ -74,7 +73,7 @@ func TestPutServerUpdate(t *testing.T) { { // update none of the things (except last update) acme_servers.UpdatePayload{ ID: 19, - UpdatedAt: 11121111, + UpdatedAt: time.Unix(11121111, 0), }, acme_servers.Server{ ID: 19, @@ -102,7 +101,7 @@ func TestPutServerUpdate(t *testing.T) { acme_servers.UpdatePayload{ ID: 4, DirectoryURL: new("https://example-put.com/directory"), - UpdatedAt: 100800111, + UpdatedAt: time.Unix(100800111, 0), }, acme_servers.Server{ ID: 4, @@ -137,15 +136,15 @@ func TestPutServerUpdate(t *testing.T) { for i, tc := range testCases { t.Run(fmt.Sprintf("#%d (id: %d)", i, tc.payload.ID), func(t *testing.T) { server, err := storage.PutServerUpdate(tc.payload) - if !errors.Is(err, tc.expectedPutErr) { - t.Errorf("expected put error '%s' but got '%s'", test_helpers.ErrorToVal(tc.expectedPutErr), test_helpers.ErrorToVal(err)) + if !helpers_test.ErrorsIs(err, tc.expectedPutErr) { + t.Errorf("expected put error '%s' but got '%s'", helpers_test.ErrorToVal(tc.expectedPutErr), helpers_test.ErrorToVal(err)) } CompareAcmeServer(t, server, tc.expectedPutResult) server, err = storage.GetOneServerById(tc.getId) - if !errors.Is(err, tc.expectedGetErr) { - t.Errorf("expected get error '%s' but got '%s'", test_helpers.ErrorToVal(tc.expectedGetErr), test_helpers.ErrorToVal(err)) + if !helpers_test.ErrorsIs(err, tc.expectedGetErr) { + t.Errorf("expected get error '%s' but got '%s'", helpers_test.ErrorToVal(tc.expectedGetErr), helpers_test.ErrorToVal(err)) } CompareAcmeServer(t, server, tc.expectedGetResult) diff --git a/pkg/storage/backup.go b/pkg/storage/backup.go index 0acfd741..acb2b15a 100644 --- a/pkg/storage/backup.go +++ b/pkg/storage/backup.go @@ -23,9 +23,9 @@ func (store *Storage) LockDBForBackup() (unlockFunc func(), err error) { return nil, err } - // make function to rollback the tx (remove the lock) + // make function to remove lock unlockFunc = func() { - _ = tx.Rollback() + _ = tx.Commit() } return unlockFunc, nil diff --git a/pkg/storage/backup_test.go b/pkg/storage/backup_test.go new file mode 100644 index 00000000..adef82ec --- /dev/null +++ b/pkg/storage/backup_test.go @@ -0,0 +1,97 @@ +package storage_test + +import ( + "certwarden-backend/pkg/domain/acme_servers" + "certwarden-backend/pkg/helpers_test" + "certwarden-backend/pkg/storage" + "context" + "fmt" + "sync" + "testing" + "time" +) + +// backupCheckErrOK triggers an error on t if err does not match the expected err +// Note: this includes deadline expiration as a lock error +func backupCheckErrOK(t *testing.T, err error, expectLockErr bool) { + if expectLockErr { + if !helpers_test.ErrorsIs(err, context.DeadlineExceeded) && !helpers_test.ErrorsIs(err, helpers_test.NewTestErrorStringComp("database is locked")) { + t.Errorf("err expected '%s' but got '%s'", helpers_test.ErrorToVal(context.DeadlineExceeded), helpers_test.ErrorToVal(err)) + } + } else { + if err != nil { + t.Errorf("err expected '%s' but got '%s'", helpers_test.ErrorToVal(nil), helpers_test.ErrorToVal(err)) + } + } +} + +// backupTestBattery is the group of tests run both while db is locked and while it is unlocked +func backupTestBattery(t *testing.T, storage *storage.Storage, expectLocked bool) { + wg := sync.WaitGroup{} + lockedStateTxt := "unlocked" + if !expectLocked { + lockedStateTxt = "locked" + } + + // read only + wg.Add(1) + go t.Run(fmt.Sprintf("%s: get all acme accounts", lockedStateTxt), func(t *testing.T) { + _, _, err := storage.GetAllAcmeAccounts(queryBuilderForTest(5, 0, "", true)) + backupCheckErrOK(t, err, false) + wg.Done() + }) + + wg.Add(1) + go t.Run(fmt.Sprintf("%s: get one key by id", lockedStateTxt), func(t *testing.T) { + _, err := storage.GetOneKeyById(62) + backupCheckErrOK(t, err, false) + wg.Done() + }) + + wg.Wait() + + // trying to write + wg.Add(1) + go t.Run(fmt.Sprintf("%s: put key api key", lockedStateTxt), func(t *testing.T) { + err := storage.PutKeyApiKey(1, "xyz", time.Unix(123, 0)) + backupCheckErrOK(t, err, expectLocked) + wg.Done() + }) + + wg.Add(1) + go t.Run(fmt.Sprintf("%s: put acme server update", lockedStateTxt), func(t *testing.T) { + payload := acme_servers.UpdatePayload{ + ID: 1, + UpdatedAt: time.Unix(6323444, 0), + } + + _, err := storage.PutServerUpdate(payload) + backupCheckErrOK(t, err, expectLocked) + wg.Done() + }) + + // wait for all tests + wg.Wait() +} + +func TestLockDBForBackup(t *testing.T) { + // create testing service + storage, err := openStorageWithTestData(t, "lockdbforbackup") + if err != nil { + t.Fatal(err) + } + + unlock, err := storage.LockDBForBackup() + if err != nil { + t.Fatalf("failed to lock db: %s", err) + } + + // try various operations (locked) + backupTestBattery(t, storage, true) + + // verify things work after unlock + unlock() + + // try various operations (unlocked) + backupTestBattery(t, storage, false) +} diff --git a/pkg/storage/certificates.go b/pkg/storage/certificates.go index d0d4a746..57b9d341 100644 --- a/pkg/storage/certificates.go +++ b/pkg/storage/certificates.go @@ -45,8 +45,8 @@ func (cert certificateDb) toCertificate() (certificates.Certificate, error) { ID: cert.id, Name: cert.name, Description: cert.description, - CertificateKey: cert.certificateKeyDb.toKey(), - CertificateAccount: cert.certificateAccountDb.toAccount(), + Key: cert.certificateKeyDb.toKey(), + Account: cert.certificateAccountDb.toAccount(), Subject: cert.subject, SubjectAltNames: cert.subjectAltNames.toSlice(), Organization: cert.organization, diff --git a/pkg/storage/certificates_delete.go b/pkg/storage/certificates_delete.go index 32e81ab4..782105aa 100644 --- a/pkg/storage/certificates_delete.go +++ b/pkg/storage/certificates_delete.go @@ -2,11 +2,14 @@ package storage import ( "context" - "database/sql" + "errors" + "fmt" ) // DeleteCert deletes a cert from the database func (store *Storage) DeleteCert(id int) (err error) { + // Note: There is no CertInUse func, so this transaction lives here instead + ctx, cancel := context.WithTimeout(store.shutdownContext, store.timeout) defer cancel() @@ -25,10 +28,11 @@ func (store *Storage) DeleteCert(id int) (err error) { ` row := tx.QueryRowContext(ctx, query, id) - temp := -2 - row.Scan(&temp) - if temp == -2 { - return sql.ErrNoRows + _discardVar := -2 + err = row.Scan(&_discardVar) + if err != nil { + // sql.ErrNoRows included here + return err } // delete @@ -39,10 +43,19 @@ func (store *Storage) DeleteCert(id int) (err error) { id = $1 ` - _, err = tx.ExecContext(ctx, query, id) + res, err := tx.ExecContext(ctx, query, id) + if err != nil { + return err + } + + // verify update actually happened + rowsAffected, err := res.RowsAffected() if err != nil { return err } + if rowsAffected != 1 { + return errors.Join(fmt.Errorf("expected 1 row update, but got '%d'", rowsAffected), ErrWrongUpdateRowCount) + } err = tx.Commit() if err != nil { diff --git a/pkg/storage/certificates_delete_test.go b/pkg/storage/certificates_delete_test.go new file mode 100644 index 00000000..daea0d1a --- /dev/null +++ b/pkg/storage/certificates_delete_test.go @@ -0,0 +1,47 @@ +package storage_test + +import ( + "certwarden-backend/pkg/domain/certificates" + "certwarden-backend/pkg/helpers_test" + "database/sql" + "fmt" + "testing" +) + +func TestDeleteCert(t *testing.T) { + testCases := []struct { + id int + expectedDelErr error + expectedGetResult certificates.Certificate + expectedGetErr error + }{ + {-12, sql.ErrNoRows, certificates.Certificate{}, sql.ErrNoRows}, // non-existent + {2, sql.ErrNoRows, certificates.Certificate{}, sql.ErrNoRows}, // non-existent + {18, nil, certificates.Certificate{}, sql.ErrNoRows}, // not in use, gets deleted + {30, nil, certificates.Certificate{}, sql.ErrNoRows}, // not in use, gets deleted + {32, nil, certificates.Certificate{}, sql.ErrNoRows}, // not in use, gets deleted + {35, nil, certificates.Certificate{}, sql.ErrNoRows}, // not in use, gets deleted + } + + // create testing service + storage, err := openStorageWithTestData(t, "deletecert") + if err != nil { + t.Fatal(err) + } + + for _, tc := range testCases { + t.Run(fmt.Sprintf("id: %d", tc.id), func(t *testing.T) { + err := storage.DeleteCert(tc.id) + if !helpers_test.ErrorsIs(err, tc.expectedDelErr) { + t.Errorf("expected delete error '%s' but got '%s'", helpers_test.ErrorToVal(tc.expectedDelErr), helpers_test.ErrorToVal(err)) + } + + cert, err := storage.GetOneCertById(tc.id) + if !helpers_test.ErrorsIs(err, tc.expectedGetErr) { + t.Errorf("expected get error '%s' but got '%s'", helpers_test.ErrorToVal(tc.expectedGetErr), helpers_test.ErrorToVal(err)) + } + + CompareCertificate(t, cert, tc.expectedGetResult) + }) + } +} diff --git a/pkg/storage/certificates_get_test.go b/pkg/storage/certificates_get_test.go new file mode 100644 index 00000000..a40225e2 --- /dev/null +++ b/pkg/storage/certificates_get_test.go @@ -0,0 +1,213 @@ +package storage_test + +import ( + "certwarden-backend/pkg/domain/certificates" + "certwarden-backend/pkg/helpers_test" + "certwarden-backend/pkg/pagination_sort" + "crypto/x509/pkix" + "database/sql" + "encoding/asn1" + "fmt" + "testing" + "time" +) + +var ( + cert18 = certificates.Certificate{ + ID: 18, + Name: "serverdefault", + Description: "its a decript", + Key: key31, + Account: acmeAcct2, + Subject: "desk.dude.example.com", + SubjectAltNames: []string{"test011.test.example.com"}, + Organization: "my org", + OrganizationalUnit: "my ou", + Country: "your country", + State: "a state", + City: "springfield", + CSRExtraExtensions: []certificates.CertExtension{ + { + Extension: pkix.Extension{ + Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 24}, + Critical: false, + Value: []byte{0x30, 0x03, 0x02, 0x01, 0x05}, + }, + Description: "OCSP Must Staple", + }, + }, + PreferredRootCN: "ISRG Root X1", + LastAccess: time.Unix(1745952074, 0), + CreatedAt: time.Unix(1709327717, 0), + UpdatedAt: time.Unix(1779386440, 0), + ApiKey: "api-secret-18", + ApiKeyNew: "api-new-secret-18", + ApiKeyViaUrl: true, + PostProcessingCommand: "./scripts/windows/post-processing.example.ps1", + PostProcessingEnvironment: []string{"asdasdasdsd=asasd"}, + PostProcessingClientAddress: "dude.greg.example.com", + PostProcessingClientKeyB64: "aaaaaaaaaaaaaaaaaaaaaaaaaaa-ccccccccccccccc", + Profile: "tlsserver", + } + + cert26 = certificates.Certificate{ + ID: 26, + Name: "test008.test.example.com", + Description: "", + Key: key55, + Account: acmeAcct1, + Subject: "test008.test.example.com", + SubjectAltNames: []string{}, + Organization: "", + OrganizationalUnit: "", + Country: "", + State: "", + City: "", + CSRExtraExtensions: []certificates.CertExtension{}, + PreferredRootCN: "", + LastAccess: time.Unix(0, 0), + CreatedAt: time.Unix(1743170701, 0), + UpdatedAt: time.Unix(1765392360, 0), + ApiKey: "api-secret-26", + ApiKeyNew: "", + ApiKeyViaUrl: false, + PostProcessingCommand: "./scripts/windows/post-processing.example.ps1", + PostProcessingEnvironment: []string{}, + PostProcessingClientAddress: "test008.test.example.com", + PostProcessingClientKeyB64: "", + Profile: "", + } + + cert27 = certificates.Certificate{ + ID: 27, + Name: "test008.test.example.com-p", + Description: "", + Key: key56, + Account: acmeAcct2, + Subject: "test008.test.example.com", + SubjectAltNames: []string{"test011.test.example.com", "*.test011.test.example.com"}, + Organization: "", + OrganizationalUnit: "", + Country: "", + State: "", + City: "", + CSRExtraExtensions: []certificates.CertExtension{}, + PreferredRootCN: "", + LastAccess: time.Unix(0, 0), + CreatedAt: time.Unix(1743171060, 0), + UpdatedAt: time.Unix(1746122825, 0), + ApiKey: "api-secret-27", + ApiKeyNew: "", + ApiKeyViaUrl: false, + PostProcessingCommand: "", + PostProcessingEnvironment: []string{}, + PostProcessingClientAddress: "", + PostProcessingClientKeyB64: "", + Profile: "", + } +) + +func TestGetAllCerts(t *testing.T) { + testCases := []struct { + q pagination_sort.Query + expectedTotalCt int + expectedResultLen int + testIndx int + expectedAtIndx certificates.Certificate + }{ + {pagination_sort.Query{}, 9, 9, 2, cert18}, + {queryBuilderForTest(1, 1, "id", true), 9, 1, 0, cert26}, + {queryBuilderForTest(3, 6, "servername", false), 9, 3, 1, cert27}, + {queryBuilderForTest(4, 1, "accountname", true), 9, 4, 0, cert18}, + } + + // create testing service + storage, err := openStorageWithTestData(t, "getallcerts") + if err != nil { + t.Fatal(err) + } + + for i, tc := range testCases { + t.Run(fmt.Sprintf("#%d (%s)", i, tc.expectedAtIndx.Name), func(t *testing.T) { + certs, totalCt, err := storage.GetAllCerts(tc.q) + if err != nil { + t.Errorf("get all failed") + return + } + + if totalCt != tc.expectedTotalCt { + t.Errorf("incorrect total count, expected '%d' but got '%d'", tc.expectedTotalCt, totalCt) + } + if len(certs) != tc.expectedResultLen { + t.Errorf("incorrect result length, expected '%d' but got '%d'", tc.expectedResultLen, len(certs)) + } + if tc.testIndx <= len(certs)-1 { + CompareCertificate(t, certs[tc.testIndx], tc.expectedAtIndx) + } else { + t.Errorf("couldnt test result at index '%d' because length of result array was only '%d'", tc.testIndx, len(certs)) + } + }) + } +} + +func TestGetOneCertById(t *testing.T) { + testCases := []struct { + id int + expectedErr error + expectedCert certificates.Certificate + }{ + {-5, sql.ErrNoRows, certificates.Certificate{}}, + {50, sql.ErrNoRows, certificates.Certificate{}}, + {18, nil, cert18}, + {26, nil, cert26}, + {27, nil, cert27}, + } + + // create testing service + storage, err := openStorageWithTestData(t, "getonecertbyid") + if err != nil { + t.Fatal(err) + } + + for i, tc := range testCases { + t.Run(fmt.Sprintf("#%d (id: %d)", i, tc.id), func(t *testing.T) { + serv, err := storage.GetOneCertById(tc.id) + if !helpers_test.ErrorsIs(err, tc.expectedErr) { + t.Errorf("expected error '%s' but got '%s'", helpers_test.ErrorToVal(tc.expectedErr), helpers_test.ErrorToVal(err)) + } + + CompareCertificate(t, serv, tc.expectedCert) + }) + } +} + +func TestGetOneCertByName(t *testing.T) { + testCases := []struct { + name string + expectedErr error + expectedCert certificates.Certificate + }{ + {"fake-bad-name", sql.ErrNoRows, certificates.Certificate{}}, + {"", sql.ErrNoRows, certificates.Certificate{}}, + {"serverDEFault", nil, cert18}, // case is wrong + {"test008.test.example.com", nil, cert26}, + {"test008.test.example.com-p", nil, cert27}, + } + + // create testing service + storage, err := openStorageWithTestData(t, "getonecertbyname") + if err != nil { + t.Fatal(err) + } + + for i, tc := range testCases { + t.Run(fmt.Sprintf("#%d (name: %s)", i, tc.name), func(t *testing.T) { + serv, err := storage.GetOneCertByName(tc.name) + if !helpers_test.ErrorsIs(err, tc.expectedErr) { + t.Errorf("expected error '%s' but got '%s'", helpers_test.ErrorToVal(tc.expectedErr), helpers_test.ErrorToVal(err)) + } + + CompareCertificate(t, serv, tc.expectedCert) + }) + } +} diff --git a/pkg/storage/certificates_post.go b/pkg/storage/certificates_post.go index 2681eac3..3cdd1f6e 100644 --- a/pkg/storage/certificates_post.go +++ b/pkg/storage/certificates_post.go @@ -40,8 +40,8 @@ func (store *Storage) PostNewCert(payload certificates.NewPayload) (certificates payload.City, makeJsonCertExtensionSlice(payload.CSRExtraExtensions, false), payload.PreferredRootCN, - payload.CreatedAt, - payload.UpdatedAt, + payload.CreatedAt.Unix(), + payload.UpdatedAt.Unix(), payload.ApiKey, payload.ApiKeyViaUrl, payload.PostProcessingCommand, diff --git a/pkg/storage/certificates_post_test.go b/pkg/storage/certificates_post_test.go new file mode 100644 index 00000000..aa545430 --- /dev/null +++ b/pkg/storage/certificates_post_test.go @@ -0,0 +1,228 @@ +package storage_test + +import ( + "certwarden-backend/pkg/domain/certificates" + "certwarden-backend/pkg/helpers_test" + "crypto/x509/pkix" + "database/sql" + "encoding/asn1" + "fmt" + "testing" + "time" +) + +func TestPostNewCert(t *testing.T) { + testCases := []struct { + newPayload certificates.NewPayload + expectedPostErr error + expectedNew certificates.Certificate + expectedGetErr error + }{ + { // valid insertion + certificates.NewPayload{ + Name: new("NewCertHere"), + Description: new("some cert ins"), + PrivateKeyID: new(58), + NewKeyAlgorithmValue: new("some-alg"), // should be ignored + AcmeAccountID: new(1), + Subject: new("some.example.com"), + SubjectAltNames: []string{"some1.example.com", "some2.example.com"}, + Organization: new("an org"), + OrganizationalUnit: new("an ou"), + Country: new("usa"), + State: new("Ca"), + City: new("los santos"), + CSRExtraExtensions: []certificates.CertExtension{ + { + Extension: pkix.Extension{ + Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 24}, + Critical: false, + Value: []byte{0x30, 0x03, 0x02, 0x01, 0x05}, + }, + Description: "OCSP Must Staple", + }, + }, + PreferredRootCN: new("Root xyz"), + PostProcessingCommand: new("./run-me.py"), + PostProcessingEnvironment: []string{"a=123", "b=456"}, + PostProcessingClientAddress: new("endpoint.example.com"), + PostProcessingClientKeyB64: new("an aes key"), + Profile: new("test-prof"), + ApiKey: "12345fffff", + ApiKeyViaUrl: true, + CreatedAt: time.Unix(770337479, 0), + UpdatedAt: time.Unix(770338000, 0), + }, + nil, + certificates.Certificate{ + ID: 36, + Name: "NewCertHere", + Description: "some cert ins", + Key: key58, + Account: acmeAcct1, + Subject: "some.example.com", + SubjectAltNames: []string{"some1.example.com", "some2.example.com"}, + Organization: "an org", + OrganizationalUnit: "an ou", + Country: "usa", + State: "Ca", + City: "los santos", + CSRExtraExtensions: []certificates.CertExtension{ + { + Extension: pkix.Extension{ + Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 24}, + Critical: false, + Value: []byte{0x30, 0x03, 0x02, 0x01, 0x05}, + }, + Description: "OCSP Must Staple", + }, + }, + PreferredRootCN: "Root xyz", + LastAccess: time.Unix(0, 0), + CreatedAt: time.Unix(770337479, 0), + UpdatedAt: time.Unix(770338000, 0), + ApiKey: "12345fffff", + ApiKeyNew: "", + ApiKeyViaUrl: true, + PostProcessingCommand: "./run-me.py", + PostProcessingEnvironment: []string{"a=123", "b=456"}, + PostProcessingClientAddress: "endpoint.example.com", + PostProcessingClientKeyB64: "an aes key", + Profile: "test-prof", + }, + nil, + }, + { // duplicate name (non-case sensitive) + certificates.NewPayload{ + Name: new("test008.TEST.example.com"), + Description: new("some cert ins"), + PrivateKeyID: new(58), + NewKeyAlgorithmValue: new("some-alg"), + AcmeAccountID: new(1), + Subject: new("some.example.com"), + SubjectAltNames: []string{}, + Organization: new("an org"), + OrganizationalUnit: new("an ou"), + Country: new("usa"), + State: new("Ca"), + City: new("los santos"), + CSRExtraExtensions: []certificates.CertExtension{}, + PreferredRootCN: new("Root xyz"), + PostProcessingCommand: new("./run-me.py"), + PostProcessingEnvironment: []string{}, + PostProcessingClientAddress: new("endpoint.example.com"), + PostProcessingClientKeyB64: new("an aes key"), + Profile: new("test-prof"), + ApiKey: "12345fffff", + ApiKeyViaUrl: true, + CreatedAt: time.Unix(770337479, 0), + UpdatedAt: time.Unix(770338000, 0), + }, + helpers_test.NewTestErrorStringComp("UNIQUE constraint failed: certificates.name"), + certificates.Certificate{}, + sql.ErrNoRows, + }, + { // incomplete payload 1 + certificates.NewPayload{ + Name: new("NewCertHerexxxxy"), + Description: new("some cert ins"), + PrivateKeyID: new(58), + NewKeyAlgorithmValue: new("some-alg"), // should be ignored + // AcmeAccountID: new(1), + Subject: new("some.example.com"), + SubjectAltNames: []string{"some1.example.com", "some2.example.com"}, + Organization: new("an org"), + OrganizationalUnit: new("an ou"), + Country: new("usa"), + State: new("Ca"), + City: new("los santos"), + CSRExtraExtensions: []certificates.CertExtension{ + { + Extension: pkix.Extension{ + Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 24}, + Critical: false, + Value: []byte{0x30, 0x03, 0x02, 0x01, 0x05}, + }, + Description: "OCSP Must Staple", + }, + }, + PreferredRootCN: new("Root xyz"), + PostProcessingCommand: new("./run-me.py"), + PostProcessingEnvironment: []string{"a=123", "b=456"}, + PostProcessingClientAddress: new("endpoint.example.com"), + PostProcessingClientKeyB64: new("an aes key"), + Profile: new("test-prof"), + ApiKey: "12345fffff", + ApiKeyViaUrl: true, + CreatedAt: time.Unix(770337479, 0), + UpdatedAt: time.Unix(770338000, 0), + }, + helpers_test.NewTestErrorStringComp("NOT NULL constraint failed: certificates.acme_account_id"), + certificates.Certificate{}, + sql.ErrNoRows, + }, + { // incomplete payload 2 + certificates.NewPayload{ + Name: new("NewCertHerexxxxyyy"), + Description: new("some cert ins"), + PrivateKeyID: new(58), + NewKeyAlgorithmValue: new("some-alg"), // should be ignored + AcmeAccountID: new(1), + // Subject: new("some.example.com"), + SubjectAltNames: []string{"some1.example.com", "some2.example.com"}, + Organization: new("an org"), + OrganizationalUnit: new("an ou"), + Country: new("usa"), + State: new("Ca"), + City: new("los santos"), + CSRExtraExtensions: []certificates.CertExtension{ + { + Extension: pkix.Extension{ + Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 24}, + Critical: false, + Value: []byte{0x30, 0x03, 0x02, 0x01, 0x05}, + }, + Description: "OCSP Must Staple", + }, + }, + PreferredRootCN: new("Root xyz"), + PostProcessingCommand: new("./run-me.py"), + PostProcessingEnvironment: []string{"a=123", "b=456"}, + PostProcessingClientAddress: new("endpoint.example.com"), + PostProcessingClientKeyB64: new("an aes key"), + Profile: new("test-prof"), + ApiKey: "12345fffff", + ApiKeyViaUrl: true, + CreatedAt: time.Unix(770337479, 0), + UpdatedAt: time.Unix(770338000, 0), + }, + helpers_test.NewTestErrorStringComp("NOT NULL constraint failed: certificates.subject"), + certificates.Certificate{}, + sql.ErrNoRows, + }, + } + + // create testing service + storage, err := openStorageWithTestData(t, "postnewcert") + if err != nil { + t.Fatal(err) + } + + for _, tc := range testCases { + t.Run(fmt.Sprintf("post name: %s", helpers_test.StringPointerToVal(tc.newPayload.Name)), func(t *testing.T) { + record, err := storage.PostNewCert(tc.newPayload) + if !helpers_test.ErrorsIs(err, tc.expectedPostErr) { + t.Errorf("expected post error '%s' but got '%s'", helpers_test.ErrorToVal(tc.expectedPostErr), helpers_test.ErrorToVal(err)) + } + + CompareCertificate(t, record, tc.expectedNew) + + record, err = storage.GetOneCertByName(record.Name) + if !helpers_test.ErrorsIs(err, tc.expectedGetErr) { + t.Errorf("expected get error '%s' but got '%s'", helpers_test.ErrorToVal(tc.expectedGetErr), helpers_test.ErrorToVal(err)) + } + + CompareCertificate(t, record, tc.expectedNew) + }) + } +} diff --git a/pkg/storage/certificates_put.go b/pkg/storage/certificates_put.go index 8f9124df..53846b7e 100644 --- a/pkg/storage/certificates_put.go +++ b/pkg/storage/certificates_put.go @@ -3,12 +3,14 @@ package storage import ( "certwarden-backend/pkg/domain/certificates" "context" + "errors" + "fmt" "time" ) -// PutDetailsCert saves details about the cert that can be updated at any time. It only updates +// PutCertUpdate saves details about the cert that can be updated at any time. It only updates // the details which are provided -func (store *Storage) PutDetailsCert(payload certificates.DetailsUpdatePayload) (certificates.Certificate, error) { +func (store *Storage) PutCertUpdate(payload certificates.UpdatePayload) (certificates.Certificate, error) { // database update ctx, cancel := context.WithTimeout(store.shutdownContext, store.timeout) defer cancel() @@ -34,13 +36,14 @@ func (store *Storage) PutDetailsCert(payload certificates.DetailsUpdatePayload) post_processing_command = case when $15 is null then post_processing_command else $15 end, post_processing_environment = case when $16 is null then post_processing_environment else $16 end, post_processing_client_address = case when $17 is null then post_processing_client_address else $17 end, - profile = case when $18 is null then profile else $18 end, - updated_at = $19 + post_processing_client_key = case when $18 is null then post_processing_client_key else $18 end, + profile = case when $19 is null then profile else $19 end, + updated_at = $20 WHERE - id = $20 + id = $21 ` - _, err := store.db.ExecContext(ctx, query, + res, err := store.db.ExecContext(ctx, query, payload.Name, payload.Description, payload.PrivateKeyId, @@ -58,14 +61,23 @@ func (store *Storage) PutDetailsCert(payload certificates.DetailsUpdatePayload) payload.PostProcessingCommand, makeJsonStringSlice(payload.PostProcessingEnvironment, true), payload.PostProcessingClientAddress, + payload.PostProcessingClientKeyB64, payload.Profile, - payload.UpdatedAt, + payload.UpdatedAt.Unix(), payload.ID, ) + if err != nil { + return certificates.Certificate{}, err + } + // verify update actually happened + rowsAffected, err := res.RowsAffected() if err != nil { return certificates.Certificate{}, err } + if rowsAffected != 1 { + return certificates.Certificate{}, errors.Join(fmt.Errorf("expected 1 row update, but got '%d'", rowsAffected), ErrWrongUpdateRowCount) + } // get updated to return updatedCert, err := store.GetOneCertById(payload.ID) @@ -76,122 +88,59 @@ func (store *Storage) PutDetailsCert(payload certificates.DetailsUpdatePayload) return updatedCert, nil } -// UpdateCertUpdatedTime sets the specified order's updated_at to now -func (store *Storage) UpdateCertUpdatedTime(certId int) (err error) { - ctx, cancel := context.WithTimeout(store.shutdownContext, store.timeout) - defer cancel() - - query := ` - UPDATE - certificates - SET - updated_at = $1 - WHERE - id = $2 - ` - - _, err = store.db.ExecContext(ctx, query, - time.Now().Unix(), - certId, - ) - if err != nil { - return err +// PutCertUpdatedAt sets the specified cert's updated_at +func (store *Storage) PutCertUpdatedAt(certId int, updatedAt time.Time) (err error) { + // leverage main Put function + payload := certificates.UpdatePayload{ + ID: certId, + UpdatedAt: updatedAt, } - // TODO: Handle 0 rows updated. - - return nil + _, err = store.PutCertUpdate(payload) + return err } -// PutCertNewApiKey sets a cert's new api key and updates the updated at time -func (store *Storage) PutCertNewApiKey(certId int, newApiKey string, updateTimeUnix int) (err error) { - // database action - ctx, cancel := context.WithTimeout(store.shutdownContext, store.timeout) - defer cancel() - - query := ` - UPDATE - certificates - SET - api_key_new = $1, - updated_at = $2 - WHERE - id = $3 - ` - - _, err = store.db.ExecContext(ctx, query, - newApiKey, - updateTimeUnix, - certId, - ) - - if err != nil { - return err +// PutCertApiKey sets a cert's api key and updates the updated at time +func (store *Storage) PutCertApiKey(certId int, apiKey string, updatedAt time.Time) (err error) { + // leverage main Put function + payload := certificates.UpdatePayload{ + ID: certId, + ApiKey: &apiKey, + UpdatedAt: updatedAt, } - return nil + _, err = store.PutCertUpdate(payload) + return err } -// PutCertApiKey sets a cert's api key and updates the updated at time -func (store *Storage) PutCertApiKey(certId int, apiKey string, updateTimeUnix int) (err error) { - // database action - ctx, cancel := context.WithTimeout(store.shutdownContext, store.timeout) - defer cancel() - - query := ` - UPDATE - certificates - SET - api_key = $1, - updated_at = $2 - WHERE - id = $3 - ` - - _, err = store.db.ExecContext(ctx, query, - apiKey, - updateTimeUnix, - certId, - ) - - if err != nil { - return err +// PutCertApiKeyNew sets a cert's new api key and updates the updated at time +func (store *Storage) PutCertApiKeyNew(certId int, apiKeyNew string, updatedAt time.Time) (err error) { + // leverage main Put function + payload := certificates.UpdatePayload{ + ID: certId, + ApiKeyNew: &apiKeyNew, + UpdatedAt: updatedAt, } - return nil + _, err = store.PutCertUpdate(payload) + return err } // PutCertClientKey sets a cert's client key and updates the updated at time -func (store *Storage) PutCertClientKey(certId int, newClientKeyB64 string, updateTimeUnix int) (err error) { - // database action - ctx, cancel := context.WithTimeout(store.shutdownContext, store.timeout) - defer cancel() - - query := ` - UPDATE - certificates - SET - post_processing_client_key = $1, - updated_at = $2 - WHERE - id = $3 - ` - - _, err = store.db.ExecContext(ctx, query, - newClientKeyB64, - updateTimeUnix, - certId, - ) - - if err != nil { - return err +func (store *Storage) PutCertClientKey(certId int, clientKeyB64 string, updatedAt time.Time) (err error) { + // leverage main Put function + payload := certificates.UpdatePayload{ + ID: certId, + PostProcessingClientKeyB64: &clientKeyB64, + UpdatedAt: updatedAt, } - return nil + _, err = store.PutCertUpdate(payload) + return err } // PutCertLastAccess sets a cert's last access time -func (store *Storage) PutCertLastAccess(certId int, unixLastAccessTime int64) (err error) { +func (store *Storage) PutCertLastAccess(certId int, lastAccess time.Time) (err error) { // database action ctx, cancel := context.WithTimeout(store.shutdownContext, store.timeout) defer cancel() @@ -205,14 +154,22 @@ func (store *Storage) PutCertLastAccess(certId int, unixLastAccessTime int64) (e id = $2 ` - _, err = store.db.ExecContext(ctx, query, - unixLastAccessTime, + res, err := store.db.ExecContext(ctx, query, + lastAccess.Unix(), certId, ) + if err != nil { + return err + } + // verify update actually happened + rowsAffected, err := res.RowsAffected() if err != nil { return err } + if rowsAffected != 1 { + return errors.Join(fmt.Errorf("expected 1 row update, but got '%d'", rowsAffected), ErrWrongUpdateRowCount) + } return nil } diff --git a/pkg/storage/certificates_put_test.go b/pkg/storage/certificates_put_test.go new file mode 100644 index 00000000..1bcdb8f4 --- /dev/null +++ b/pkg/storage/certificates_put_test.go @@ -0,0 +1,930 @@ +package storage_test + +import ( + "certwarden-backend/pkg/domain/certificates" + "certwarden-backend/pkg/helpers_test" + "certwarden-backend/pkg/storage" + "crypto/x509/pkix" + "database/sql" + "encoding/asn1" + "fmt" + "testing" + "time" +) + +func TestPutDetailsCert(t *testing.T) { + testCases := []struct { + payload certificates.UpdatePayload + + expectedPutResult certificates.Certificate + expectedPutErr error + + getId int + expectedGetResult certificates.Certificate + expectedGetErr error + }{ + { // invalid cert + certificates.UpdatePayload{ + ID: -1, + }, + certificates.Certificate{}, + storage.ErrWrongUpdateRowCount, + -1, + certificates.Certificate{}, + sql.ErrNoRows, + }, + { // invalid key + certificates.UpdatePayload{ + ID: 522, + }, + certificates.Certificate{}, + storage.ErrWrongUpdateRowCount, + 522, + certificates.Certificate{}, + sql.ErrNoRows, + }, + { // update all things + certificates.UpdatePayload{ + ID: 18, + Name: new("somenewNameHere"), + Description: new("some new desc goes here"), + PrivateKeyId: new(58), + SubjectAltNames: []string{"new.example.com"}, + Organization: new("new org 1"), + OrganizationalUnit: new("new orgu 1"), + Country: new("new orgco 1"), + State: new("new orgst 1"), + City: new("new orgci 1"), + CSRExtraExtensions: []certificates.CertExtension{ + { + Extension: pkix.Extension{ + Id: asn1.ObjectIdentifier{1, 2, 4}, + Critical: true, + Value: []byte{0xaa, 0xbb, 0x11}, + }, + Description: "a", + }, + }, + PreferredRootCN: new("different new cn"), + PostProcessingCommand: new("./app.exe"), + PostProcessingEnvironment: []string{"a=123", "b=zba"}, + PostProcessingClientAddress: new("xyz.com"), + PostProcessingClientKeyB64: new("aaa888aaabbbccc"), + Profile: new("new prof 2"), + ApiKey: new("api-key---"), + ApiKeyNew: new("api-key-new---"), + ApiKeyViaUrl: new(false), + UpdatedAt: time.Unix(222223333, 0), + }, + certificates.Certificate{ + ID: 18, + Name: "somenewNameHere", + Description: "some new desc goes here", + Key: key58, + Account: acmeAcct2, + Subject: "desk.dude.example.com", + SubjectAltNames: []string{"new.example.com"}, + Organization: "new org 1", + OrganizationalUnit: "new orgu 1", + Country: "new orgco 1", + State: "new orgst 1", + City: "new orgci 1", + CSRExtraExtensions: []certificates.CertExtension{ + { + Extension: pkix.Extension{ + Id: asn1.ObjectIdentifier{1, 2, 4}, + Critical: true, + Value: []byte{0xaa, 0xbb, 0x11}, + }, + Description: "a", + }, + }, + PreferredRootCN: "different new cn", + LastAccess: time.Unix(1745952074, 0), + CreatedAt: time.Unix(1709327717, 0), + UpdatedAt: time.Unix(222223333, 0), + ApiKey: "api-key---", + ApiKeyNew: "api-key-new---", + ApiKeyViaUrl: false, + PostProcessingCommand: "./app.exe", + PostProcessingEnvironment: []string{"a=123", "b=zba"}, + PostProcessingClientAddress: "xyz.com", + PostProcessingClientKeyB64: "aaa888aaabbbccc", + Profile: "new prof 2", + }, + nil, + 18, + certificates.Certificate{ + ID: 18, + Name: "somenewNameHere", + Description: "some new desc goes here", + Key: key58, + Account: acmeAcct2, + Subject: "desk.dude.example.com", + SubjectAltNames: []string{"new.example.com"}, + Organization: "new org 1", + OrganizationalUnit: "new orgu 1", + Country: "new orgco 1", + State: "new orgst 1", + City: "new orgci 1", + CSRExtraExtensions: []certificates.CertExtension{ + { + Extension: pkix.Extension{ + Id: asn1.ObjectIdentifier{1, 2, 4}, + Critical: true, + Value: []byte{0xaa, 0xbb, 0x11}, + }, + Description: "a", + }, + }, + PreferredRootCN: "different new cn", + LastAccess: time.Unix(1745952074, 0), + CreatedAt: time.Unix(1709327717, 0), + UpdatedAt: time.Unix(222223333, 0), + ApiKey: "api-key---", + ApiKeyNew: "api-key-new---", + ApiKeyViaUrl: false, + PostProcessingCommand: "./app.exe", + PostProcessingEnvironment: []string{"a=123", "b=zba"}, + PostProcessingClientAddress: "xyz.com", + PostProcessingClientKeyB64: "aaa888aaabbbccc", + Profile: "new prof 2", + }, + nil, + }, + // update nothing (except mandatory update time) + { + certificates.UpdatePayload{ + ID: 27, + UpdatedAt: time.Unix(15151515, 0), + }, + certificates.Certificate{ + ID: 27, + Name: "test008.test.example.com-p", + Description: "", + Key: key56, + Account: acmeAcct2, + Subject: "test008.test.example.com", + SubjectAltNames: []string{"test011.test.example.com", "*.test011.test.example.com"}, + Organization: "", + OrganizationalUnit: "", + Country: "", + State: "", + City: "", + CSRExtraExtensions: []certificates.CertExtension{}, + PreferredRootCN: "", + LastAccess: time.Unix(0, 0), + CreatedAt: time.Unix(1743171060, 0), + UpdatedAt: time.Unix(15151515, 0), + ApiKey: "api-secret-27", + ApiKeyNew: "", + ApiKeyViaUrl: false, + PostProcessingCommand: "", + PostProcessingEnvironment: []string{}, + PostProcessingClientAddress: "", + PostProcessingClientKeyB64: "", + Profile: "", + }, + nil, + 27, + certificates.Certificate{ + ID: 27, + Name: "test008.test.example.com-p", + Description: "", + Key: key56, + Account: acmeAcct2, + Subject: "test008.test.example.com", + SubjectAltNames: []string{"test011.test.example.com", "*.test011.test.example.com"}, + Organization: "", + OrganizationalUnit: "", + Country: "", + State: "", + City: "", + CSRExtraExtensions: []certificates.CertExtension{}, + PreferredRootCN: "", + LastAccess: time.Unix(0, 0), + CreatedAt: time.Unix(1743171060, 0), + UpdatedAt: time.Unix(15151515, 0), + ApiKey: "api-secret-27", + ApiKeyNew: "", + ApiKeyViaUrl: false, + PostProcessingCommand: "", + PostProcessingEnvironment: []string{}, + PostProcessingClientAddress: "", + PostProcessingClientKeyB64: "", + Profile: "", + }, + nil, + }, + // update a couple things (including empty slice) + { + certificates.UpdatePayload{ + ID: 27, + SubjectAltNames: []string{}, + PostProcessingClientAddress: new("someaddr.example.com"), + UpdatedAt: time.Unix(151222215, 0), + }, + certificates.Certificate{ + ID: 27, + Name: "test008.test.example.com-p", + Description: "", + Key: key56, + Account: acmeAcct2, + Subject: "test008.test.example.com", + SubjectAltNames: []string{}, + Organization: "", + OrganizationalUnit: "", + Country: "", + State: "", + City: "", + CSRExtraExtensions: []certificates.CertExtension{}, + PreferredRootCN: "", + LastAccess: time.Unix(0, 0), + CreatedAt: time.Unix(1743171060, 0), + UpdatedAt: time.Unix(151222215, 0), + ApiKey: "api-secret-27", + ApiKeyNew: "", + ApiKeyViaUrl: false, + PostProcessingCommand: "", + PostProcessingEnvironment: []string{}, + PostProcessingClientAddress: "someaddr.example.com", + PostProcessingClientKeyB64: "", + Profile: "", + }, + nil, + 27, + certificates.Certificate{ + ID: 27, + Name: "test008.test.example.com-p", + Description: "", + Key: key56, + Account: acmeAcct2, + Subject: "test008.test.example.com", + SubjectAltNames: []string{}, + Organization: "", + OrganizationalUnit: "", + Country: "", + State: "", + City: "", + CSRExtraExtensions: []certificates.CertExtension{}, + PreferredRootCN: "", + LastAccess: time.Unix(0, 0), + CreatedAt: time.Unix(1743171060, 0), + UpdatedAt: time.Unix(151222215, 0), + ApiKey: "api-secret-27", + ApiKeyNew: "", + ApiKeyViaUrl: false, + PostProcessingCommand: "", + PostProcessingEnvironment: []string{}, + PostProcessingClientAddress: "someaddr.example.com", + PostProcessingClientKeyB64: "", + Profile: "", + }, + nil, + }, + } + + // create testing service + storage, err := openStorageWithTestData(t, "putcertupdate") + if err != nil { + t.Fatal(err) + } + + for i, tc := range testCases { + t.Run(fmt.Sprintf("#%d (id: %d)", i, tc.payload.ID), func(t *testing.T) { + c, err := storage.PutCertUpdate(tc.payload) + if !helpers_test.ErrorsIs(err, tc.expectedPutErr) { + t.Errorf("expected put error '%s' but got '%s'", helpers_test.ErrorToVal(tc.expectedPutErr), helpers_test.ErrorToVal(err)) + } + + CompareCertificate(t, c, tc.expectedPutResult) + + c, err = storage.GetOneCertById(tc.getId) + if !helpers_test.ErrorsIs(err, tc.expectedGetErr) { + t.Errorf("expected get error '%s' but got '%s'", helpers_test.ErrorToVal(tc.expectedGetErr), helpers_test.ErrorToVal(err)) + } + + CompareCertificate(t, c, tc.expectedGetResult) + }) + } +} + +func TestPutCertApiKey(t *testing.T) { + testCases := []struct { + certId int + apiKey string + updateTime time.Time + + expectedCert certificates.Certificate + expectedPutErr error + expectedGetErr error + }{ + { // invalid id + -1, + "fake", + time.Unix(100005, 0), + certificates.Certificate{}, + storage.ErrWrongUpdateRowCount, + sql.ErrNoRows, + }, + { // invalid id + 500, + "anotherfake", + time.Unix(10005000, 0), + certificates.Certificate{}, + storage.ErrWrongUpdateRowCount, + sql.ErrNoRows, + }, + // do some updates + { + 18, + "somekey31cert", + time.Unix(101300522, 0), + certificates.Certificate{ + ID: 18, + Name: "serverdefault", + Description: "its a decript", + Key: key31, + Account: acmeAcct2, + Subject: "desk.dude.example.com", + SubjectAltNames: []string{"test011.test.example.com"}, + Organization: "my org", + OrganizationalUnit: "my ou", + Country: "your country", + State: "a state", + City: "springfield", + CSRExtraExtensions: []certificates.CertExtension{ + { + Extension: pkix.Extension{ + Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 24}, + Critical: false, + Value: []byte{0x30, 0x03, 0x02, 0x01, 0x05}, + }, + Description: "OCSP Must Staple", + }, + }, + PreferredRootCN: "ISRG Root X1", + LastAccess: time.Unix(1745952074, 0), + CreatedAt: time.Unix(1709327717, 0), + UpdatedAt: time.Unix(101300522, 0), + ApiKey: "somekey31cert", + ApiKeyNew: "api-new-secret-18", + ApiKeyViaUrl: true, + PostProcessingCommand: "./scripts/windows/post-processing.example.ps1", + PostProcessingEnvironment: []string{"asdasdasdsd=asasd"}, + PostProcessingClientAddress: "dude.greg.example.com", + PostProcessingClientKeyB64: "aaaaaaaaaaaaaaaaaaaaaaaaaaa-ccccccccccccccc", + Profile: "tlsserver", + }, + nil, + nil, + }, + { + 26, + "", + time.Unix(0, 0), + certificates.Certificate{ + ID: 26, + Name: "test008.test.example.com", + Description: "", + Key: key55, + Account: acmeAcct1, + Subject: "test008.test.example.com", + SubjectAltNames: []string{}, + Organization: "", + OrganizationalUnit: "", + Country: "", + State: "", + City: "", + CSRExtraExtensions: []certificates.CertExtension{}, + PreferredRootCN: "", + LastAccess: time.Unix(0, 0), + CreatedAt: time.Unix(1743170701, 0), + UpdatedAt: time.Unix(0, 0), + ApiKey: "", + ApiKeyNew: "", + ApiKeyViaUrl: false, + PostProcessingCommand: "./scripts/windows/post-processing.example.ps1", + PostProcessingEnvironment: []string{}, + PostProcessingClientAddress: "test008.test.example.com", + PostProcessingClientKeyB64: "", + Profile: "", + }, + nil, + nil, + }, + } + + // create testing service + storage, err := openStorageWithTestData(t, "putcertapikey") + if err != nil { + t.Fatal(err) + } + + for _, tc := range testCases { + t.Run(fmt.Sprintf("id: %d)", tc.certId), func(t *testing.T) { + err := storage.PutCertApiKey(tc.certId, tc.apiKey, tc.updateTime) + if !helpers_test.ErrorsIs(err, tc.expectedPutErr) { + t.Errorf("expected put cert api key error '%s' but got '%s'", helpers_test.ErrorToVal(tc.expectedPutErr), helpers_test.ErrorToVal(err)) + } + + cert, err := storage.GetOneCertById(tc.certId) + if !helpers_test.ErrorsIs(err, tc.expectedGetErr) { + t.Errorf("expected get cert error '%s' but got '%s'", helpers_test.ErrorToVal(tc.expectedGetErr), helpers_test.ErrorToVal(err)) + } + + CompareCertificate(t, cert, tc.expectedCert) + }) + } +} + +func TestCertApiKeyNew(t *testing.T) { + testCases := []struct { + certId int + apiKeyNew string + updatedAt time.Time + + expectedCert certificates.Certificate + expectedPutErr error + expectedGetErr error + }{ + { // invalid id + -1, + "", + time.Unix(10999051, 0), + certificates.Certificate{}, + storage.ErrWrongUpdateRowCount, + sql.ErrNoRows, + }, + { // invalid id + 500, + "anotherfake", + time.Unix(10445000, 0), + certificates.Certificate{}, + storage.ErrWrongUpdateRowCount, + sql.ErrNoRows, + }, + // do some updates + { + 27, + "certkey27", + time.Unix(102202305, 0), + certificates.Certificate{ + ID: 27, + Name: "test008.test.example.com-p", + Description: "", + Key: key56, + Account: acmeAcct2, + Subject: "test008.test.example.com", + SubjectAltNames: []string{"test011.test.example.com", "*.test011.test.example.com"}, + Organization: "", + OrganizationalUnit: "", + Country: "", + State: "", + City: "", + CSRExtraExtensions: []certificates.CertExtension{}, + PreferredRootCN: "", + LastAccess: time.Unix(0, 0), + CreatedAt: time.Unix(1743171060, 0), + UpdatedAt: time.Unix(102202305, 0), + ApiKey: "api-secret-27", + ApiKeyNew: "certkey27", + ApiKeyViaUrl: false, + PostProcessingCommand: "", + PostProcessingEnvironment: []string{}, + PostProcessingClientAddress: "", + PostProcessingClientKeyB64: "", + Profile: "", + }, + nil, + nil, + }, + { + 18, + "", + time.Unix(0, 0), + certificates.Certificate{ + ID: 18, + Name: "serverdefault", + Description: "its a decript", + Key: key31, + Account: acmeAcct2, + Subject: "desk.dude.example.com", + SubjectAltNames: []string{"test011.test.example.com"}, + Organization: "my org", + OrganizationalUnit: "my ou", + Country: "your country", + State: "a state", + City: "springfield", + CSRExtraExtensions: []certificates.CertExtension{ + { + Extension: pkix.Extension{ + Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 24}, + Critical: false, + Value: []byte{0x30, 0x03, 0x02, 0x01, 0x05}, + }, + Description: "OCSP Must Staple", + }, + }, + PreferredRootCN: "ISRG Root X1", + LastAccess: time.Unix(1745952074, 0), + CreatedAt: time.Unix(1709327717, 0), + UpdatedAt: time.Unix(0, 0), + ApiKey: "api-secret-18", + ApiKeyNew: "", + ApiKeyViaUrl: true, + PostProcessingCommand: "./scripts/windows/post-processing.example.ps1", + PostProcessingEnvironment: []string{"asdasdasdsd=asasd"}, + PostProcessingClientAddress: "dude.greg.example.com", + PostProcessingClientKeyB64: "aaaaaaaaaaaaaaaaaaaaaaaaaaa-ccccccccccccccc", + Profile: "tlsserver", + }, + nil, + nil, + }, + } + + // create testing service + storage, err := openStorageWithTestData(t, "putkeyapikeynew") + if err != nil { + t.Fatal(err) + } + + for _, tc := range testCases { + t.Run(fmt.Sprintf("id: %d)", tc.certId), func(t *testing.T) { + err := storage.PutCertApiKeyNew(tc.certId, tc.apiKeyNew, tc.updatedAt) + if !helpers_test.ErrorsIs(err, tc.expectedPutErr) { + t.Errorf("expected apikeynew put error '%s' but got '%s'", helpers_test.ErrorToVal(tc.expectedPutErr), helpers_test.ErrorToVal(err)) + } + + cert, err := storage.GetOneCertById(tc.certId) + if !helpers_test.ErrorsIs(err, tc.expectedGetErr) { + t.Errorf("expected cert get error '%s' but got '%s'", helpers_test.ErrorToVal(tc.expectedGetErr), helpers_test.ErrorToVal(err)) + } + + CompareCertificate(t, cert, tc.expectedCert) + }) + } + +} + +func TestPutCertUpdatedAt(t *testing.T) { + testCases := []struct { + certId int + updatedAt time.Time + + expectedCert certificates.Certificate + expectedPutErr error + expectedGetErr error + }{ + { // invalid key id + -1, + time.Unix(28888111, 0), + certificates.Certificate{}, + storage.ErrWrongUpdateRowCount, + sql.ErrNoRows, + }, + { // invalid key id + 500, + time.Unix(28888222, 0), + certificates.Certificate{}, + storage.ErrWrongUpdateRowCount, + sql.ErrNoRows, + }, + // do update + { + 18, + time.Unix(0, 0), + certificates.Certificate{ + ID: 18, + Name: "serverdefault", + Description: "its a decript", + Key: key31, + Account: acmeAcct2, + Subject: "desk.dude.example.com", + SubjectAltNames: []string{"test011.test.example.com"}, + Organization: "my org", + OrganizationalUnit: "my ou", + Country: "your country", + State: "a state", + City: "springfield", + CSRExtraExtensions: []certificates.CertExtension{ + { + Extension: pkix.Extension{ + Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 24}, + Critical: false, + Value: []byte{0x30, 0x03, 0x02, 0x01, 0x05}, + }, + Description: "OCSP Must Staple", + }, + }, + PreferredRootCN: "ISRG Root X1", + LastAccess: time.Unix(1745952074, 0), + CreatedAt: time.Unix(1709327717, 0), + UpdatedAt: time.Unix(0, 0), + ApiKey: "api-secret-18", + ApiKeyNew: "api-new-secret-18", + ApiKeyViaUrl: true, + PostProcessingCommand: "./scripts/windows/post-processing.example.ps1", + PostProcessingEnvironment: []string{"asdasdasdsd=asasd"}, + PostProcessingClientAddress: "dude.greg.example.com", + PostProcessingClientKeyB64: "aaaaaaaaaaaaaaaaaaaaaaaaaaa-ccccccccccccccc", + Profile: "tlsserver", + }, + nil, + nil, + }, + { + 18, + time.Unix(333444442, 0), + certificates.Certificate{ + ID: 18, + Name: "serverdefault", + Description: "its a decript", + Key: key31, + Account: acmeAcct2, + Subject: "desk.dude.example.com", + SubjectAltNames: []string{"test011.test.example.com"}, + Organization: "my org", + OrganizationalUnit: "my ou", + Country: "your country", + State: "a state", + City: "springfield", + CSRExtraExtensions: []certificates.CertExtension{ + { + Extension: pkix.Extension{ + Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 24}, + Critical: false, + Value: []byte{0x30, 0x03, 0x02, 0x01, 0x05}, + }, + Description: "OCSP Must Staple", + }, + }, + PreferredRootCN: "ISRG Root X1", + LastAccess: time.Unix(1745952074, 0), + CreatedAt: time.Unix(1709327717, 0), + UpdatedAt: time.Unix(333444442, 0), + ApiKey: "api-secret-18", + ApiKeyNew: "api-new-secret-18", + ApiKeyViaUrl: true, + PostProcessingCommand: "./scripts/windows/post-processing.example.ps1", + PostProcessingEnvironment: []string{"asdasdasdsd=asasd"}, + PostProcessingClientAddress: "dude.greg.example.com", + PostProcessingClientKeyB64: "aaaaaaaaaaaaaaaaaaaaaaaaaaa-ccccccccccccccc", + Profile: "tlsserver", + }, + nil, + nil, + }, + } + + // create testing service + storage, err := openStorageWithTestData(t, "putcertupdatedat") + if err != nil { + t.Fatal(err) + } + + for _, tc := range testCases { + t.Run(fmt.Sprintf("id: %d)", tc.certId), func(t *testing.T) { + err := storage.PutCertUpdatedAt(tc.certId, tc.updatedAt) + if !helpers_test.ErrorsIs(err, tc.expectedPutErr) { + t.Errorf("expected put error '%s' but got '%s'", helpers_test.ErrorToVal(tc.expectedPutErr), helpers_test.ErrorToVal(err)) + } + + record, err := storage.GetOneCertById(tc.certId) + if !helpers_test.ErrorsIs(err, tc.expectedGetErr) { + t.Errorf("expected get error '%s' but got '%s'", helpers_test.ErrorToVal(tc.expectedGetErr), helpers_test.ErrorToVal(err)) + } + + CompareCertificate(t, record, tc.expectedCert) + }) + } + +} + +func TestPutCertClientKey(t *testing.T) { + testCases := []struct { + certId int + newKey string + updatedAt time.Time + + expectedCert certificates.Certificate + expectedPutErr error + expectedGetErr error + }{ + { // invalid key id 1 + -1, + "new-b64-key", + time.Unix(8883333, 0), + certificates.Certificate{}, + storage.ErrWrongUpdateRowCount, + sql.ErrNoRows, + }, + { // invalid key id 2 + 8888, + "new-b64-key", + time.Unix(88833331, 0), + certificates.Certificate{}, + storage.ErrWrongUpdateRowCount, + sql.ErrNoRows, + }, + { // valid update + 18, + "new-b64-key-xxx111yyyy", + time.Unix(88832231, 0), + certificates.Certificate{ + ID: 18, + Name: "serverdefault", + Description: "its a decript", + Key: key31, + Account: acmeAcct2, + Subject: "desk.dude.example.com", + SubjectAltNames: []string{"test011.test.example.com"}, + Organization: "my org", + OrganizationalUnit: "my ou", + Country: "your country", + State: "a state", + City: "springfield", + CSRExtraExtensions: []certificates.CertExtension{ + { + Extension: pkix.Extension{ + Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 24}, + Critical: false, + Value: []byte{0x30, 0x03, 0x02, 0x01, 0x05}, + }, + Description: "OCSP Must Staple", + }, + }, + PreferredRootCN: "ISRG Root X1", + LastAccess: time.Unix(1745952074, 0), + CreatedAt: time.Unix(1709327717, 0), + UpdatedAt: time.Unix(88832231, 0), + ApiKey: "api-secret-18", + ApiKeyNew: "api-new-secret-18", + ApiKeyViaUrl: true, + PostProcessingCommand: "./scripts/windows/post-processing.example.ps1", + PostProcessingEnvironment: []string{"asdasdasdsd=asasd"}, + PostProcessingClientAddress: "dude.greg.example.com", + PostProcessingClientKeyB64: "new-b64-key-xxx111yyyy", + Profile: "tlsserver", + }, + nil, + nil, + }, + } + + // create testing service + storage, err := openStorageWithTestData(t, "putcertclientkey") + if err != nil { + t.Fatal(err) + } + + for _, tc := range testCases { + t.Run(fmt.Sprintf("id: %d)", tc.certId), func(t *testing.T) { + err := storage.PutCertClientKey(tc.certId, tc.newKey, tc.updatedAt) + if !helpers_test.ErrorsIs(err, tc.expectedPutErr) { + t.Errorf("expected put error '%s' but got '%s'", helpers_test.ErrorToVal(tc.expectedPutErr), helpers_test.ErrorToVal(err)) + } + + record, err := storage.GetOneCertById(tc.certId) + if !helpers_test.ErrorsIs(err, tc.expectedGetErr) { + t.Errorf("expected get error '%s' but got '%s'", helpers_test.ErrorToVal(tc.expectedGetErr), helpers_test.ErrorToVal(err)) + } + + CompareCertificate(t, record, tc.expectedCert) + }) + } +} + +func TestPutCertLastAccess(t *testing.T) { + testCases := []struct { + certId int + lastAccess time.Time + + expectedCert certificates.Certificate + expectedPutErr error + expectedGetErr error + }{ + { // invalid key id + -1, + time.Unix(88888111, 0), + certificates.Certificate{}, + storage.ErrWrongUpdateRowCount, + sql.ErrNoRows, + }, + { // invalid key id + 500, + time.Unix(88888222, 0), + certificates.Certificate{}, + storage.ErrWrongUpdateRowCount, + sql.ErrNoRows, + }, + // do update + { + 18, + time.Unix(0, 0), + certificates.Certificate{ + ID: 18, + Name: "serverdefault", + Description: "its a decript", + Key: key31, + Account: acmeAcct2, + Subject: "desk.dude.example.com", + SubjectAltNames: []string{"test011.test.example.com"}, + Organization: "my org", + OrganizationalUnit: "my ou", + Country: "your country", + State: "a state", + City: "springfield", + CSRExtraExtensions: []certificates.CertExtension{ + { + Extension: pkix.Extension{ + Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 24}, + Critical: false, + Value: []byte{0x30, 0x03, 0x02, 0x01, 0x05}, + }, + Description: "OCSP Must Staple", + }, + }, + PreferredRootCN: "ISRG Root X1", + LastAccess: time.Unix(0, 0), + CreatedAt: time.Unix(1709327717, 0), + UpdatedAt: time.Unix(1779386440, 0), + ApiKey: "api-secret-18", + ApiKeyNew: "api-new-secret-18", + ApiKeyViaUrl: true, + PostProcessingCommand: "./scripts/windows/post-processing.example.ps1", + PostProcessingEnvironment: []string{"asdasdasdsd=asasd"}, + PostProcessingClientAddress: "dude.greg.example.com", + PostProcessingClientKeyB64: "aaaaaaaaaaaaaaaaaaaaaaaaaaa-ccccccccccccccc", + Profile: "tlsserver", + }, + nil, + nil, + }, + { + 18, + time.Unix(1122885, 0), + certificates.Certificate{ + ID: 18, + Name: "serverdefault", + Description: "its a decript", + Key: key31, + Account: acmeAcct2, + Subject: "desk.dude.example.com", + SubjectAltNames: []string{"test011.test.example.com"}, + Organization: "my org", + OrganizationalUnit: "my ou", + Country: "your country", + State: "a state", + City: "springfield", + CSRExtraExtensions: []certificates.CertExtension{ + { + Extension: pkix.Extension{ + Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 24}, + Critical: false, + Value: []byte{0x30, 0x03, 0x02, 0x01, 0x05}, + }, + Description: "OCSP Must Staple", + }, + }, + PreferredRootCN: "ISRG Root X1", + LastAccess: time.Unix(1122885, 0), + CreatedAt: time.Unix(1709327717, 0), + UpdatedAt: time.Unix(1779386440, 0), + ApiKey: "api-secret-18", + ApiKeyNew: "api-new-secret-18", + ApiKeyViaUrl: true, + PostProcessingCommand: "./scripts/windows/post-processing.example.ps1", + PostProcessingEnvironment: []string{"asdasdasdsd=asasd"}, + PostProcessingClientAddress: "dude.greg.example.com", + PostProcessingClientKeyB64: "aaaaaaaaaaaaaaaaaaaaaaaaaaa-ccccccccccccccc", + Profile: "tlsserver", + }, + nil, + nil, + }, + } + + // create testing service + storage, err := openStorageWithTestData(t, "putcertlastaccess") + if err != nil { + t.Fatal(err) + } + + for _, tc := range testCases { + t.Run(fmt.Sprintf("id: %d)", tc.certId), func(t *testing.T) { + err := storage.PutCertLastAccess(tc.certId, tc.lastAccess) + if !helpers_test.ErrorsIs(err, tc.expectedPutErr) { + t.Errorf("expected put error '%s' but got '%s'", helpers_test.ErrorToVal(tc.expectedPutErr), helpers_test.ErrorToVal(err)) + } + + record, err := storage.GetOneCertById(tc.certId) + if !helpers_test.ErrorsIs(err, tc.expectedGetErr) { + t.Errorf("expected get error '%s' but got '%s'", helpers_test.ErrorToVal(tc.expectedGetErr), helpers_test.ErrorToVal(err)) + } + + CompareCertificate(t, record, tc.expectedCert) + }) + } +} diff --git a/pkg/storage/certificates_test.go b/pkg/storage/certificates_test.go new file mode 100644 index 00000000..16845d3c --- /dev/null +++ b/pkg/storage/certificates_test.go @@ -0,0 +1,144 @@ +package storage_test + +import ( + "bytes" + "certwarden-backend/pkg/domain/certificates" + "slices" + "testing" +) + +// CompareCertificateCSRExtensions is for comparing the special csr extra extensions +func CompareCertificateCSRExtensions(t *testing.T, extns, expectedExtns []certificates.CertExtension) { + if len(extns) != len(expectedExtns) { + t.Errorf("certificate: csr extra extensions expected length '%d' but got '%d'", len(expectedExtns), len(extns)) + } + + // check all expected + for i, extn := range expectedExtns { + if i >= len(extns) { + t.Errorf("certificate: csr extra extensions expected '%v' but got none", extn) + continue + } + + // check each field + if !slices.Equal(extn.Id, extns[i].Id) { + t.Errorf("certificate: csr extra extension %d expected oid '%v' but got '%v'", i, extn.Id, extns[i].Id) + } + + if extn.Critical != extns[i].Critical { + t.Errorf("certificate: csr extra extension %d expected critical '%t' but got '%t'", i, extn.Critical, extns[i].Critical) + } + + if !bytes.Equal(extn.Value, extns[i].Value) { + t.Errorf("certificate: csr extra extension %d expected value '%v' but got '%v'", i, extn.Value, extns[i].Value) + } + + if extn.Description != extns[i].Description { + t.Errorf("certificate: csr extra extension %d expected description '%s' but got '%s'", i, extn.Description, extns[i].Description) + } + } + + // error for any extras + if len(extns) > len(expectedExtns) { + for i := len(expectedExtns); i < len(extns); i++ { + t.Errorf("certificate: csr extra extensions expected no additional but got '%v'", extns[i]) + } + } +} + +// CompareCertificate compares cert to expectedCert and throws appropriate errors for any differences +func CompareCertificate(t *testing.T, cert, expectedCert certificates.Certificate) { + if cert.ID != expectedCert.ID { + t.Errorf("certificate: id expected '%d' but got '%d'", expectedCert.ID, cert.ID) + } + + if cert.Name != expectedCert.Name { + t.Errorf("certificate: name expected '%s' but got '%s'", expectedCert.Name, cert.Name) + } + + if cert.Description != expectedCert.Description { + t.Errorf("certificate: description expected '%s' but got '%s'", expectedCert.Description, cert.Description) + } + + CompareKey(t, cert.Key, expectedCert.Key) + + CompareAcmeAccount(t, cert.Account, expectedCert.Account) + + if cert.Subject != expectedCert.Subject { + t.Errorf("certificate: subject expected '%s' but got '%s'", expectedCert.Subject, cert.Subject) + } + + if !slices.Equal(cert.SubjectAltNames, expectedCert.SubjectAltNames) { + t.Errorf("certificate: subject alt names expected '%v' but got '%v'", expectedCert.SubjectAltNames, cert.SubjectAltNames) + } + + if cert.Organization != expectedCert.Organization { + t.Errorf("certificate: organization expected '%s' but got '%s'", expectedCert.Organization, cert.Organization) + } + + if cert.OrganizationalUnit != expectedCert.OrganizationalUnit { + t.Errorf("certificate: organizational unit expected '%s' but got '%s'", expectedCert.OrganizationalUnit, cert.OrganizationalUnit) + } + + if cert.Country != expectedCert.Country { + t.Errorf("certificate: country expected '%s' but got '%s'", expectedCert.Country, cert.Country) + } + + if cert.State != expectedCert.State { + t.Errorf("certificate: state expected '%s' but got '%s'", expectedCert.State, cert.State) + } + + if cert.City != expectedCert.City { + t.Errorf("certificate: city expected '%s' but got '%s'", expectedCert.City, cert.City) + } + + CompareCertificateCSRExtensions(t, cert.CSRExtraExtensions, expectedCert.CSRExtraExtensions) + + if cert.PreferredRootCN != expectedCert.PreferredRootCN { + t.Errorf("certificate: preferred root cn expected '%s' but got '%s'", expectedCert.PreferredRootCN, cert.PreferredRootCN) + } + + if !cert.LastAccess.Equal(expectedCert.LastAccess) { + t.Errorf("certificate: last access expected '%s' but got '%s'", expectedCert.LastAccess.UTC(), cert.LastAccess.UTC()) + } + + if !cert.CreatedAt.Equal(expectedCert.CreatedAt) { + t.Errorf("certificater: created at expected '%s' but got '%s'", expectedCert.CreatedAt.UTC(), cert.CreatedAt.UTC()) + } + + if !cert.UpdatedAt.Equal(expectedCert.UpdatedAt) { + t.Errorf("certificate: updated at expected '%s' but got '%s'", expectedCert.UpdatedAt.UTC(), cert.UpdatedAt.UTC()) + } + + if cert.ApiKey != expectedCert.ApiKey { + t.Errorf("certificate: api key expected '%s' but got '%s'", expectedCert.ApiKey, cert.ApiKey) + } + + if cert.ApiKeyNew != expectedCert.ApiKeyNew { + t.Errorf("certificate: api key new expected '%s' but got '%s'", expectedCert.ApiKeyNew, cert.ApiKeyNew) + } + + if cert.ApiKeyViaUrl != expectedCert.ApiKeyViaUrl { + t.Errorf("acme server: api key via url expected '%t' but got '%t'", expectedCert.ApiKeyViaUrl, cert.ApiKeyViaUrl) + } + + if cert.PostProcessingCommand != expectedCert.PostProcessingCommand { + t.Errorf("acme server: post processing command expected '%s' but got '%s'", expectedCert.PostProcessingCommand, cert.PostProcessingCommand) + } + + if !slices.Equal(cert.PostProcessingEnvironment, expectedCert.PostProcessingEnvironment) { + t.Errorf("certificate: post processing environment expected '%v' but got '%v'", expectedCert.PostProcessingEnvironment, cert.PostProcessingEnvironment) + } + + if cert.PostProcessingClientAddress != expectedCert.PostProcessingClientAddress { + t.Errorf("acme server: post processing client address expected '%s' but got '%s'", expectedCert.PostProcessingClientAddress, cert.PostProcessingClientAddress) + } + + if cert.PostProcessingClientKeyB64 != expectedCert.PostProcessingClientKeyB64 { + t.Errorf("acme server: post processing client key base64 expected '%s' but got '%s'", expectedCert.PostProcessingClientKeyB64, cert.PostProcessingClientKeyB64) + } + + if cert.Profile != expectedCert.Profile { + t.Errorf("acme server: profile expected '%s' but got '%s'", expectedCert.Profile, cert.Profile) + } +} diff --git a/pkg/storage/keys_delete.go b/pkg/storage/keys_delete.go index 42f39cda..bcdb9a69 100644 --- a/pkg/storage/keys_delete.go +++ b/pkg/storage/keys_delete.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "errors" + "fmt" ) // KeyInUse returns a bool if the specified key is in use, it returns @@ -16,6 +17,12 @@ func (store *Storage) KeyInUse(id int) (inUse bool, err error) { ctx, cancel := context.WithTimeout(store.shutdownContext, store.timeout) defer cancel() + tx, err := store.db.BeginTx(ctx, nil) + if err != nil { + return false, err + } + defer tx.Rollback() + // check key exists query := ` SELECT id @@ -23,7 +30,7 @@ func (store *Storage) KeyInUse(id int) (inUse bool, err error) { WHERE id = $1 ` - row := store.db.QueryRowContext(ctx, query, id) + row := tx.QueryRowContext(ctx, query, id) _discardVar := -2 err = row.Scan(&_discardVar) if err != nil { @@ -39,7 +46,7 @@ func (store *Storage) KeyInUse(id int) (inUse bool, err error) { WHERE private_key_id = $1 ` - row = store.db.QueryRowContext(ctx, query, id) + row = tx.QueryRowContext(ctx, query, id) err = row.Scan(&_discardVar) if !errors.Is(err, sql.ErrNoRows) { return true, err @@ -54,7 +61,7 @@ func (store *Storage) KeyInUse(id int) (inUse bool, err error) { WHERE private_key_id = $1 ` - row = store.db.QueryRowContext(ctx, query, id) + row = tx.QueryRowContext(ctx, query, id) err = row.Scan(&_discardVar) if !errors.Is(err, sql.ErrNoRows) { return true, err @@ -86,20 +93,22 @@ func (store *Storage) KeyInUse(id int) (inUse bool, err error) { finalized_key_id = $1 ` - row = store.db.QueryRowContext(ctx, query, id) + row = tx.QueryRowContext(ctx, query, id) err = row.Scan(&_discardVar) if !errors.Is(err, sql.ErrNoRows) { return true, err } + err = tx.Commit() + if err != nil { + return false, err + } + return false, nil } // DeleteKey deletes a private key from the database func (store *Storage) DeleteKey(id int) error { - ctx, cancel := context.WithTimeout(store.shutdownContext, store.timeout) - defer cancel() - // check that delete is safe inUse, err := store.KeyInUse(id) if err != nil { @@ -109,6 +118,9 @@ func (store *Storage) DeleteKey(id int) error { return ErrInUse } + ctx, cancel := context.WithTimeout(store.shutdownContext, store.timeout) + defer cancel() + // delete query := ` DELETE FROM @@ -117,10 +129,19 @@ func (store *Storage) DeleteKey(id int) error { id = $1 ` - _, err = store.db.ExecContext(ctx, query, id) + res, err := store.db.ExecContext(ctx, query, id) if err != nil { return err } + // verify update actually happened + rowsAffected, err := res.RowsAffected() + if err != nil { + return err + } + if rowsAffected != 1 { + return errors.Join(fmt.Errorf("expected 1 row update, but got '%d'", rowsAffected), ErrWrongUpdateRowCount) + } + return nil } diff --git a/pkg/storage/keys_delete_test.go b/pkg/storage/keys_delete_test.go index ab070b89..cbf89f96 100644 --- a/pkg/storage/keys_delete_test.go +++ b/pkg/storage/keys_delete_test.go @@ -2,10 +2,9 @@ package storage_test import ( "certwarden-backend/pkg/domain/private_keys" + "certwarden-backend/pkg/helpers_test" "certwarden-backend/pkg/storage" - "certwarden-backend/pkg/test_helpers" "database/sql" - "errors" "fmt" "testing" ) @@ -34,10 +33,10 @@ func TestKeyInUse(t *testing.T) { } for _, tc := range testCases { - t.Run(fmt.Sprintf("key id: %d", tc.keyID), func(t *testing.T) { + t.Run(fmt.Sprintf("id: %d", tc.keyID), func(t *testing.T) { inUse, err := storage.KeyInUse(tc.keyID) - if !errors.Is(err, tc.expectedErr) { - t.Errorf("expected error '%s' but got '%s'", test_helpers.ErrorToVal(tc.expectedErr), test_helpers.ErrorToVal(err)) + if !helpers_test.ErrorsIs(err, tc.expectedErr) { + t.Errorf("expected error '%s' but got '%s'", helpers_test.ErrorToVal(tc.expectedErr), helpers_test.ErrorToVal(err)) } if inUse != tc.expectedInUse { @@ -69,15 +68,15 @@ func TestDeleteKey(t *testing.T) { } for _, tc := range testCases { - t.Run(fmt.Sprintf("key id: %d", tc.keyID), func(t *testing.T) { + t.Run(fmt.Sprintf("id: %d", tc.keyID), func(t *testing.T) { err := storage.DeleteKey(tc.keyID) - if !errors.Is(err, tc.expectedDelErr) { - t.Errorf("expected delete error '%s' but got '%s'", test_helpers.ErrorToVal(tc.expectedDelErr), test_helpers.ErrorToVal(err)) + if !helpers_test.ErrorsIs(err, tc.expectedDelErr) { + t.Errorf("expected delete error '%s' but got '%s'", helpers_test.ErrorToVal(tc.expectedDelErr), helpers_test.ErrorToVal(err)) } key, err := storage.GetOneKeyById(tc.keyID) - if !errors.Is(err, tc.expectedGetErr) { - t.Errorf("expected get error '%s' but got '%s'", test_helpers.ErrorToVal(tc.expectedGetErr), test_helpers.ErrorToVal(err)) + if !helpers_test.ErrorsIs(err, tc.expectedGetErr) { + t.Errorf("expected get error '%s' but got '%s'", helpers_test.ErrorToVal(tc.expectedGetErr), helpers_test.ErrorToVal(err)) } CompareKey(t, key, tc.expectedGetResult) diff --git a/pkg/storage/keys_get_test.go b/pkg/storage/keys_get_test.go index a06b2612..5196d733 100644 --- a/pkg/storage/keys_get_test.go +++ b/pkg/storage/keys_get_test.go @@ -3,10 +3,9 @@ package storage_test import ( "certwarden-backend/pkg/domain/private_keys" "certwarden-backend/pkg/domain/private_keys/key_crypto" + "certwarden-backend/pkg/helpers_test" "certwarden-backend/pkg/pagination_sort" - "certwarden-backend/pkg/test_helpers" "database/sql" - "errors" "fmt" "testing" "time" @@ -68,6 +67,42 @@ red-31 UpdatedAt: time.Unix(1732748628, 0), } + key55 = private_keys.Key{ + ID: 55, + Name: "test008.test.example.com", + Description: "", + Algorithm: key_crypto.AlgorithmECDSAp256, + Pem: `-----BEGIN EC PRIVATE KEY----- +red-55 +-----END EC PRIVATE KEY----- +`, + ApiKey: "key-api-key-55", + ApiKeyNew: "", + ApiKeyDisabled: false, + ApiKeyViaUrl: false, + LastAccess: time.Unix(0, 0), + CreatedAt: time.Unix(1743170701, 0), + UpdatedAt: time.Unix(0, 0), + } + + key56 = private_keys.Key{ + ID: 56, + Name: "test008.test.example.com-p", + Description: "", + Algorithm: key_crypto.AlgorithmECDSAp256, + Pem: `-----BEGIN EC PRIVATE KEY----- +red-56 +-----END EC PRIVATE KEY----- +`, + ApiKey: "key-api-key-56", + ApiKeyNew: "", + ApiKeyDisabled: false, + ApiKeyViaUrl: false, + LastAccess: time.Unix(0, 0), + CreatedAt: time.Unix(1743171060, 0), + UpdatedAt: time.Unix(0, 0), + } + key58 = private_keys.Key{ ID: 58, Name: "_Buypass_Staging", @@ -227,8 +262,7 @@ red-69 ApiKeyViaUrl: false, LastAccess: time.Unix(1777555692, 0), CreatedAt: time.Unix(1775761592, 0), - // TODO: Research why this is 0 in the db. Might find while writing other tests. Also possible this is just junk data from a previous bug. - UpdatedAt: time.Unix(0, 0), + UpdatedAt: time.Unix(0, 0), } ) @@ -242,8 +276,8 @@ func TestGetAllKeys(t *testing.T) { expectedKeyAtIndx private_keys.Key }{ {pagination_sort.Query{}, 19, 19, 0, key63}, - {QueryBuilderForTest(5, 15, "algorithm", true), 19, 4, 2, key67}, - {QueryBuilderForTest(10, 0, "last_access", false), 19, 10, 2, key31}, + {queryBuilderForTest(5, 15, "algorithm", true), 19, 4, 2, key67}, + {queryBuilderForTest(10, 0, "last_access", false), 19, 10, 2, key31}, } // create testing service @@ -256,20 +290,20 @@ func TestGetAllKeys(t *testing.T) { t.Run(fmt.Sprintf("#%d (%s)", i, tc.expectedKeyAtIndx.Name), func(t *testing.T) { keys, totalCt, err := storage.GetAllKeys(tc.q) if err != nil { - t.Errorf("get all keys failed") + t.Errorf("get all failed") return } if totalCt != tc.expectedTotalCt { - t.Errorf("get all keys returned incorrect total count, expected '%d' but got '%d'", tc.expectedTotalCt, totalCt) + t.Errorf("incorrect total count, expected '%d' but got '%d'", tc.expectedTotalCt, totalCt) } if len(keys) != tc.expectedResultLen { - t.Errorf("get all keys returned incorrect keys length, expected '%d' but got '%d'", tc.expectedResultLen, len(keys)) + t.Errorf("incorrect result length, expected '%d' but got '%d'", tc.expectedResultLen, len(keys)) } if tc.testIndx <= len(keys)-1 { CompareKey(t, keys[tc.testIndx], tc.expectedKeyAtIndx) } else { - t.Errorf("couldnt test key at index '%d' because length of key array was only '%d'", tc.testIndx, len(keys)) + t.Errorf("couldnt test result at index '%d' because length of result array was only '%d'", tc.testIndx, len(keys)) } }) } @@ -296,8 +330,8 @@ func TestGetOneKeyById(t *testing.T) { for i, tc := range testCases { t.Run(fmt.Sprintf("#%d (id: %d)", i, tc.id), func(t *testing.T) { key, err := storage.GetOneKeyById(tc.id) - if !errors.Is(err, tc.expectedErr) { - t.Errorf("expected error '%s' but got '%s'", test_helpers.ErrorToVal(tc.expectedErr), test_helpers.ErrorToVal(err)) + if !helpers_test.ErrorsIs(err, tc.expectedErr) { + t.Errorf("expected error '%s' but got '%s'", helpers_test.ErrorToVal(tc.expectedErr), helpers_test.ErrorToVal(err)) } CompareKey(t, key, tc.expectedKey) @@ -313,8 +347,8 @@ func TestGetOneKeyByName(t *testing.T) { }{ {"", sql.ErrNoRows, private_keys.Key{}}, {"fake-bad-name", sql.ErrNoRows, private_keys.Key{}}, - {"certwarden", nil, key31}, - {"_Another_Test_Acct_LE_Staging", nil, key63}, + {"cerTWarden", nil, key31}, + {"_Another_TEST_Acct_le_Staging", nil, key63}, // case is wrong } // create testing service @@ -326,8 +360,8 @@ func TestGetOneKeyByName(t *testing.T) { for i, tc := range testCases { t.Run(fmt.Sprintf("#%d (name: %s)", i, tc.name), func(t *testing.T) { key, err := storage.GetOneKeyByName(tc.name) - if !errors.Is(err, tc.expectedErr) { - t.Errorf("expected error '%s' but got '%s'", test_helpers.ErrorToVal(tc.expectedErr), test_helpers.ErrorToVal(err)) + if !helpers_test.ErrorsIs(err, tc.expectedErr) { + t.Errorf("expected error '%s' but got '%s'", helpers_test.ErrorToVal(tc.expectedErr), helpers_test.ErrorToVal(err)) } CompareKey(t, key, tc.expectedKey) @@ -344,19 +378,19 @@ func TestGetAvailableKeys(t *testing.T) { keys, err := storage.GetAvailableKeys() if err != nil { - t.Errorf("get available keys failed") + t.Errorf("get all failed") return } expectedResultLen := 3 if len(keys) != expectedResultLen { - t.Errorf("get available keys returned incorrect keys length, expected '%d' but got '%d'", expectedResultLen, len(keys)) + t.Errorf("returned incorrect result length, expected '%d' but got '%d'", expectedResultLen, len(keys)) } expectedKeys := []private_keys.Key{key58, key62, key69} for i, expectedKey := range expectedKeys { if i > len(keys)-1 { - t.Errorf("expected key id '%d' at index '%d' but result was too short", expectedKey.ID, i) + t.Errorf("expected id '%d' at index '%d' but result was too short", expectedKey.ID, i) continue } diff --git a/pkg/storage/keys_post.go b/pkg/storage/keys_post.go index 117c8c4f..3e2a609e 100644 --- a/pkg/storage/keys_post.go +++ b/pkg/storage/keys_post.go @@ -27,8 +27,8 @@ func (store *Storage) PostNewKey(payload private_keys.NewPayload) (private_keys. payload.ApiKey, payload.ApiKeyDisabled, payload.ApiKeyViaUrl, - payload.CreatedAt, - payload.UpdatedAt, + payload.CreatedAt.Unix(), + payload.UpdatedAt.Unix(), ).Scan(&id) if err != nil { diff --git a/pkg/storage/keys_post_test.go b/pkg/storage/keys_post_test.go index 021adae2..c000d9e5 100644 --- a/pkg/storage/keys_post_test.go +++ b/pkg/storage/keys_post_test.go @@ -3,9 +3,8 @@ package storage_test import ( "certwarden-backend/pkg/domain/private_keys" "certwarden-backend/pkg/domain/private_keys/key_crypto" - "certwarden-backend/pkg/test_helpers" + "certwarden-backend/pkg/helpers_test" "database/sql" - "errors" "fmt" "testing" "time" @@ -27,8 +26,8 @@ func TestPostNewKey(t *testing.T) { ApiKey: "apikeyxyz", ApiKeyDisabled: new(true), ApiKeyViaUrl: true, - CreatedAt: 1780336479, - UpdatedAt: 1780337000, + CreatedAt: time.Unix(1780336479, 0), + UpdatedAt: time.Unix(1780337000, 0), }, nil, private_keys.Key{ @@ -56,10 +55,10 @@ func TestPostNewKey(t *testing.T) { ApiKey: "irrelevant", ApiKeyDisabled: new(false), ApiKeyViaUrl: false, - CreatedAt: 1780336477, - UpdatedAt: 1780337010, + CreatedAt: time.Unix(1780336477, 0), + UpdatedAt: time.Unix(1780337010, 0), }, - test_helpers.ErrAnyType, + helpers_test.NewTestErrorStringComp("UNIQUE constraint failed"), private_keys.Key{}, sql.ErrNoRows, }, @@ -71,10 +70,10 @@ func TestPostNewKey(t *testing.T) { ApiKey: "irrelevant2", ApiKeyDisabled: new(false), ApiKeyViaUrl: false, - CreatedAt: 1780336480, - UpdatedAt: 1780337001, + CreatedAt: time.Unix(1780336480, 0), + UpdatedAt: time.Unix(1780337001, 0), }, - test_helpers.ErrAnyType, + helpers_test.NewTestErrorStringComp("NOT NULL constraint failed"), private_keys.Key{}, sql.ErrNoRows, }, @@ -87,17 +86,17 @@ func TestPostNewKey(t *testing.T) { } for _, tc := range testCases { - t.Run(fmt.Sprintf("post name: %s", test_helpers.StringPointerToVal(tc.newKeyPayload.Name)), func(t *testing.T) { + t.Run(fmt.Sprintf("post name: %s", helpers_test.StringPointerToVal(tc.newKeyPayload.Name)), func(t *testing.T) { key, err := storage.PostNewKey(tc.newKeyPayload) - if !test_helpers.ErrorsIs(err, tc.expectedPostErr) { - t.Errorf("expected post error '%s' but got '%s'", test_helpers.ErrorToVal(tc.expectedPostErr), test_helpers.ErrorToVal(err)) + if !helpers_test.ErrorsIs(err, tc.expectedPostErr) { + t.Errorf("expected post error '%s' but got '%s'", helpers_test.ErrorToVal(tc.expectedPostErr), helpers_test.ErrorToVal(err)) } CompareKey(t, key, tc.expectedNewKey) key, err = storage.GetOneKeyByName(key.Name) - if !errors.Is(err, tc.expectedGetErr) { - t.Errorf("expected get error '%s' but got '%s'", test_helpers.ErrorToVal(tc.expectedGetErr), test_helpers.ErrorToVal(err)) + if !helpers_test.ErrorsIs(err, tc.expectedGetErr) { + t.Errorf("expected get error '%s' but got '%s'", helpers_test.ErrorToVal(tc.expectedGetErr), helpers_test.ErrorToVal(err)) } CompareKey(t, key, tc.expectedNewKey) diff --git a/pkg/storage/keys_put.go b/pkg/storage/keys_put.go index 1321c9ac..aaa551fa 100644 --- a/pkg/storage/keys_put.go +++ b/pkg/storage/keys_put.go @@ -5,6 +5,7 @@ import ( "context" "errors" "fmt" + "time" ) // PutKeyUpdate updates an existing key in the db using any non-null @@ -36,10 +37,9 @@ func (store *Storage) PutKeyUpdate(payload private_keys.UpdatePayload) (private_ payload.ApiKeyNew, payload.ApiKeyDisabled, payload.ApiKeyViaUrl, - payload.UpdatedAt, + payload.UpdatedAt.Unix(), payload.ID, ) - if err != nil { return private_keys.Key{}, err } @@ -63,25 +63,25 @@ func (store *Storage) PutKeyUpdate(payload private_keys.UpdatePayload) (private_ } // PutKeyApiKey sets a key's api key and updates the updated at time -func (store *Storage) PutKeyApiKey(keyId int, apiKey string, updateTimeUnix int) (err error) { +func (store *Storage) PutKeyApiKey(keyId int, apiKey string, updatedAt time.Time) (err error) { // leverage main Put function payload := private_keys.UpdatePayload{ ID: keyId, ApiKey: &apiKey, - UpdatedAt: updateTimeUnix, + UpdatedAt: updatedAt, } _, err = store.PutKeyUpdate(payload) return err } -// PutKeyUpdate sets a key's new api key and updates the updated at time -func (store *Storage) PutKeyNewApiKey(keyId int, newApiKey string, updateTimeUnix int) (err error) { +// PutKeyApiKeyNew sets a key's new api key and updates the updated at time +func (store *Storage) PutKeyApiKeyNew(keyId int, apiKeyNew string, updatedAt time.Time) (err error) { // leverage main Put function payload := private_keys.UpdatePayload{ ID: keyId, - ApiKeyNew: &newApiKey, - UpdatedAt: updateTimeUnix, + ApiKeyNew: &apiKeyNew, + UpdatedAt: updatedAt, } _, err = store.PutKeyUpdate(payload) @@ -89,7 +89,7 @@ func (store *Storage) PutKeyNewApiKey(keyId int, newApiKey string, updateTimeUni } // PutKeyLastAccess sets a key's last access time -func (store *Storage) PutKeyLastAccess(keyId int, lastAccessTimeUnix int64) (err error) { +func (store *Storage) PutKeyLastAccess(keyId int, lastAccess time.Time) (err error) { // database action ctx, cancel := context.WithTimeout(store.shutdownContext, store.timeout) defer cancel() @@ -104,7 +104,7 @@ func (store *Storage) PutKeyLastAccess(keyId int, lastAccessTimeUnix int64) (err ` res, err := store.db.ExecContext(ctx, query, - lastAccessTimeUnix, + lastAccess.Unix(), keyId, ) if err != nil { diff --git a/pkg/storage/keys_put_test.go b/pkg/storage/keys_put_test.go index cb5d40d2..51773dfc 100644 --- a/pkg/storage/keys_put_test.go +++ b/pkg/storage/keys_put_test.go @@ -3,10 +3,9 @@ package storage_test import ( "certwarden-backend/pkg/domain/private_keys" "certwarden-backend/pkg/domain/private_keys/key_crypto" + "certwarden-backend/pkg/helpers_test" "certwarden-backend/pkg/storage" - "certwarden-backend/pkg/test_helpers" "database/sql" - "errors" "fmt" "testing" "time" @@ -43,51 +42,57 @@ func TestPutKeyUpdate(t *testing.T) { }, { // update all things private_keys.UpdatePayload{ - ID: 31, - UpdatedAt: 100555, + ID: 31, + Name: new("newNameHere21"), + Description: new("a new desc"), + ApiKey: new("122222"), + ApiKeyNew: new("2222"), + ApiKeyDisabled: new(true), + ApiKeyViaUrl: new(false), + UpdatedAt: time.Unix(1001111, 0), }, private_keys.Key{ ID: 31, - Name: "certwarden", - Description: "localhost / dev work w/ real cert", + Name: "newNameHere21", + Description: "a new desc", Algorithm: key_crypto.AlgorithmECDSAp256, Pem: `-----BEGIN EC PRIVATE KEY----- red-31 -----END EC PRIVATE KEY----- `, - ApiKey: "key-api-key-31", - ApiKeyNew: "key-api-new-key-31", - ApiKeyDisabled: false, - ApiKeyViaUrl: true, + ApiKey: "122222", + ApiKeyNew: "2222", + ApiKeyDisabled: true, + ApiKeyViaUrl: false, LastAccess: time.Unix(1745952074, 0), CreatedAt: time.Unix(1709327549, 0), - UpdatedAt: time.Unix(100555, 0), + UpdatedAt: time.Unix(1001111, 0), }, nil, 31, private_keys.Key{ ID: 31, - Name: "certwarden", - Description: "localhost / dev work w/ real cert", + Name: "newNameHere21", + Description: "a new desc", Algorithm: key_crypto.AlgorithmECDSAp256, Pem: `-----BEGIN EC PRIVATE KEY----- red-31 -----END EC PRIVATE KEY----- `, - ApiKey: "key-api-key-31", - ApiKeyNew: "key-api-new-key-31", - ApiKeyDisabled: false, - ApiKeyViaUrl: true, + ApiKey: "122222", + ApiKeyNew: "2222", + ApiKeyDisabled: true, + ApiKeyViaUrl: false, LastAccess: time.Unix(1745952074, 0), CreatedAt: time.Unix(1709327549, 0), - UpdatedAt: time.Unix(100555, 0), + UpdatedAt: time.Unix(1001111, 0), }, nil, }, { // update none of the things (except last update) private_keys.UpdatePayload{ ID: 62, - UpdatedAt: 1001111, + UpdatedAt: time.Unix(1001111, 0), }, private_keys.Key{ ID: 62, @@ -131,7 +136,7 @@ red-62 private_keys.UpdatePayload{ ID: 58, ApiKeyDisabled: new(false), - UpdatedAt: 1751730000, + UpdatedAt: time.Unix(1751730000, 0), }, private_keys.Key{ ID: 58, @@ -182,15 +187,15 @@ red-58 for i, tc := range testCases { t.Run(fmt.Sprintf("#%d (id: %d)", i, tc.payload.ID), func(t *testing.T) { key, err := storage.PutKeyUpdate(tc.payload) - if !errors.Is(err, tc.expectedPutErr) { - t.Errorf("expected put error '%s' but got '%s'", test_helpers.ErrorToVal(tc.expectedPutErr), test_helpers.ErrorToVal(err)) + if !helpers_test.ErrorsIs(err, tc.expectedPutErr) { + t.Errorf("expected put error '%s' but got '%s'", helpers_test.ErrorToVal(tc.expectedPutErr), helpers_test.ErrorToVal(err)) } CompareKey(t, key, tc.expectedPutResult) key, err = storage.GetOneKeyById(tc.getId) - if !errors.Is(err, tc.expectedGetErr) { - t.Errorf("expected get error '%s' but got '%s'", test_helpers.ErrorToVal(tc.expectedGetErr), test_helpers.ErrorToVal(err)) + if !helpers_test.ErrorsIs(err, tc.expectedGetErr) { + t.Errorf("expected get error '%s' but got '%s'", helpers_test.ErrorToVal(tc.expectedGetErr), helpers_test.ErrorToVal(err)) } CompareKey(t, key, tc.expectedGetResult) @@ -200,9 +205,9 @@ red-58 func TestPutKeyApiKey(t *testing.T) { testCases := []struct { - keyId int - apiKey string - updateTimeUnix int + keyId int + apiKey string + updatedAt time.Time expectedKey private_keys.Key expectedPutErr error @@ -211,7 +216,7 @@ func TestPutKeyApiKey(t *testing.T) { { // invalid key id -1, "fake", - 100005, + time.Unix(100005, 0), private_keys.Key{}, storage.ErrWrongUpdateRowCount, sql.ErrNoRows, @@ -219,7 +224,7 @@ func TestPutKeyApiKey(t *testing.T) { { // invalid key id 500, "anotherfake", - 10005000, + time.Unix(10005000, 0), private_keys.Key{}, storage.ErrWrongUpdateRowCount, sql.ErrNoRows, @@ -228,7 +233,7 @@ func TestPutKeyApiKey(t *testing.T) { { 31, "fake31", - 1022005, + time.Unix(1022005, 0), private_keys.Key{ ID: 31, Name: "certwarden", @@ -251,8 +256,8 @@ red-31 }, { 62, - "62thing", - 0, + "", + time.Unix(0, 0), private_keys.Key{ ID: 62, Name: "SomeKEy", @@ -262,7 +267,7 @@ red-31 red-62 -----END EC PRIVATE KEY----- `, - ApiKey: "62thing", + ApiKey: "", ApiKeyNew: "key-api-new-key-62", ApiKeyDisabled: false, ApiKeyViaUrl: false, @@ -283,14 +288,14 @@ red-62 for _, tc := range testCases { t.Run(fmt.Sprintf("id: %d)", tc.keyId), func(t *testing.T) { - err := storage.PutKeyApiKey(tc.keyId, tc.apiKey, tc.updateTimeUnix) - if !errors.Is(err, tc.expectedPutErr) { - t.Errorf("expected put error '%s' but got '%s'", test_helpers.ErrorToVal(tc.expectedPutErr), test_helpers.ErrorToVal(err)) + err := storage.PutKeyApiKey(tc.keyId, tc.apiKey, tc.updatedAt) + if !helpers_test.ErrorsIs(err, tc.expectedPutErr) { + t.Errorf("expected put error '%s' but got '%s'", helpers_test.ErrorToVal(tc.expectedPutErr), helpers_test.ErrorToVal(err)) } key, err := storage.GetOneKeyById(tc.keyId) - if !errors.Is(err, tc.expectedGetErr) { - t.Errorf("expected get error '%s' but got '%s'", test_helpers.ErrorToVal(tc.expectedGetErr), test_helpers.ErrorToVal(err)) + if !helpers_test.ErrorsIs(err, tc.expectedGetErr) { + t.Errorf("expected get error '%s' but got '%s'", helpers_test.ErrorToVal(tc.expectedGetErr), helpers_test.ErrorToVal(err)) } CompareKey(t, key, tc.expectedKey) @@ -298,11 +303,11 @@ red-62 } } -func TestPutKeyNewApiKey(t *testing.T) { +func TestPutKeyApiKeyNew(t *testing.T) { testCases := []struct { - keyId int - apiKeyNew string - updateTimeUnix int + keyId int + apiKeyNew string + updatedAt time.Time expectedKey private_keys.Key expectedPutErr error @@ -311,7 +316,7 @@ func TestPutKeyNewApiKey(t *testing.T) { { // invalid key id -1, "", - 1099905, + time.Unix(1099905, 0), private_keys.Key{}, storage.ErrWrongUpdateRowCount, sql.ErrNoRows, @@ -319,7 +324,7 @@ func TestPutKeyNewApiKey(t *testing.T) { { // invalid key id 500, "anotherfake", - 10005000, + time.Unix(10005000, 0), private_keys.Key{}, storage.ErrWrongUpdateRowCount, sql.ErrNoRows, @@ -328,7 +333,7 @@ func TestPutKeyNewApiKey(t *testing.T) { { 69, "fakenew69", - 1022005, + time.Unix(1022005, 0), private_keys.Key{ ID: 69, Name: "STAGING_persist--test007.test.example2.com", @@ -352,7 +357,7 @@ red-69 { 67, "otherfakenew67", - 0, + time.Unix(0, 0), private_keys.Key{ ID: 67, Name: "_GC3", @@ -376,21 +381,21 @@ red-67 } // create testing service - storage, err := openStorageWithTestData(t, "putkeynewapikey") + storage, err := openStorageWithTestData(t, "putkeyapikeynew") if err != nil { t.Fatal(err) } for _, tc := range testCases { t.Run(fmt.Sprintf("id: %d)", tc.keyId), func(t *testing.T) { - err := storage.PutKeyNewApiKey(tc.keyId, tc.apiKeyNew, tc.updateTimeUnix) - if !errors.Is(err, tc.expectedPutErr) { - t.Errorf("expected put error '%s' but got '%s'", test_helpers.ErrorToVal(tc.expectedPutErr), test_helpers.ErrorToVal(err)) + err := storage.PutKeyApiKeyNew(tc.keyId, tc.apiKeyNew, tc.updatedAt) + if !helpers_test.ErrorsIs(err, tc.expectedPutErr) { + t.Errorf("expected put error '%s' but got '%s'", helpers_test.ErrorToVal(tc.expectedPutErr), helpers_test.ErrorToVal(err)) } key, err := storage.GetOneKeyById(tc.keyId) - if !errors.Is(err, tc.expectedGetErr) { - t.Errorf("expected get error '%s' but got '%s'", test_helpers.ErrorToVal(tc.expectedGetErr), test_helpers.ErrorToVal(err)) + if !helpers_test.ErrorsIs(err, tc.expectedGetErr) { + t.Errorf("expected get error '%s' but got '%s'", helpers_test.ErrorToVal(tc.expectedGetErr), helpers_test.ErrorToVal(err)) } CompareKey(t, key, tc.expectedKey) @@ -400,8 +405,8 @@ red-67 func TestPutKeyLastAccess(t *testing.T) { testCases := []struct { - keyId int - lastAccessTimeUnix int64 + keyId int + lastAccess time.Time expectedKey private_keys.Key expectedPutErr error @@ -409,14 +414,14 @@ func TestPutKeyLastAccess(t *testing.T) { }{ { // invalid key id -1, - 88888888, + time.Unix(88888888, 0), private_keys.Key{}, storage.ErrWrongUpdateRowCount, sql.ErrNoRows, }, { // invalid key id 500, - 88888888, + time.Unix(88888888, 0), private_keys.Key{}, storage.ErrWrongUpdateRowCount, sql.ErrNoRows, @@ -424,7 +429,7 @@ func TestPutKeyLastAccess(t *testing.T) { // do some updates { 64, - 1022885, + time.Unix(1022885, 0), private_keys.Key{ ID: 64, Name: "_Another_Test_Acct_LE_Staging_Roll", @@ -447,7 +452,7 @@ red-64 }, { 63, - 9999999, + time.Unix(9999999, 0), private_keys.Key{ ID: 63, Name: "_Another_Test_Acct_LE_Staging", @@ -470,7 +475,7 @@ red-63 }, { 62, - 0, + time.Unix(0, 0), private_keys.Key{ ID: 62, Name: "SomeKEy", @@ -501,14 +506,14 @@ red-62 for _, tc := range testCases { t.Run(fmt.Sprintf("id: %d)", tc.keyId), func(t *testing.T) { - err := storage.PutKeyLastAccess(tc.keyId, tc.lastAccessTimeUnix) - if !errors.Is(err, tc.expectedPutErr) { - t.Errorf("expected put error '%s' but got '%s'", test_helpers.ErrorToVal(tc.expectedPutErr), test_helpers.ErrorToVal(err)) + err := storage.PutKeyLastAccess(tc.keyId, tc.lastAccess) + if !helpers_test.ErrorsIs(err, tc.expectedPutErr) { + t.Errorf("expected put error '%s' but got '%s'", helpers_test.ErrorToVal(tc.expectedPutErr), helpers_test.ErrorToVal(err)) } key, err := storage.GetOneKeyById(tc.keyId) - if !errors.Is(err, tc.expectedGetErr) { - t.Errorf("expected get error '%s' but got '%s'", test_helpers.ErrorToVal(tc.expectedGetErr), test_helpers.ErrorToVal(err)) + if !helpers_test.ErrorsIs(err, tc.expectedGetErr) { + t.Errorf("expected get error '%s' but got '%s'", helpers_test.ErrorToVal(tc.expectedGetErr), helpers_test.ErrorToVal(err)) } CompareKey(t, key, tc.expectedKey) diff --git a/pkg/storage/keys_test.go b/pkg/storage/keys_test.go index a0bdaa4b..4ad33049 100644 --- a/pkg/storage/keys_test.go +++ b/pkg/storage/keys_test.go @@ -48,10 +48,10 @@ func CompareKey(t *testing.T, key, expectedKey private_keys.Key) { } if !key.CreatedAt.Equal(expectedKey.CreatedAt) { - t.Errorf("key: last access expected '%s' but got '%s'", expectedKey.CreatedAt.UTC(), key.CreatedAt.UTC()) + t.Errorf("key: created at expected '%s' but got '%s'", expectedKey.CreatedAt.UTC(), key.CreatedAt.UTC()) } if !key.UpdatedAt.Equal(expectedKey.UpdatedAt) { - t.Errorf("key: last access expected '%s' but got '%s'", expectedKey.UpdatedAt.UTC(), key.UpdatedAt.UTC()) + t.Errorf("key: updated at expected '%s' but got '%s'", expectedKey.UpdatedAt.UTC(), key.UpdatedAt.UTC()) } } diff --git a/pkg/storage/service_mock_test.go b/pkg/storage/service_mock_test.go index 3035f9f2..724298ff 100644 --- a/pkg/storage/service_mock_test.go +++ b/pkg/storage/service_mock_test.go @@ -1,6 +1,7 @@ package storage_test import ( + "certwarden-backend/pkg/helpers_test" "certwarden-backend/pkg/pagination_sort" "certwarden-backend/pkg/storage" "context" @@ -61,7 +62,7 @@ func openStorageWithTestData(t *testing.T, testName string) (_ *storage.Storage, _, err := os.Stat(thisTestFolder) if err == nil { os.RemoveAll(thisTestFolder) - } else if !errors.Is(err, os.ErrNotExist) { + } else if !helpers_test.ErrorsIs(err, os.ErrNotExist) { return nil, err } @@ -108,8 +109,8 @@ func openStorageWithTestData(t *testing.T, testName string) (_ *storage.Storage, return storage, nil } -// QueryBuilderForTest generates a Query for use in tests -func QueryBuilderForTest(limit int, offset int, sortField string, sortAsc bool) pagination_sort.Query { +// queryBuilderForTest generates a Query for use in tests +func queryBuilderForTest(limit int, offset int, sortField string, sortAsc bool) pagination_sort.Query { sortDirText := "desc" if sortAsc { sortDirText = "asc" diff --git a/pkg/storage/time.go b/pkg/storage/time_DELETE_ME.go similarity index 81% rename from pkg/storage/time.go rename to pkg/storage/time_DELETE_ME.go index babffac5..93863093 100644 --- a/pkg/storage/time.go +++ b/pkg/storage/time_DELETE_ME.go @@ -1,10 +1,9 @@ package storage -import ( - "time" -) +import "time" // timeNow() returns unix time as a NullInt32 +// TODO: Remove func timeNow() (unixTime int) { return int(time.Now().Unix()) } diff --git a/pkg/storage/types.go b/pkg/storage/types.go index 9b5dc5ce..85288be0 100644 --- a/pkg/storage/types.go +++ b/pkg/storage/types.go @@ -52,35 +52,24 @@ func makeJsonStringSlice(stringSlice []string, nullOk bool) *jsonStringSlice { // jsonCertExtensionSlice is a json formatted string that is a slice of CertExtension type jsonCertExtensionSlice string -// transform JCES into a slice of proper CertExtension +// transform JCES into as slice of proper CertExtension func (jces jsonCertExtensionSlice) toCertExtensionSlice() ([]certificates.CertExtension, error) { if jces == "" { return []certificates.CertExtension{}, nil } // unmarshal the json to the json object - extSlice := []certificates.CertExtensionJSON{} - err := json.Unmarshal([]byte(jces), &extSlice) + cextSlice := []certificates.CertExtension{} + err := json.Unmarshal([]byte(jces), &cextSlice) if err != nil { return nil, err } - // convert json objs to real objs - certExtSlice := []certificates.CertExtension{} - for i := range extSlice { - certExt, err := extSlice[i].ToCertExtension() - if err != nil { - // if invalid data stored, return err - return nil, err - } - certExtSlice = append(certExtSlice, certExt) - } - - return certExtSlice, nil + return cextSlice, nil } // makeJsonCertExtensionSlice creates a JCES from a slice of CertExtensionJSON -func makeJsonCertExtensionSlice(extensionSlice []certificates.CertExtensionJSON, nullOk bool) *jsonCertExtensionSlice { +func makeJsonCertExtensionSlice(extensionSlice []certificates.CertExtension, nullOk bool) *jsonCertExtensionSlice { if extensionSlice == nil { if !nullOk { empty := jsonCertExtensionSlice("[]") diff --git a/pkg/test_helpers/err_is.go b/pkg/test_helpers/err_is.go deleted file mode 100644 index 2e644972..00000000 --- a/pkg/test_helpers/err_is.go +++ /dev/null @@ -1,16 +0,0 @@ -package test_helpers - -import "errors" - -var ErrAnyType = errors.New("error of any type") - -// ErrorsIs is a custom implementation of errors.Is() to provide an error type -// that will match any error type; otherwise, it is just a wrapper for errors.Is(); -// Only needs to be used when avoiding an import of the exact error type -func ErrorsIs(err error, target error) bool { - if err != nil && errors.Is(target, ErrAnyType) { - return true - } - - return errors.Is(err, target) -} diff --git a/pkg/test_helpers/err_is_test.go b/pkg/test_helpers/err_is_test.go deleted file mode 100644 index 72599d62..00000000 --- a/pkg/test_helpers/err_is_test.go +++ /dev/null @@ -1,57 +0,0 @@ -package test_helpers_test - -import ( - "certwarden-backend/pkg/acme" - "certwarden-backend/pkg/test_helpers" - "database/sql" - "fmt" - "testing" -) - -func TestErrorsIs(t *testing.T) { - testCases := []struct { - err error - target error - expectedResult bool - }{ - { - err: nil, - target: nil, - expectedResult: true, - }, - { - err: nil, - target: test_helpers.ErrAnyType, - expectedResult: false, - }, - { - err: test_helpers.ErrAnyType, - target: nil, - expectedResult: false, - }, - { - err: sql.ErrNoRows, - target: acme.ErrChallengeMalformed, - expectedResult: false, - }, - { - err: sql.ErrNoRows, - target: test_helpers.ErrAnyType, - expectedResult: true, - }, - { - err: acme.ErrChallengeMalformed, - target: test_helpers.ErrAnyType, - expectedResult: true, - }, - } - - for i, tc := range testCases { - t.Run(fmt.Sprintf("#%d:", i), func(t *testing.T) { - res := test_helpers.ErrorsIs(tc.err, tc.target) - if res != tc.expectedResult { - t.Errorf("err '%s' with target '%s' expected '%t' but got '%t'", test_helpers.ErrorToVal(tc.err), test_helpers.ErrorToVal(tc.target), tc.expectedResult, res) - } - }) - } -} diff --git a/test_data/testdata_v11.db b/test_data/testdata_v11.db index fb303bd5..75a604ef 100644 Binary files a/test_data/testdata_v11.db and b/test_data/testdata_v11.db differ