diff --git a/v3/api/certificate.go b/v3/api/certificate.go index 95a7af8..bd01c40 100644 --- a/v3/api/certificate.go +++ b/v3/api/certificate.go @@ -113,8 +113,8 @@ func (c *Client) EnrollPFXV2(ea *EnrollPFXFctArgsV2) (*EnrollResponseV2, error) var missingFields []string // TODO: Probably a better way to express these if blocks - if ea.Template == "" { - missingFields = append(missingFields, "Template") + if ea.Template == "" && ea.EnrollmentPatternId == 0 { + missingFields = append(missingFields, "Template or EnrollmentPatternId") } if ea.CertificateAuthority == "" { missingFields = append(missingFields, "CertificateAuthority") @@ -151,7 +151,11 @@ func (c *Client) EnrollPFXV2(ea *EnrollPFXFctArgsV2) (*EnrollResponseV2, error) } ea.SubjectString = subject } else { - return nil, fmt.Errorf("subject is required to use enrollpfx(). Please configure either SubjectString or Subject") + log.Println("[DEBUG] EnrollPFXV2: Subject is nil checks if there are SANs") + if ea.SANs == nil || (len(ea.SANs.DNS) == 0 && len(ea.SANs.URI) == 0 && len(ea.SANs.IP4) == 0 && + len(ea.SANs.IP6) == 0) { + return nil, fmt.Errorf("subject or subject alternative names are required to use enrollpfx(). Please configure either SubjectString or Subject or SANs") + } } } @@ -191,13 +195,16 @@ func (c *Client) EnrollPFXV2(ea *EnrollPFXFctArgsV2) (*EnrollResponseV2, error) // Returns: // - Leaf certificate // - Certificate chain +// - Raw certificate data (as base64 string, if applicable) +// - Error func (c *Client) DownloadCertificate( certId int, thumbprint string, serialNumber string, issuerDn string, collectionId int, -) (*x509.Certificate, []*x509.Certificate, error) { + certificateFormat string, +) (*x509.Certificate, []*x509.Certificate, *string, error) { log.Println("[INFO] Downloading certificate") /* The download certificate endpoint requires one of the following to retrieve a cert: @@ -217,7 +224,7 @@ func (c *Client) DownloadCertificate( } if !validInput { - return nil, nil, fmt.Errorf("certID, thumbprint, or serial number AND issuer DN required to dowload certificate") + return nil, nil, nil, fmt.Errorf("certID, thumbprint, or serial number AND issuer DN required to dowload certificate") } payload := &downloadCertificateBody{ @@ -243,11 +250,19 @@ func (c *Client) DownloadCertificate( } // Set Keyfactor-specific headers + switch certificateFormat { + case "CER", "CRT", "DER", "PEM": + // do nothing these are valid formats + break + default: + // if not specified or invalid format then default to P7B + certificateFormat = "P7B" + } headers := &apiHeaders{ Headers: []StringTuple{ {"x-keyfactor-api-version", "1"}, {"x-keyfactor-requested-with", "APIClient"}, - {"x-certificateformat", "P7B"}, + {"x-certificateformat", certificateFormat}, }, } @@ -261,13 +276,13 @@ func (c *Client) DownloadCertificate( resp, err := c.sendRequest(keyfactorAPIStruct) if err != nil { - return nil, nil, err + return nil, nil, nil, err } jsonResp := &downloadCertificateResponse{} err = json.NewDecoder(resp.Body).Decode(&jsonResp) if err != nil { - return nil, nil, err + return nil, nil, nil, err } //buf, err := base64.StdEncoding.DecodeString(jsonResp.Content) //if err != nil { @@ -281,17 +296,17 @@ func (c *Client) DownloadCertificate( certs, p7bErr := ConvertBase64P7BtoCertificates(jsonResp.Content) if p7bErr != nil { - return nil, nil, p7bErr + return nil, nil, &jsonResp.Content, p7bErr } var leaf *x509.Certificate if len(certs) > 1 { //leaf is last cert in chain leaf = certs[0] // First cert in chain is the leaf - return leaf, certs, nil + return leaf, certs, &jsonResp.Content, nil } - return certs[0], nil, nil + return certs[0], nil, &jsonResp.Content, nil } // EnrollCSR takes arguments for EnrollCSRFctArgs to enroll a passed Certificate Signing @@ -659,7 +674,8 @@ func (c *Client) RecoverCertificate( issuerDn string, password string, collectionId int, -) (interface{}, *x509.Certificate, []*x509.Certificate, error) { + certificateFormat string, +) (interface{}, *x509.Certificate, []*x509.Certificate, *string, error) { log.Println("[DEBUG] Enter RecoverCertificate") log.Println("[INFO] Recovering certificate ID:", certId) /* The download certificate endpoint requires one of the following to retrieve a cert: @@ -669,6 +685,9 @@ func (c *Client) RecoverCertificate( Check for this input */ + if certificateFormat == "" { + certificateFormat = "PFX" + } validInput := false if certId != 0 { validInput = true @@ -680,12 +699,12 @@ func (c *Client) RecoverCertificate( if !validInput { log.Println("[ERROR] RecoverCertificate: certID, thumbprint, or serial number AND issuer DN required to download certificate") - return nil, nil, nil, fmt.Errorf("certID, thumbprint, or serial number AND issuer DN required to download certificate") + return nil, nil, nil, nil, fmt.Errorf("certID, thumbprint, or serial number AND issuer DN required to download certificate") } log.Println("[DEBUG] RecoverCertificate: Valid input") if password == "" { - return nil, nil, nil, fmt.Errorf("password required to recover private key with certificate") + return nil, nil, nil, nil, fmt.Errorf("password required to recover private key with certificate") } rca := &recoverCertArgs{ @@ -703,7 +722,7 @@ func (c *Client) RecoverCertificate( Headers: []StringTuple{ {"x-keyfactor-api-version", "1"}, {"x-keyfactor-requested-with", "APIClient"}, - {"x-certificateformat", "PFX"}, + {"x-certificateformat", certificateFormat}, }, } @@ -734,7 +753,7 @@ func (c *Client) RecoverCertificate( resp, err := c.sendRequest(keyfactorAPIStruct) if err != nil { log.Println("[ERROR] RecoverCertificate: Error recovering certificate from Keyfactor Command", err.Error()) - return nil, nil, nil, err + return nil, nil, nil, nil, err } jsonResp := &recoverCertResponse{} @@ -742,26 +761,144 @@ func (c *Client) RecoverCertificate( err = json.NewDecoder(resp.Body).Decode(&jsonResp) if err != nil { log.Println("[ERROR] RecoverCertificate: Error decoding response from Keyfactor Command", err.Error()) - return nil, nil, nil, err + return nil, nil, nil, nil, err } - log.Println("[DEBUG] RecoverCertificate: Decoding PFX") - pfxDer, err := base64.StdEncoding.DecodeString(jsonResp.PFX) - if err != nil { - log.Println("[ERROR] RecoverCertificate: Error decoding PFX", err.Error()) - return nil, nil, nil, err + switch certificateFormat { + case "PFX", "pfx", "pkcs12", "p12", "jks", "JKS": + log.Println("[DEBUG] RecoverCertificate: decoding `PFX` response field") + pfxDer := jsonResp.PFX + if pfxDer == "" { + log.Println("[ERROR] RecoverCertificate: Error decoding PFX", err.Error()) + return nil, nil, nil, &pfxDer, fmt.Errorf("pfx field in response is empty") + } + log.Println("[INFO] Recovered certificate successfully") + log.Println("[DEBUG] RecoverCertificate returning in PFX format") + return nil, nil, nil, &pfxDer, nil + case "PEM", "pem": + log.Println("[DEBUG] RecoverCertificate: Decoding PFX") + pfxDer, dErr := base64.StdEncoding.DecodeString(jsonResp.PFX) + if dErr != nil { + log.Println("[ERROR] RecoverCertificate: Error decoding PFX", dErr.Error()) + return nil, nil, nil, &jsonResp.PFX, dErr + } + + log.Println("[DEBUG] RecoverCertificate: Decoding PFX chain") + priv, leaf, chain, pErr := pkcs12.DecodeChain(pfxDer, rca.Password) + if pErr != nil { + log.Println("[ERROR] RecoverCertificate: Error decoding PFX chain", pErr.Error()) + return nil, nil, nil, &jsonResp.PFX, pErr + } + + log.Println("[INFO] Recovered certificate successfully") + log.Println("[DEBUG] RecoverCertificate: ", leaf, chain) + return priv, leaf, chain, &jsonResp.PFX, nil + default: + log.Println("[DEBUG] RecoverCertificate: Decoding PFX") + pfxDer, dErr := base64.StdEncoding.DecodeString(jsonResp.PFX) + if dErr != nil { + log.Println("[ERROR] RecoverCertificate: Error decoding PFX", dErr.Error()) + return nil, nil, nil, &jsonResp.PFX, dErr + } + + log.Println("[DEBUG] RecoverCertificate: Decoding PFX chain") + priv, leaf, chain, pErr := pkcs12.DecodeChain(pfxDer, rca.Password) + if pErr != nil { + log.Println("[ERROR] RecoverCertificate: Error decoding PFX chain", pErr.Error()) + return nil, nil, nil, &jsonResp.PFX, pErr + } + + log.Println("[INFO] Recovered certificate successfully") + log.Println("[DEBUG] RecoverCertificate returning in PEM format") + + var pemCerts []string + + // Encode leaf certificate to PEM + pemLeaf := pem.EncodeToMemory( + &pem.Block{ + Type: "CERTIFICATE", + Bytes: leaf.Raw, + }, + ) + pemCerts = append(pemCerts, string(pemLeaf)) + + // Encode chain certificates to PEM + for _, cert := range chain { + pemCert := pem.EncodeToMemory( + &pem.Block{ + Type: "CERTIFICATE", + Bytes: cert.Raw, + }, + ) + pemCerts = append(pemCerts, string(pemCert)) + } + + pemData := strings.Join(pemCerts, "\n") + return priv, leaf, chain, &pemData, nil + } + +} + +// ChangeCertificateOwnerRole changes the certificate's owner. Users must be in the current owner's role and the new owner's role. +// If removing the owner, leave both NewRoleId and NewRoleName empty in the request. +// Calls PUT /Certificates/{id}/Owner endpoint. +func (c *Client) ChangeCertificateOwnerRole( + certificateId int, + req *OwnerRequest, + params ...*CertificateOwnerChangeParams, +) error { + log.Printf("[INFO] Changing owner of certificate with ID %d in Keyfactor", certificateId) + + // Validate certificate ID + if certificateId <= 0 { + return errors.New("certificate ID must be a positive integer") + } + + // Set Keyfactor-specific headers + headers := &apiHeaders{ + Headers: []StringTuple{ + {"x-keyfactor-api-version", "1"}, + {"x-keyfactor-requested-with", "APIClient"}, + {"Content-Type", "application/json"}, + }, + } + + // Build URL with query parameters + endpoint := fmt.Sprintf("Certificates/%d/Owner", certificateId) + var queryParams []string + + if len(params) > 0 && params[0] != nil { + param := params[0] + if param.CollectionId != nil { + queryParams = append(queryParams, fmt.Sprintf("collectionId=%d", *param.CollectionId)) + } + if param.ContainerId != nil { + queryParams = append(queryParams, fmt.Sprintf("containerId=%d", *param.ContainerId)) + } + } + + if len(queryParams) > 0 { + endpoint += "?" + strings.Join(queryParams, "&") } - log.Println("[DEBUG] RecoverCertificate: Decoding PFX chain") - priv, leaf, chain, err := pkcs12.DecodeChain(pfxDer, rca.Password) + keyfactorAPIStruct := &request{ + Method: "PUT", + Endpoint: endpoint, + Headers: headers, + Payload: req, + } + + resp, err := c.sendRequest(keyfactorAPIStruct) if err != nil { - log.Println("[ERROR] RecoverCertificate: Error decoding PFX chain", err.Error()) - return nil, nil, nil, err + return err } - log.Println("[INFO] Recovered certificate successfully") - log.Println("[DEBUG] RecoverCertificate: ", leaf, chain) - return priv, leaf, chain, nil + // Check if the response indicates success (204 No Content expected) + if resp.StatusCode != http.StatusNoContent { + return fmt.Errorf("failed to change certificate owner: HTTP %d", resp.StatusCode) + } + + return nil } // createSubject builds the certificate subject string from a passed CertificateSubject argument. diff --git a/v3/api/certificate_models.go b/v3/api/certificate_models.go index 98604f6..945ab65 100644 --- a/v3/api/certificate_models.go +++ b/v3/api/certificate_models.go @@ -47,39 +47,54 @@ type EnrollPFXFctArgs struct { type EnrollPFXFctArgsV2 struct { Stores []CertificateStore `json:"Stores,omitempty"` CustomFriendlyName string `json:"CustomFriendlyName,omitempty"` - Password string `json:"Password"` + Password string `json:"Password,omitempty"` PopulateMissingValuesFromAD bool `json:"PopulateMissingValuesFromAD"` // Configure the SubjectString field as the full string subject for the certificate. For example, if you don't have // subject fields individually separated, and the subject is already in the format required by RFC5280, use the SubjectString field. - SubjectString string `json:"Subject"` // If the certificate subject is not already in the format required by RFC5280, configure the subject fields using a CertificateSubject // struct, and EnrollPFX will automatically compile this information into a proper subject. - Subject *CertificateSubject `json:"-"` - IncludeChain bool `json:"IncludeChain"` - RenewalCertificateId int `json:"RenewalCertificateId,omitempty"` - CertificateAuthority string `json:"CertificateAuthority"` - Timestamp string `json:"Timestamp"` - Template string `json:"Template"` - SANs *SANs `json:"SANs,omitempty"` - Metadata map[string]interface{} `json:"Metadata,omitempty"` - CertFormat string `json:"-"` - InstallIntoExistingCertificateStores bool `json:"InstallIntoExistingCertificateStores,omitempty"` - ChainOrder string `json:"ChainOrder,omitempty"` - KeyType string `json:"KeyType,omitempty"` - KeyLength int `json:"KeyLength,omitempty"` + Subject *CertificateSubject `json:"-"` + SubjectString string `json:"Subject,omitempty"` + IncludeChain bool `json:"IncludeChain"` + IncludeSubjectHeader bool `json:"IncludeSubjectHeader,omitempty"` + RenewalCertificateId int `json:"RenewalCertificateId,omitempty"` + CertificateAuthority string `json:"CertificateAuthority"` + Timestamp string `json:"Timestamp"` + Template string `json:"Template"` + SANs *SANs `json:"SANs,omitempty"` + Metadata map[string]interface{} `json:"Metadata,omitempty"` + AdditionalEnrollmentFields *map[string]interface{} `json:"AdditionalEnrollmentFields,omitempty"` + CertFormat string `json:"-"` // Needs to be passed as header X-Certificate-Format + InstallIntoExistingCertificateStores bool `json:"InstallIntoExistingCertificateStores,omitempty"` + ChainOrder string `json:"ChainOrder,omitempty"` + AlternativeKeyType string `json:"AlternativeKeyType,omitempty"` // Requires Command 25.0.0+ + KeyType string `json:"KeyType,omitempty"` + AlternativeKeyLength int `json:"AlternativeKeyLength,omitempty"` // Requires Command 25.0.0+ + KeyLength int `json:"KeyLength,omitempty"` + Curve string `json:"Curve,omitempty"` + EnrollmentPatternId int `json:"EnrollmentPatternId,omitempty"` // Requires Command 25.1.0+ + OwnerRoleId int `json:"OwnerRoleId,omitempty"` // Requires Command 12.3.0+ + OwnerRoleName string `json:"OwnerRoleName,omitempty"` // Requires Command 12.3.0+ } // EnrollCSRFctArgs holds the function arguments used for calling the EnrollCSR method. type EnrollCSRFctArgs struct { - CSR string - Timestamp string `json:"Timestamp"` - Template string `json:"Template"` - CertFormat string `json:"-"` - CertificateAuthority string `json:"CertificateAuthority"` - IncludeChain bool `json:"IncludeChain"` - SANs *SANs `json:"SANs"` - Metadata map[string]interface{} `json:"Metadata"` + CSR string `json:"CSR"` //required + PrivateKey string `json:"PrivateKey,omitempty"` + RenewalCertificateId int `json:"RenewalCertificateId,omitempty"` + CertificateAuthority string `json:"CertificateAuthority,omitempty"` + IncludeChain bool `json:"IncludeChain"` + IncludeSubjectHeader bool `json:"IncludeSubjectHeader,omitempty"` + Timestamp string `json:"Timestamp"` + Template string `json:"Template,omitempty"` + EnrollmentPatternId int `json:"EnrollmentPatternId,omitempty"` // Requires Command 25.1.0+ + CertFormat string `json:"-"` + SANs *SANs `json:"SANs,omitempty"` + Metadata map[string]interface{} `json:"Metadata,omitempty"` + AdditionalEnrollmentFields map[string]interface{} `json:"AdditionalEnrollmentFields,omitempty"` + OwnerRoleId int `json:"OwnerRoleId,omitempty"` // Requires Command 12.3.0+ + OwnerRoleName string `json:"OwnerRoleName,omitempty"` // Requires Command 12.3.0+ } // RevokeCertArgs holds the function arguments used for calling the RevokeCert method. @@ -213,16 +228,25 @@ type GetCertificateResponse struct { NotAfter string `json:"NotAfter"` IssuerDN string `json:"IssuerDN"` PrincipalId string `json:"PrincipalId"` + OwnerRoleId int `json:"OwnerRoleId;omitempty"` // Requires Command 12.3.0+ + OwnerRoleName string `json:"OwnerRoleName,omitempty"` // Requires Command 12.3.0+ TemplateId int `json:"TemplateId"` CertState int `json:"CertState"` KeySizeInBits int `json:"KeySizeInBits"` KeyType int `json:"KeyType"` + KeyAlgorithm string `json:"KeyAlgorithm"` + AltKeyAlgorithm string `json:"AltKeyAlgorithm,omitempty"` // Requires Command 25.0.0+ + AltKeySizeInBits int `json:"AltKeySizeInBits,omitempty"` // Requires Command 25.0.0+ + AltKeyType int `json:"AltKeyType,omitempty"` // Requires Command 25.0.0+ RequesterId int `json:"RequesterId"` IssuedOU string `json:"IssuedOU"` + IssuedEmail string `json:"IssuedEmail"` KeyUsage int `json:"KeyUsage"` SigningAlgorithm string `json:"SigningAlgorithm"` + AltSigningAlgorithm string `json:"AltSigningAlgorithm,omitempty"` // Requires Command 25.0.0+ CertStateString string `json:"CertStateString"` KeyTypeString string `json:"KeyTypeString"` + AltKeyTypeString string `json:"AltKeyTypeString,omitempty"` // Requires Command 25.0.0+ RevocationEffDate string `json:"RevocationEffDate"` RevocationReason int `json:"RevocationReason"` RevocationComment string `json:"RevocationComment"` @@ -231,6 +255,7 @@ type GetCertificateResponse struct { TemplateName string `json:"TemplateName"` ArchivedKey bool `json:"ArchivedKey"` HasPrivateKey bool `json:"HasPrivateKey"` + HasAltPrivateKey bool `json:"HasAltPrivateKey,omitempty"` // Requires Command 25.0.0+ PrincipalName string `json:"PrincipalName"` CertRequestId int `json:"CertRequestId"` RequesterName string `json:"RequesterName"` @@ -244,8 +269,11 @@ type GetCertificateResponse struct { Metadata interface{} `json:"Metadata"` CertificateKeyId int `json:"CertificateKeyId"` CARowIndex int `json:"CARowIndex"` + CARecordId string `json:"CARecordId"` DetailedKeyUsage []DetailedKeyUsage `json:"detailed_key_usage"` KeyRecoverable bool `json:"KeyRecoverable"` + Curve string `json:"Curve,omitempty"` + EnrollmentPatternId int `json:"EnrollmentPatternId,omitempty"` // Requires Command 25.1.0+ } type ListCertificateResponse struct { @@ -319,3 +347,15 @@ type SubjectAltNameElements struct { type downloadCertificateResponse struct { Content string `json:"Content"` } + +// OwnerRequest represents the request structure for changing certificate ownership +type OwnerRequest struct { + NewRoleId *int `json:"NewRoleId,omitempty"` + NewRoleName *string `json:"NewRoleName,omitempty"` +} + +// CertificateOwnerChangeParams represents the parameters for changing certificate ownership +type CertificateOwnerChangeParams struct { + CollectionId *int `json:"collectionId,omitempty"` + ContainerId *int `json:"containerId,omitempty"` +} diff --git a/v3/api/enrollment_patterns.go b/v3/api/enrollment_patterns.go new file mode 100644 index 0000000..574feb0 --- /dev/null +++ b/v3/api/enrollment_patterns.go @@ -0,0 +1,270 @@ +// Copyright 2024 Keyfactor +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "encoding/json" + "errors" + "fmt" + "log" + "strconv" + "strings" +) + +// CreateEnrollmentPattern creates a new enrollment pattern with the provided properties +func (c *Client) CreateEnrollmentPattern( + req *EnrollmentPatternCreateRequest, + forceTemplateDefault ...bool, +) (*EnrollmentPatternResponse, error) { + log.Println("[INFO] Creating enrollment pattern with Keyfactor") + + // Validate required fields + var missingFields []string + if req.Name == "" { + missingFields = append(missingFields, "Name") + } + if req.Template == 0 { + missingFields = append(missingFields, "Template") + } + + if len(missingFields) > 0 { + return nil, errors.New("Required field(s) missing: " + strings.Join(missingFields, ", ")) + } + + // Set Keyfactor-specific headers + headers := &apiHeaders{ + Headers: []StringTuple{ + {"x-keyfactor-api-version", "1"}, + {"x-keyfactor-requested-with", "APIClient"}, + {"Content-Type", "application/json"}, + }, + } + + // Build URL with query parameters + endpoint := "EnrollmentPatterns" + if len(forceTemplateDefault) > 0 && forceTemplateDefault[0] { + endpoint += "?forceTemplateDefault=true" + } + + keyfactorAPIStruct := &request{ + Method: "POST", + Endpoint: endpoint, + Headers: headers, + Payload: req, + } + + resp, err := c.sendRequest(keyfactorAPIStruct) + if err != nil { + return nil, err + } + + jsonResp := &EnrollmentPatternResponse{} + err = json.NewDecoder(resp.Body).Decode(&jsonResp) + if err != nil { + return nil, err + } + + return jsonResp, nil +} + +// GetEnrollmentPatterns returns all enrollment patterns according to the provided filter and output parameters +func (c *Client) GetEnrollmentPatterns(params ...*EnrollmentPatternsQueryParams) ([]EnrollmentPatternResponse, error) { + log.Println("[INFO] Fetching enrollment patterns from Keyfactor") + + // Set Keyfactor-specific headers + headers := &apiHeaders{ + Headers: []StringTuple{ + {"x-keyfactor-api-version", "1"}, + {"x-keyfactor-requested-with", "APIClient"}, + }, + } + + // Build URL with query parameters + endpoint := "EnrollmentPatterns" + var queryParams []string + + if len(params) > 0 && params[0] != nil { + param := params[0] + if param.QueryString != "" { + queryParams = append(queryParams, "QueryString="+param.QueryString) + } + if param.PageReturned > 0 { + queryParams = append(queryParams, "PageReturned="+strconv.Itoa(param.PageReturned)) + } + if param.ReturnLimit > 0 { + queryParams = append(queryParams, "ReturnLimit="+strconv.Itoa(param.ReturnLimit)) + } + if param.SortField != "" { + queryParams = append(queryParams, "SortField="+param.SortField) + } + if param.SortAscending != nil { + queryParams = append(queryParams, "SortAscending="+strconv.Itoa(*param.SortAscending)) + } + } + + if len(queryParams) > 0 { + endpoint += "?" + strings.Join(queryParams, "&") + } + + keyfactorAPIStruct := &request{ + Method: "GET", + Endpoint: endpoint, + Headers: headers, + } + + resp, err := c.sendRequest(keyfactorAPIStruct) + if err != nil { + return nil, err + } + + var jsonResp []EnrollmentPatternResponse + err = json.NewDecoder(resp.Body).Decode(&jsonResp) + if err != nil { + return nil, err + } + + return jsonResp, nil +} + +// GetEnrollmentPattern returns the enrollment pattern associated with the provided ID +func (c *Client) GetEnrollmentPattern(id int) (*EnrollmentPatternResponse, error) { + log.Printf("[INFO] Fetching enrollment pattern with ID %d from Keyfactor", id) + + if id <= 0 { + return nil, errors.New("ID must be a positive integer") + } + + // Set Keyfactor-specific headers + headers := &apiHeaders{ + Headers: []StringTuple{ + {"x-keyfactor-api-version", "1"}, + {"x-keyfactor-requested-with", "APIClient"}, + }, + } + + keyfactorAPIStruct := &request{ + Method: "GET", + Endpoint: fmt.Sprintf("EnrollmentPatterns/%d", id), + Headers: headers, + } + + resp, err := c.sendRequest(keyfactorAPIStruct) + if err != nil { + return nil, err + } + + var jsonResp EnrollmentPatternResponse + err = json.NewDecoder(resp.Body).Decode(&jsonResp) + if err != nil { + return nil, err + } + + return &jsonResp, nil +} + +// UpdateEnrollmentPattern updates an enrollment pattern according to the provided properties and Keyfactor identifier +func (c *Client) UpdateEnrollmentPattern( + id int, + req *EnrollmentPatternRequest, + forceTemplateDefault ...bool, +) (*EnrollmentPatternResponse, error) { + log.Printf("[INFO] Updating enrollment pattern with ID %d in Keyfactor", id) + + if id <= 0 { + return nil, errors.New("ID must be a positive integer") + } + + // Validate required fields + var missingFields []string + if req.Name == "" { + missingFields = append(missingFields, "Name") + } + + if len(missingFields) > 0 { + return nil, errors.New("Required field(s) missing: " + strings.Join(missingFields, ", ")) + } + + // Set Keyfactor-specific headers + headers := &apiHeaders{ + Headers: []StringTuple{ + {"x-keyfactor-api-version", "1"}, + {"x-keyfactor-requested-with", "APIClient"}, + {"Content-Type", "application/json"}, + }, + } + + // Build URL with query parameters + endpoint := fmt.Sprintf("EnrollmentPatterns/%d", id) + if len(forceTemplateDefault) > 0 && forceTemplateDefault[0] { + endpoint += "?forceTemplateDefault=true" + } + + keyfactorAPIStruct := &request{ + Method: "PUT", + Endpoint: endpoint, + Headers: headers, + Payload: req, + } + + resp, err := c.sendRequest(keyfactorAPIStruct) + if err != nil { + return nil, err + } + + var jsonResp EnrollmentPatternResponse + err = json.NewDecoder(resp.Body).Decode(&jsonResp) + if err != nil { + return nil, err + } + + return &jsonResp, nil +} + +// DeleteEnrollmentPattern deletes an enrollment pattern by ID +// Note: This method assumes DELETE is supported based on REST conventions, +// though it may not be explicitly defined in the provided schema +func (c *Client) DeleteEnrollmentPattern(id int) error { + log.Printf("[INFO] Deleting enrollment pattern with ID %d from Keyfactor", id) + + if id <= 0 { + return errors.New("ID must be a positive integer") + } + + // Set Keyfactor-specific headers + headers := &apiHeaders{ + Headers: []StringTuple{ + {"x-keyfactor-api-version", "1"}, + {"x-keyfactor-requested-with", "APIClient"}, + }, + } + + keyfactorAPIStruct := &request{ + Method: "DELETE", + Endpoint: fmt.Sprintf("EnrollmentPatterns/%d", id), + Headers: headers, + } + + resp, err := c.sendRequest(keyfactorAPIStruct) + if err != nil { + return err + } + + // Check if the response indicates success (2xx status codes) + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return fmt.Errorf("failed to delete enrollment pattern: HTTP %d", resp.StatusCode) + } + + return nil +} diff --git a/v3/api/enrollment_patterns_models.go b/v3/api/enrollment_patterns_models.go new file mode 100644 index 0000000..0d2ac42 --- /dev/null +++ b/v3/api/enrollment_patterns_models.go @@ -0,0 +1,219 @@ +// Copyright 2024 Keyfactor +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +// EnrollmentPatternCreateRequest represents the request structure for creating a new enrollment pattern +type EnrollmentPatternCreateRequest struct { + Template int `json:"Template"` + Name string `json:"Name"` + Description *string `json:"Description,omitempty"` + TemplateDefault bool `json:"TemplateDefault,omitempty"` + AssociatedRoles []string `json:"AssociatedRoles,omitempty"` + UseADPermissions bool `json:"UseADPermissions,omitempty"` + CertificateAuthorities []int `json:"CertificateAuthorities,omitempty"` + AllowedEnrollmentTypes int `json:"AllowedEnrollmentTypes,omitempty"` + Regexes []EnrollmentPatternRegexesRequest `json:"Regexes,omitempty"` + MetadataFields []EnrollmentPatternMetadataFieldRequest `json:"MetadataFields,omitempty"` + RestrictCAs bool `json:"RestrictCAs,omitempty"` + Policies EnrollmentPatternPolicyRequest `json:"Policies"` + Defaults []EnrollmentPatternDefaultRequest `json:"Defaults,omitempty"` + EnrollmentFields []EnrollmentPatternFieldRequest `json:"EnrollmentFields,omitempty"` +} + +// EnrollmentPatternRequest represents the request structure for updating an enrollment pattern +type EnrollmentPatternRequest struct { + Name string `json:"Name"` + Description string `json:"Description,omitempty"` + TemplateDefault bool `json:"TemplateDefault,omitempty"` + AssociatedRoles []string `json:"AssociatedRoles,omitempty"` + UseADPermissions bool `json:"UseADPermissions,omitempty"` + CertificateAuthorities []int `json:"CertificateAuthorities,omitempty"` + AllowedEnrollmentTypes int `json:"AllowedEnrollmentTypes,omitempty"` + Regexes []EnrollmentPatternRegexesRequest `json:"Regexes,omitempty"` + MetadataFields []EnrollmentPatternMetadataFieldRequest `json:"MetadataFields,omitempty"` + RestrictCAs bool `json:"RestrictCAs,omitempty"` + Policies EnrollmentPatternPolicyRequest `json:"Policies"` + Defaults []EnrollmentPatternDefaultRequest `json:"Defaults,omitempty"` + EnrollmentFields []EnrollmentPatternFieldRequest `json:"EnrollmentFields,omitempty"` +} + +// EnrollmentPatternResponse represents the response structure for enrollment pattern operations +type EnrollmentPatternResponse struct { + ID int `json:"Id,omitempty"` + Name string `json:"Name,omitempty"` + Description string `json:"Description,omitempty"` + Template *EnrollmentPatternTemplateResponse `json:"Template,omitempty"` + TemplateDefault bool `json:"TemplateDefault,omitempty"` + UseADPermissions bool `json:"UseADPermissions,omitempty"` + AssociatedRoles []EnrollmentPatternAssociatedRoleResponse `json:"AssociatedRoles,omitempty"` + CertificateAuthorities []EnrollmentPatternCAResponse `json:"CertificateAuthorities,omitempty"` + AllowedEnrollmentTypes *int `json:"AllowedEnrollmentTypes,omitempty"` + Regexes []EnrollmentPatternRegexesResponse `json:"Regexes,omitempty"` + MetadataFields []EnrollmentPatternMetadataFieldResponse `json:"MetadataFields,omitempty"` + RestrictCAs bool `json:"RestrictCAs,omitempty"` + Policies *EnrollmentPatternPolicyResponse `json:"Policies,omitempty"` + Defaults []EnrollmentPatternDefaultResponse `json:"Defaults,omitempty"` + EnrollmentFields []EnrollmentPatternFieldResponse `json:"EnrollmentFields,omitempty"` +} + +// EnrollmentPatternRegexesRequest represents regex validation rules for enrollment patterns +type EnrollmentPatternRegexesRequest struct { + SubjectPart string `json:"SubjectPart"` + Regex string `json:"Regex,omitempty"` + Error string `json:"Error,omitempty"` + CaseSensitive bool `json:"CaseSensitive,omitempty"` +} + +// EnrollmentPatternRegexesResponse represents regex validation rules in responses +type EnrollmentPatternRegexesResponse struct { + SubjectPart string `json:"SubjectPart,omitempty"` + Regex string `json:"Regex,omitempty"` + Error string `json:"Error,omitempty"` + CaseSensitive bool `json:"CaseSensitive,omitempty"` +} + +// EnrollmentPatternPolicyRequest represents policy settings for enrollment patterns +type EnrollmentPatternPolicyRequest struct { + AllowKeyReuse *bool `json:"AllowKeyReuse,omitempty"` + AllowWildcards *bool `json:"AllowWildcards,omitempty"` + RFCEnforcement *bool `json:"RFCEnforcement,omitempty"` + CertificateOwnerRole *int `json:"CertificateOwnerRole,omitempty"` + DefaultCertificateOwnerRoleId *int `json:"DefaultCertificateOwnerRoleId,omitempty"` + DefaultCertificateOwnerRoleName *string `json:"DefaultCertificateOwnerRoleName,omitempty"` + DefaultCertificateOwnerOverride bool `json:"DefaultCertificateOwnerOverride,omitempty"` + PrimaryKeyAlgorithms []AlgorithmDataRequestV2 `json:"PrimaryKeyAlgorithms,omitempty"` + AlternativeKeyAlgorithms []AlgorithmDataRequestV2 `json:"AlternativeKeyAlgorithms,omitempty"` +} + +// EnrollmentPatternPolicyResponse represents policy settings in responses +type EnrollmentPatternPolicyResponse struct { + AllowKeyReuse bool `json:"AllowKeyReuse,omitempty"` + AllowWildcards bool `json:"AllowWildcards,omitempty"` + RFCEnforcement bool `json:"RFCEnforcement,omitempty"` + CertificateOwnerRole int `json:"CertificateOwnerRole,omitempty"` + DefaultCertificateOwnerRoleId int `json:"DefaultCertificateOwnerRoleId,omitempty"` + DefaultCertificateOwnerRoleName string `json:"DefaultCertificateOwnerRoleName,omitempty"` + DefaultCertificateOwnerOverride bool `json:"DefaultCertificateOwnerOverride,omitempty"` + PrimaryKeyAlgorithms []AlgorithmDataResponse `json:"PrimaryKeyAlgorithms,omitempty"` + AlternativeKeyAlgorithms []AlgorithmDataResponse `json:"AlternativeKeyAlgorithms,omitempty"` +} + +// EnrollmentPatternMetadataFieldRequest represents metadata field configuration for requests +type EnrollmentPatternMetadataFieldRequest struct { + Id int `json:"Id,omitempty"` + DefaultValue string `json:"DefaultValue,omitempty"` + Validation string `json:"Validation,omitempty"` + Enrollment int `json:"Enrollment,omitempty"` + Message string `json:"Message,omitempty"` + Options string `json:"Options,omitempty"` + DependsOn string `json:"DependsOn,omitempty"` + DependsOnValue string `json:"DependsOnValue,omitempty"` +} + +// EnrollmentPatternMetadataFieldResponse represents metadata field configuration in responses +type EnrollmentPatternMetadataFieldResponse struct { + MetadataId int `json:"MetadataId,omitempty"` + DefaultValue string `json:"DefaultValue,omitempty"` + Validation string `json:"Validation,omitempty"` + Enrollment int `json:"Enrollment,omitempty"` + Message string `json:"Message,omitempty"` + CaseSensitive bool `json:"CaseSensitive,omitempty"` +} + +// EnrollmentPatternDefaultRequest represents default value settings for requests +type EnrollmentPatternDefaultRequest struct { + SubjectPart string `json:"SubjectPart"` + DefaultValue string `json:"DefaultValue,omitempty"` +} + +// EnrollmentPatternDefaultResponse represents default value settings in responses +type EnrollmentPatternDefaultResponse struct { + SubjectPart string `json:"SubjectPart,omitempty"` + Value string `json:"Value,omitempty"` +} + +// EnrollmentPatternFieldRequest represents enrollment field configuration for requests +type EnrollmentPatternFieldRequest struct { + Id int `json:"Id,omitempty"` + DefaultValue string `json:"DefaultValue,omitempty"` + Validation string `json:"Validation,omitempty"` + Enrollment int `json:"Enrollment,omitempty"` + Message string `json:"Message,omitempty"` + Options string `json:"Options,omitempty"` + DependsOn string `json:"DependsOn,omitempty"` + DependsOnValue string `json:"DependsOnValue,omitempty"` +} + +// EnrollmentPatternFieldResponse represents enrollment field configuration in responses +type EnrollmentPatternFieldResponse struct { + Id int `json:"Id,omitempty"` + Name string `json:"Name,omitempty"` + DefaultValue string `json:"DefaultValue,omitempty"` + Validation string `json:"Validation,omitempty"` + Enrollment int `json:"Enrollment,omitempty"` + Message string `json:"Message,omitempty"` + Options []string `json:"Options,omitempty"` + DependsOn string `json:"DependsOn,omitempty"` + DependsOnValue string `json:"DependsOnValue,omitempty"` + DataType int `json:"DataType,omitempty"` + Hint string `json:"Hint,omitempty"` +} + +// EnrollmentPatternTemplateResponse represents template information in responses +type EnrollmentPatternTemplateResponse struct { + Id int `json:"Id,omitempty"` + TemplateName string `json:"TemplateName,omitempty"` + CommonName string `json:"CommonName,omitempty"` + ConfigurationTenant string `json:"ConfigurationTenant,omitempty"` + RequiresApproval bool `json:"RequiresApproval,omitempty"` + FriendlyName string `json:"FriendlyName,omitempty"` +} + +// EnrollmentPatternAssociatedRoleResponse represents associated role information in responses +type EnrollmentPatternAssociatedRoleResponse struct { + Id int `json:"Id,omitempty"` + Name string `json:"Name,omitempty"` +} + +// EnrollmentPatternCAResponse represents certificate authority information in responses +type EnrollmentPatternCAResponse struct { + Id int `json:"Id,omitempty"` + LogicalName string `json:"LogicalName,omitempty"` + HostName string `json:"HostName,omitempty"` + ConfigurationTenant string `json:"ConfigurationTenant,omitempty"` +} + +// AlgorithmDataRequestV2 represents algorithm configuration for requests +type AlgorithmDataRequestV2 struct { + KeyType *string `json:"KeyType,omitempty"` + KeySize *int `json:"KeySize,omitempty"` + CurveName *string `json:"CurveName,omitempty"` +} + +// AlgorithmDataResponse represents algorithm configuration in responses +type AlgorithmDataResponse struct { + Name string `json:"Name,omitempty"` + BitLengths []int `json:"bit_lengths,omitempty"` + Curves []string `json:"curves,omitempty"` +} + +// EnrollmentPatternsQueryParams represents query parameters for listing enrollment patterns +type EnrollmentPatternsQueryParams struct { + QueryString string `json:"queryString,omitempty"` + PageReturned int `json:"pageReturned,omitempty"` + ReturnLimit int `json:"returnLimit,omitempty"` + SortField string `json:"sortField,omitempty"` + SortAscending *int `json:"sortAscending,omitempty"` // 0=ascending, 1=descending +} diff --git a/v3/api/store_models.go b/v3/api/store_models.go index bf4db2c..a210cad 100644 --- a/v3/api/store_models.go +++ b/v3/api/store_models.go @@ -51,14 +51,14 @@ type UpdateStoreFctArgs struct { // automatically populated by the CreateStore method. However, if configured, this field will be used. PropertiesString string `json:"Properties,omitempty"` // Mapped name-value pair field used to configure properties. - Properties map[string]interface{} `json:"-"` - AgentId string `json:"AgentId"` - AgentAssigned *bool `json:"AgentAssigned,omitempty"` - ContainerName *string `json:"ContainerName,omitempty"` - InventorySchedule *InventorySchedule `json:"InventorySchedule,omitempty"` - ReEnrollmentStatus *ReEnrollmnentConfig `json:"ReEnrollmentStatus,omitempty"` - SetNewPasswordAllowed *bool `json:"SetNewPasswordAllowed,omitempty"` - Password *UpdateStorePasswordConfig `json:"Password"` + Properties map[string]interface{} `json:"-"` + AgentId string `json:"AgentId"` + AgentAssigned *bool `json:"AgentAssigned,omitempty"` + ContainerName *string `json:"ContainerName,omitempty"` + InventorySchedule *InventorySchedule `json:"InventorySchedule,omitempty"` + ReEnrollmentStatus *ReEnrollmnentConfig `json:"ReEnrollmentStatus,omitempty"` + SetNewPasswordAllowed *bool `json:"SetNewPasswordAllowed,omitempty"` + Password *StorePasswordConfig `json:"Password"` } type UpdateStorePasswordConfig struct {