github.com/akamai/AkamaiOPEN-edgegrid-golang/v8@v8.1.0/pkg/cps/enrollments.go (about)

     1  package cps
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"net/http"
     8  	"net/url"
     9  	"strconv"
    10  
    11  	validation "github.com/go-ozzo/ozzo-validation/v4"
    12  )
    13  
    14  type (
    15  	// Enrollments is a CPS enrollments API interface
    16  	Enrollments interface {
    17  		// ListEnrollments fetches all enrollments with given contractId
    18  		//
    19  		// See https://techdocs.akamai.com/cps/reference/get-enrollments
    20  		ListEnrollments(context.Context, ListEnrollmentsRequest) (*ListEnrollmentsResponse, error)
    21  
    22  		// GetEnrollment fetches enrollment object with given ID
    23  		//
    24  		// See: https://techdocs.akamai.com/cps/reference/get-enrollment
    25  		GetEnrollment(context.Context, GetEnrollmentRequest) (*GetEnrollmentResponse, error)
    26  
    27  		// CreateEnrollment creates a new enrollment
    28  		//
    29  		// See: https://techdocs.akamai.com/cps/reference/post-enrollment
    30  		CreateEnrollment(context.Context, CreateEnrollmentRequest) (*CreateEnrollmentResponse, error)
    31  
    32  		// UpdateEnrollment updates a single enrollment entry with given ID
    33  		//
    34  		// See: https://techdocs.akamai.com/cps/reference/put-enrollment
    35  		UpdateEnrollment(context.Context, UpdateEnrollmentRequest) (*UpdateEnrollmentResponse, error)
    36  
    37  		// RemoveEnrollment removes an enrollment with given ID
    38  		//
    39  		// See: https://techdocs.akamai.com/cps/reference/delete-enrollment
    40  		RemoveEnrollment(context.Context, RemoveEnrollmentRequest) (*RemoveEnrollmentResponse, error)
    41  	}
    42  
    43  	// ListEnrollmentsResponse represents list of CPS enrollment objects under given contractId. It is used as a response body while fetching enrollments by contractId
    44  	ListEnrollmentsResponse struct {
    45  		Enrollments []Enrollment `json:"enrollments"`
    46  	}
    47  
    48  	// GetEnrollmentResponse contains response body from GetEnrollment operation
    49  	GetEnrollmentResponse Enrollment
    50  
    51  	// Enrollment represents a CPS enrollment object. It is used as a response body while fetching enrollment by ID and listing multiple enrollments
    52  	Enrollment struct {
    53  		ID                             int                   `json:"id"`
    54  		ProductionSlots                []int                 `json:"productionSlots"`
    55  		StagingSlots                   []int                 `json:"stagingSlots"`
    56  		AssignedSlots                  []int                 `json:"assignedSlots"`
    57  		AdminContact                   *Contact              `json:"adminContact"`
    58  		AutoRenewalStartTime           string                `json:"autoRenewalStartTime,omitempty"`
    59  		CertificateChainType           string                `json:"certificateChainType,omitempty"`
    60  		CertificateType                string                `json:"certificateType"`
    61  		ChangeManagement               bool                  `json:"changeManagement"`
    62  		CSR                            *CSR                  `json:"csr"`
    63  		EnableMultiStackedCertificates bool                  `json:"enableMultiStackedCertificates"`
    64  		Location                       string                `json:"location,omitempty"`
    65  		MaxAllowedSanNames             int                   `json:"maxAllowedSanNames,omitempty"`
    66  		MaxAllowedWildcardSanNames     int                   `json:"maxAllowedWildcardSanNames,omitempty"`
    67  		NetworkConfiguration           *NetworkConfiguration `json:"networkConfiguration"`
    68  		Org                            *Org                  `json:"org"`
    69  		OrgID                          *int                  `json:"orgId"`
    70  		PendingChanges                 []PendingChange       `json:"pendingChanges,omitempty"`
    71  		RA                             string                `json:"ra"`
    72  		SignatureAlgorithm             string                `json:"signatureAlgorithm,omitempty"`
    73  		TechContact                    *Contact              `json:"techContact"`
    74  		ThirdParty                     *ThirdParty           `json:"thirdParty,omitempty"`
    75  		ValidationType                 string                `json:"validationType"`
    76  	}
    77  
    78  	// Contact contains contact information
    79  	Contact struct {
    80  		AddressLineOne   string `json:"addressLineOne,omitempty"`
    81  		AddressLineTwo   string `json:"addressLineTwo,omitempty"`
    82  		City             string `json:"city,omitempty"`
    83  		Country          string `json:"country,omitempty"`
    84  		Email            string `json:"email,omitempty"`
    85  		FirstName        string `json:"firstName,omitempty"`
    86  		LastName         string `json:"lastName,omitempty"`
    87  		OrganizationName string `json:"organizationName,omitempty"`
    88  		Phone            string `json:"phone,omitempty"`
    89  		PostalCode       string `json:"postalCode,omitempty"`
    90  		Region           string `json:"region,omitempty"`
    91  		Title            string `json:"title,omitempty"`
    92  	}
    93  
    94  	// CSR is a Certificate Signing Request object
    95  	CSR struct {
    96  		C                   string   `json:"c,omitempty"`
    97  		CN                  string   `json:"cn"`
    98  		L                   string   `json:"l,omitempty"`
    99  		O                   string   `json:"o,omitempty"`
   100  		OU                  string   `json:"ou,omitempty"`
   101  		PreferredTrustChain string   `json:"preferredTrustChain,omitempty"`
   102  		SANS                []string `json:"sans,omitempty"`
   103  		ST                  string   `json:"st,omitempty"`
   104  	}
   105  
   106  	// NetworkConfiguration contains settings that specify any network information and TLS Metadata you want CPS to use to push the completed certificate to the network
   107  	NetworkConfiguration struct {
   108  		ClientMutualAuthentication *ClientMutualAuthentication `json:"clientMutualAuthentication,omitempty"`
   109  		DisallowedTLSVersions      []string                    `json:"disallowedTlsVersions,omitempty"`
   110  		DNSNameSettings            *DNSNameSettings            `json:"dnsNameSettings,omitempty"`
   111  		Geography                  string                      `json:"geography,omitempty"`
   112  		MustHaveCiphers            string                      `json:"mustHaveCiphers,omitempty"`
   113  		OCSPStapling               OCSPStapling                `json:"ocspStapling,omitempty"`
   114  		PreferredCiphers           string                      `json:"preferredCiphers,omitempty"`
   115  		QuicEnabled                bool                        `json:"quicEnabled"`
   116  		SecureNetwork              string                      `json:"secureNetwork,omitempty"`
   117  		SNIOnly                    bool                        `json:"sniOnly"`
   118  	}
   119  
   120  	// ClientMutualAuthentication specifies the trust chain that is used to verify client certificates and some configuration options
   121  	ClientMutualAuthentication struct {
   122  		AuthenticationOptions *AuthenticationOptions `json:"authenticationOptions,omitempty"`
   123  		SetID                 string                 `json:"setId,omitempty"`
   124  	}
   125  
   126  	// AuthenticationOptions contain the configuration options for the selected trust chain
   127  	AuthenticationOptions struct {
   128  		OCSP               *OCSP `json:"ocsp,omitempty"`
   129  		SendCAListToClient *bool `json:"sendCaListToClient,omitempty"`
   130  	}
   131  
   132  	// OCSP specifies whether you want to enable ocsp stapling for client certificates
   133  	OCSP struct {
   134  		Enabled *bool `json:"enabled,omitempty"`
   135  	}
   136  
   137  	// DNSNameSettings contain DNS name setting in given network configuration
   138  	DNSNameSettings struct {
   139  		CloneDNSNames bool     `json:"cloneDnsNames"`
   140  		DNSNames      []string `json:"dnsNames,omitempty"`
   141  	}
   142  
   143  	// Org represents organization information
   144  	Org struct {
   145  		AddressLineOne string `json:"addressLineOne,omitempty"`
   146  		AddressLineTwo string `json:"addressLineTwo,omitempty"`
   147  		City           string `json:"city,omitempty"`
   148  		Country        string `json:"country,omitempty"`
   149  		Name           string `json:"name,omitempty"`
   150  		Phone          string `json:"phone,omitempty"`
   151  		PostalCode     string `json:"postalCode,omitempty"`
   152  		Region         string `json:"region,omitempty"`
   153  	}
   154  
   155  	// PendingChange represents pending change information
   156  	PendingChange struct {
   157  		ChangeType string `json:"changeType,omitempty"`
   158  		Location   string `json:"location,omitempty"`
   159  	}
   160  
   161  	// ThirdParty specifies that you want to use a third party certificate
   162  	ThirdParty struct {
   163  		ExcludeSANS bool `json:"excludeSans"`
   164  	}
   165  
   166  	// ListEnrollmentsRequest contains Contract ID of enrollments that are to be fetched with ListEnrollments
   167  	ListEnrollmentsRequest struct {
   168  		ContractID string
   169  	}
   170  
   171  	// GetEnrollmentRequest contains ID of an enrollment that is to be fetched with GetEnrollment
   172  	GetEnrollmentRequest struct {
   173  		EnrollmentID int
   174  	}
   175  
   176  	// CreateEnrollmentRequest contains request body and path parameters used to create an enrollment
   177  	CreateEnrollmentRequest struct {
   178  		EnrollmentRequestBody
   179  		ContractID       string
   180  		DeployNotAfter   string
   181  		DeployNotBefore  string
   182  		AllowDuplicateCN bool
   183  	}
   184  
   185  	// EnrollmentRequestBody represents request body parameters specific to the enrollment
   186  	EnrollmentRequestBody struct {
   187  		AdminContact                   *Contact              `json:"adminContact"`
   188  		AutoRenewalStartTime           string                `json:"autoRenewalStartTime,omitempty"`
   189  		CertificateChainType           string                `json:"certificateChainType,omitempty"`
   190  		CertificateType                string                `json:"certificateType"`
   191  		ChangeManagement               bool                  `json:"changeManagement"`
   192  		CSR                            *CSR                  `json:"csr"`
   193  		EnableMultiStackedCertificates bool                  `json:"enableMultiStackedCertificates"`
   194  		NetworkConfiguration           *NetworkConfiguration `json:"networkConfiguration"`
   195  		Org                            *Org                  `json:"org"`
   196  		OrgID                          *int                  `json:"orgId,omitempty"`
   197  		RA                             string                `json:"ra"`
   198  		SignatureAlgorithm             string                `json:"signatureAlgorithm,omitempty"`
   199  		TechContact                    *Contact              `json:"techContact"`
   200  		ThirdParty                     *ThirdParty           `json:"thirdParty,omitempty"`
   201  		ValidationType                 string                `json:"validationType"`
   202  	}
   203  
   204  	// CreateEnrollmentResponse contains response body returned after successful enrollment creation
   205  	CreateEnrollmentResponse struct {
   206  		ID         int
   207  		Enrollment string   `json:"enrollment"`
   208  		Changes    []string `json:"changes"`
   209  	}
   210  
   211  	// UpdateEnrollmentRequest contains request body and path parameters used to update an enrollment
   212  	UpdateEnrollmentRequest struct {
   213  		EnrollmentRequestBody
   214  		EnrollmentID              int
   215  		AllowCancelPendingChanges *bool
   216  		AllowStagingBypass        *bool
   217  		DeployNotAfter            string
   218  		DeployNotBefore           string
   219  		ForceRenewal              *bool
   220  		RenewalDateCheckOverride  *bool
   221  	}
   222  
   223  	// UpdateEnrollmentResponse contains response body returned after successful enrollment update
   224  	UpdateEnrollmentResponse struct {
   225  		ID         int
   226  		Enrollment string   `json:"enrollment"`
   227  		Changes    []string `json:"changes"`
   228  	}
   229  
   230  	// RemoveEnrollmentRequest contains parameters necessary to send a RemoveEnrollment request
   231  	RemoveEnrollmentRequest struct {
   232  		EnrollmentID              int
   233  		AllowCancelPendingChanges *bool
   234  		DeployNotAfter            string
   235  		DeployNotBefore           string
   236  	}
   237  
   238  	// RemoveEnrollmentResponse contains response body returned after successful enrollment deletion
   239  	RemoveEnrollmentResponse struct {
   240  		Enrollment string   `json:"enrollment"`
   241  		Changes    []string `json:"changes"`
   242  	}
   243  
   244  	// OCSPStapling is used to enable OCSP stapling for an enrollment
   245  	OCSPStapling string
   246  )
   247  
   248  const (
   249  	// OCSPStaplingOn parameter value
   250  	OCSPStaplingOn OCSPStapling = "on"
   251  	// OCSPStaplingOff parameter value
   252  	OCSPStaplingOff OCSPStapling = "off"
   253  	// OCSPStaplingNotSet parameter value
   254  	OCSPStaplingNotSet OCSPStapling = "not-set"
   255  )
   256  
   257  // Validate performs validation on EnrollmentRequestBody
   258  func (e EnrollmentRequestBody) Validate() error {
   259  	errs := validation.Errors{
   260  		"adminContact":         validation.Validate(e.AdminContact, validation.Required),
   261  		"certificateType":      validation.Validate(e.CertificateType, validation.Required),
   262  		"csr":                  validation.Validate(e.CSR, validation.Required),
   263  		"networkConfiguration": validation.Validate(e.NetworkConfiguration, validation.Required),
   264  		"org":                  validation.Validate(e.Org, validation.Required),
   265  		"ra":                   validation.Validate(e.RA, validation.Required),
   266  		"techContact":          validation.Validate(e.TechContact, validation.Required),
   267  		"validationType":       validation.Validate(e.ValidationType, validation.Required),
   268  		"thirdParty":           validation.Validate(e.ThirdParty),
   269  	}
   270  
   271  	if e.CSR != nil {
   272  		errs["csr.preferredTrustChain"] = validation.Validate(e.CSR.PreferredTrustChain,
   273  			validation.When(e.ValidationType != "dv", validation.Empty.Error("must be blank when 'validationType' is not 'dv'")))
   274  	}
   275  
   276  	return errs.Filter()
   277  }
   278  
   279  // Validate performs validation on CSR
   280  func (c CSR) Validate() error {
   281  	return validation.Errors{
   282  		"cn": validation.Validate(c.CN, validation.Required),
   283  	}.Filter()
   284  }
   285  
   286  // Validate performs validation on NetworkConfiguration
   287  func (n NetworkConfiguration) Validate() error {
   288  	return validation.Errors{
   289  		"ocspStapling": validation.Validate(n.OCSPStapling, validation.In(OCSPStaplingOn, OCSPStaplingOff, OCSPStaplingNotSet)),
   290  	}.Filter()
   291  }
   292  
   293  // Validate performs validation on ListEnrollmentsRequest
   294  func (e ListEnrollmentsRequest) Validate() error {
   295  	return validation.Errors{
   296  		"contractId": validation.Validate(e.ContractID, validation.Required),
   297  	}.Filter()
   298  }
   299  
   300  // Validate performs validation on GetEnrollmentRequest
   301  func (e GetEnrollmentRequest) Validate() error {
   302  	return validation.Errors{
   303  		"enrollmentId": validation.Validate(e.EnrollmentID, validation.Required),
   304  	}.Filter()
   305  }
   306  
   307  // Validate performs validation on CreateEnrollmentRequest
   308  func (e CreateEnrollmentRequest) Validate() error {
   309  	return validation.Errors{
   310  		"enrollment": validation.Validate(e.EnrollmentRequestBody, validation.Required),
   311  		"contractId": validation.Validate(e.ContractID, validation.Required),
   312  	}.Filter()
   313  }
   314  
   315  // Validate performs validation on UpdateEnrollmentRequest
   316  func (e UpdateEnrollmentRequest) Validate() error {
   317  	return validation.Errors{
   318  		"enrollment":   validation.Validate(e.EnrollmentRequestBody, validation.Required),
   319  		"enrollmentId": validation.Validate(e.EnrollmentID, validation.Required),
   320  	}.Filter()
   321  }
   322  
   323  // Validate performs validation on RemoveEnrollmentRequest
   324  func (e RemoveEnrollmentRequest) Validate() error {
   325  	return validation.Errors{
   326  		"enrollmentId": validation.Validate(e.EnrollmentID, validation.Required),
   327  	}.Filter()
   328  }
   329  
   330  var (
   331  	// ErrListEnrollments is returned when ListEnrollments fails
   332  	ErrListEnrollments = errors.New("fetching enrollments")
   333  	// ErrGetEnrollment is returned when GetEnrollment fails
   334  	ErrGetEnrollment = errors.New("fetching enrollment")
   335  	// ErrCreateEnrollment is returned when CreateEnrollment fails
   336  	ErrCreateEnrollment = errors.New("create enrollment")
   337  	// ErrUpdateEnrollment is returned when UpdateEnrollment fails
   338  	ErrUpdateEnrollment = errors.New("update enrollment")
   339  	// ErrRemoveEnrollment is returned when RemoveEnrollment fails
   340  	ErrRemoveEnrollment = errors.New("remove enrollment")
   341  )
   342  
   343  func (c *cps) ListEnrollments(ctx context.Context, params ListEnrollmentsRequest) (*ListEnrollmentsResponse, error) {
   344  	if err := params.Validate(); err != nil {
   345  		return nil, fmt.Errorf("%s: %w: %s", ErrListEnrollments, ErrStructValidation, err)
   346  	}
   347  
   348  	logger := c.Log(ctx)
   349  	logger.Debug("ListEnrollments")
   350  
   351  	uri := fmt.Sprintf("/cps/v2/enrollments?contractId=%s", params.ContractID)
   352  
   353  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri, nil)
   354  	if err != nil {
   355  		return nil, fmt.Errorf("%w: failed to create request: %s", ErrListEnrollments, err)
   356  	}
   357  	req.Header.Set("Accept", "application/vnd.akamai.cps.enrollments.v11+json")
   358  
   359  	var result ListEnrollmentsResponse
   360  
   361  	resp, err := c.Exec(req, &result)
   362  	if err != nil {
   363  		return nil, fmt.Errorf("%w: request failed: %s", ErrListEnrollments, err)
   364  	}
   365  
   366  	if resp.StatusCode != http.StatusOK {
   367  		return nil, fmt.Errorf("%s: %w", ErrListEnrollments, c.Error(resp))
   368  	}
   369  
   370  	return &result, nil
   371  }
   372  
   373  func (c *cps) GetEnrollment(ctx context.Context, params GetEnrollmentRequest) (*GetEnrollmentResponse, error) {
   374  	if err := params.Validate(); err != nil {
   375  		return nil, fmt.Errorf("%s: %w: %s", ErrGetEnrollment, ErrStructValidation, err)
   376  	}
   377  
   378  	logger := c.Log(ctx)
   379  	logger.Debug("GetEnrollment")
   380  
   381  	uri := fmt.Sprintf("/cps/v2/enrollments/%d", params.EnrollmentID)
   382  
   383  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri, nil)
   384  	if err != nil {
   385  		return nil, fmt.Errorf("%w: failed to create request: %s", ErrGetEnrollment, err)
   386  	}
   387  	req.Header.Set("Accept", "application/vnd.akamai.cps.enrollment.v11+json")
   388  
   389  	var result GetEnrollmentResponse
   390  
   391  	resp, err := c.Exec(req, &result)
   392  	if err != nil {
   393  		return nil, fmt.Errorf("%w: request failed: %s", ErrGetEnrollment, err)
   394  	}
   395  
   396  	if resp.StatusCode != http.StatusOK {
   397  		return nil, fmt.Errorf("%s: %w", ErrGetEnrollment, c.Error(resp))
   398  	}
   399  
   400  	return &result, nil
   401  }
   402  
   403  func (c *cps) CreateEnrollment(ctx context.Context, params CreateEnrollmentRequest) (*CreateEnrollmentResponse, error) {
   404  	if err := params.Validate(); err != nil {
   405  		return nil, fmt.Errorf("%s: %w: %s", ErrCreateEnrollment, ErrStructValidation, err)
   406  	}
   407  
   408  	logger := c.Log(ctx)
   409  	logger.Debug("CreateEnrollment")
   410  
   411  	uri, err := url.Parse(fmt.Sprintf("/cps/v2/enrollments?contractId=%s", params.ContractID))
   412  	if err != nil {
   413  		return nil, fmt.Errorf("%w: parsing URL: %s", ErrCreateEnrollment, err)
   414  	}
   415  	query := uri.Query()
   416  	if params.DeployNotAfter != "" {
   417  		query.Add("deploy-not-after", params.DeployNotAfter)
   418  	}
   419  	if params.DeployNotBefore != "" {
   420  		query.Add("deploy-not-before", params.DeployNotBefore)
   421  	}
   422  	if params.AllowDuplicateCN {
   423  		query.Add("allow-duplicate-cn", "true")
   424  	}
   425  	uri.RawQuery = query.Encode()
   426  	req, err := http.NewRequestWithContext(ctx, http.MethodPost, uri.String(), nil)
   427  	if err != nil {
   428  		return nil, fmt.Errorf("%w: failed to create request: %s", ErrCreateEnrollment, err)
   429  	}
   430  	req.Header.Set("Accept", "application/vnd.akamai.cps.enrollment-status.v1+json")
   431  	req.Header.Set("Content-Type", "application/vnd.akamai.cps.enrollment.v11+json; charset=utf-8")
   432  
   433  	var result CreateEnrollmentResponse
   434  
   435  	resp, err := c.Exec(req, &result, params.EnrollmentRequestBody)
   436  	if err != nil {
   437  		return nil, fmt.Errorf("%w: request failed: %s", ErrCreateEnrollment, err)
   438  	}
   439  
   440  	if resp.StatusCode != http.StatusAccepted {
   441  		return nil, fmt.Errorf("%s: %w", ErrCreateEnrollment, c.Error(resp))
   442  	}
   443  	id, err := GetIDFromLocation(result.Enrollment)
   444  	if err != nil {
   445  		return nil, fmt.Errorf("%s: %w: %s", ErrCreateEnrollment, ErrInvalidLocation, err)
   446  	}
   447  	result.ID = id
   448  
   449  	return &result, nil
   450  }
   451  
   452  func (c *cps) UpdateEnrollment(ctx context.Context, params UpdateEnrollmentRequest) (*UpdateEnrollmentResponse, error) {
   453  	if err := params.Validate(); err != nil {
   454  		return nil, fmt.Errorf("%s: %w: %s", ErrCreateEnrollment, ErrStructValidation, err)
   455  	}
   456  
   457  	logger := c.Log(ctx)
   458  	logger.Debug("UpdateEnrollment")
   459  
   460  	uri, err := url.Parse(fmt.Sprintf("/cps/v2/enrollments/%d", params.EnrollmentID))
   461  	if err != nil {
   462  		return nil, fmt.Errorf("%w: parsing URL: %s", ErrUpdateEnrollment, err)
   463  	}
   464  	query := uri.Query()
   465  	if params.AllowCancelPendingChanges != nil {
   466  		query.Add("allow-cancel-pending-changes", strconv.FormatBool(*params.AllowCancelPendingChanges))
   467  	}
   468  	if params.AllowStagingBypass != nil {
   469  		query.Add("allow-staging-bypass", strconv.FormatBool(*params.AllowStagingBypass))
   470  	}
   471  	if params.DeployNotAfter != "" {
   472  		query.Add("deploy-not-after", params.DeployNotAfter)
   473  	}
   474  	if params.DeployNotBefore != "" {
   475  		query.Add("deploy-not-before", params.DeployNotBefore)
   476  	}
   477  	if params.ForceRenewal != nil {
   478  		query.Add("force-renewal", strconv.FormatBool(*params.ForceRenewal))
   479  	}
   480  	if params.RenewalDateCheckOverride != nil {
   481  		query.Add("renewal-date-check-override", strconv.FormatBool(*params.RenewalDateCheckOverride))
   482  	}
   483  
   484  	uri.RawQuery = query.Encode()
   485  	req, err := http.NewRequestWithContext(ctx, http.MethodPut, uri.String(), nil)
   486  	if err != nil {
   487  		return nil, fmt.Errorf("%w: failed to create request: %s", ErrUpdateEnrollment, err)
   488  	}
   489  	req.Header.Set("Accept", "application/vnd.akamai.cps.enrollment-status.v1+json")
   490  	req.Header.Set("Content-Type", "application/vnd.akamai.cps.enrollment.v11+json; charset=utf-8")
   491  
   492  	var result UpdateEnrollmentResponse
   493  
   494  	resp, err := c.Exec(req, &result, params.EnrollmentRequestBody)
   495  	if err != nil {
   496  		return nil, fmt.Errorf("%w: request failed: %s", ErrUpdateEnrollment, err)
   497  	}
   498  
   499  	if resp.StatusCode != http.StatusAccepted && resp.StatusCode != http.StatusOK {
   500  		return nil, fmt.Errorf("%s: %w", ErrUpdateEnrollment, c.Error(resp))
   501  	}
   502  	id, err := GetIDFromLocation(result.Enrollment)
   503  	if err != nil {
   504  		return nil, fmt.Errorf("%s: %w: %s", ErrCreateEnrollment, ErrInvalidLocation, err)
   505  	}
   506  	result.ID = id
   507  
   508  	return &result, nil
   509  }
   510  
   511  func (c *cps) RemoveEnrollment(ctx context.Context, params RemoveEnrollmentRequest) (*RemoveEnrollmentResponse, error) {
   512  	if err := params.Validate(); err != nil {
   513  		return nil, fmt.Errorf("%s: %w: %s", ErrRemoveEnrollment, ErrStructValidation, err)
   514  	}
   515  
   516  	logger := c.Log(ctx)
   517  	logger.Debug("RemoveEnrollment")
   518  
   519  	uri, err := url.Parse(fmt.Sprintf("/cps/v2/enrollments/%d", params.EnrollmentID))
   520  	if err != nil {
   521  		return nil, fmt.Errorf("%w: parsing URL: %s", ErrRemoveEnrollment, err)
   522  	}
   523  	query := uri.Query()
   524  	if params.AllowCancelPendingChanges != nil {
   525  		query.Add("allow-cancel-pending-changes", strconv.FormatBool(*params.AllowCancelPendingChanges))
   526  	}
   527  	if params.DeployNotAfter != "" {
   528  		query.Add("deploy-not-after", params.DeployNotAfter)
   529  	}
   530  	if params.DeployNotBefore != "" {
   531  		query.Add("deploy-not-before", params.DeployNotBefore)
   532  	}
   533  
   534  	uri.RawQuery = query.Encode()
   535  	req, err := http.NewRequestWithContext(ctx, http.MethodDelete, uri.String(), nil)
   536  	if err != nil {
   537  		return nil, fmt.Errorf("%w: failed to create request: %s", ErrRemoveEnrollment, err)
   538  	}
   539  	req.Header.Set("Accept", "application/vnd.akamai.cps.enrollment-status.v1+json")
   540  
   541  	var result RemoveEnrollmentResponse
   542  
   543  	resp, err := c.Exec(req, &result)
   544  	if err != nil {
   545  		return nil, fmt.Errorf("%w: request failed: %s", ErrRemoveEnrollment, err)
   546  	}
   547  
   548  	if resp.StatusCode != http.StatusAccepted && resp.StatusCode != http.StatusOK {
   549  		return nil, fmt.Errorf("%s: %w", ErrRemoveEnrollment, c.Error(resp))
   550  	}
   551  
   552  	return &result, nil
   553  }