From 0e052fe2993d2fa398b013e0ffef845eeb35661d Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Wed, 30 Mar 2022 14:21:39 +0200 Subject: [PATCH] Add authority policy API --- authority/admin/api/acme.go | 2 +- authority/admin/api/acme_test.go | 11 ++- authority/admin/api/handler.go | 18 ++-- authority/admin/api/middleware.go | 38 +++++++- authority/admin/api/middleware_test.go | 10 +- authority/admin/api/policy.go | 122 +++++++---------------- authority/authority.go | 1 + authority/policy.go | 82 ++++++++++++---- ca/adminClient.go | 129 +++++++++++++++++++++++++ 9 files changed, 284 insertions(+), 129 deletions(-) diff --git a/authority/admin/api/acme.go b/authority/admin/api/acme.go index 39be50c7..88bed2f5 100644 --- a/authority/admin/api/acme.go +++ b/authority/admin/api/acme.go @@ -35,7 +35,7 @@ type GetExternalAccountKeysResponse struct { // requireEABEnabled is a middleware that ensures ACME EAB is enabled // before serving requests that act on ACME EAB credentials. -func (h *Handler) requireEABEnabled(next nextHTTP) nextHTTP { +func (h *Handler) requireEABEnabled(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() provName := chi.URLParam(r, "provisionerName") diff --git a/authority/admin/api/acme_test.go b/authority/admin/api/acme_test.go index 6ffe1418..5c61656d 100644 --- a/authority/admin/api/acme_test.go +++ b/authority/admin/api/acme_test.go @@ -12,12 +12,15 @@ import ( "testing" "github.com/go-chi/chi" + + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" + + "go.step.sm/linkedca" + "github.com/smallstep/assert" "github.com/smallstep/certificates/authority/admin" "github.com/smallstep/certificates/authority/provisioner" - "go.step.sm/linkedca" - "google.golang.org/protobuf/encoding/protojson" - "google.golang.org/protobuf/proto" ) func readProtoJSON(r io.ReadCloser, m proto.Message) error { @@ -34,7 +37,7 @@ func TestHandler_requireEABEnabled(t *testing.T) { ctx context.Context adminDB admin.DB auth adminAuthority - next nextHTTP + next http.HandlerFunc err *admin.Error statusCode int } diff --git a/authority/admin/api/handler.go b/authority/admin/api/handler.go index 0dd45cb0..aa7b6300 100644 --- a/authority/admin/api/handler.go +++ b/authority/admin/api/handler.go @@ -1,6 +1,8 @@ package api import ( + "net/http" + "github.com/smallstep/certificates/acme" "github.com/smallstep/certificates/api" "github.com/smallstep/certificates/authority/admin" @@ -29,19 +31,19 @@ func NewHandler(auth adminAuthority, adminDB admin.DB, acmeDB acme.DB, acmeRespo // Route traffic and implement the Router interface. func (h *Handler) Route(r api.Router) { - authnz := func(next nextHTTP) nextHTTP { + authnz := func(next http.HandlerFunc) http.HandlerFunc { return h.extractAuthorizeTokenAdmin(h.requireAPIEnabled(next)) } - requireEABEnabled := func(next nextHTTP) nextHTTP { + requireEABEnabled := func(next http.HandlerFunc) http.HandlerFunc { return h.requireEABEnabled(next) } - enabledInStandalone := func(next nextHTTP) nextHTTP { + enabledInStandalone := func(next http.HandlerFunc) http.HandlerFunc { return h.checkAction(next, true) } - disabledInStandalone := func(next nextHTTP) nextHTTP { + disabledInStandalone := func(next http.HandlerFunc) http.HandlerFunc { return h.checkAction(next, false) } @@ -73,10 +75,10 @@ func (h *Handler) Route(r api.Router) { // Policy - Provisioner //r.MethodFunc("GET", "/provisioners/{name}/policy", noauth(h.policyResponder.GetProvisionerPolicy)) - r.MethodFunc("GET", "/provisioners/{name}/policy", authnz(disabledInStandalone(h.policyResponder.GetProvisionerPolicy))) - r.MethodFunc("POST", "/provisioners/{name}/policy", authnz(disabledInStandalone(h.policyResponder.CreateProvisionerPolicy))) - r.MethodFunc("PUT", "/provisioners/{name}/policy", authnz(disabledInStandalone(h.policyResponder.UpdateProvisionerPolicy))) - r.MethodFunc("DELETE", "/provisioners/{name}/policy", authnz(disabledInStandalone(h.policyResponder.DeleteProvisionerPolicy))) + r.MethodFunc("GET", "/provisioners/{provisionerName}/policy", authnz(disabledInStandalone(h.loadProvisionerByName(h.policyResponder.GetProvisionerPolicy)))) + r.MethodFunc("POST", "/provisioners/{provisionerName}/policy", authnz(disabledInStandalone(h.loadProvisionerByName(h.policyResponder.CreateProvisionerPolicy)))) + r.MethodFunc("PUT", "/provisioners/{provisionerName}/policy", authnz(disabledInStandalone(h.loadProvisionerByName(h.policyResponder.UpdateProvisionerPolicy)))) + r.MethodFunc("DELETE", "/provisioners/{provisionerName}/policy", authnz(disabledInStandalone(h.loadProvisionerByName(h.policyResponder.DeleteProvisionerPolicy)))) // Policy - ACME Account // TODO: ensure we don't clash with eab; might want to change eab paths slightly (as long as we don't have it released completely; needs changes in adminClient too) diff --git a/authority/admin/api/middleware.go b/authority/admin/api/middleware.go index 1acc661e..426d1d42 100644 --- a/authority/admin/api/middleware.go +++ b/authority/admin/api/middleware.go @@ -5,16 +5,17 @@ import ( "go.step.sm/linkedca" + "github.com/go-chi/chi" + "github.com/smallstep/certificates/api" "github.com/smallstep/certificates/authority/admin" "github.com/smallstep/certificates/authority/admin/db/nosql" + "github.com/smallstep/certificates/authority/provisioner" ) -type nextHTTP = func(http.ResponseWriter, *http.Request) - // requireAPIEnabled is a middleware that ensures the Administration API // is enabled before servicing requests. -func (h *Handler) requireAPIEnabled(next nextHTTP) nextHTTP { +func (h *Handler) requireAPIEnabled(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if !h.auth.IsAdminAPIEnabled() { api.WriteError(w, admin.NewError(admin.ErrorNotImplementedType, @@ -26,7 +27,7 @@ func (h *Handler) requireAPIEnabled(next nextHTTP) nextHTTP { } // extractAuthorizeTokenAdmin is a middleware that extracts and caches the bearer token. -func (h *Handler) extractAuthorizeTokenAdmin(next nextHTTP) nextHTTP { +func (h *Handler) extractAuthorizeTokenAdmin(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { tok := r.Header.Get("Authorization") @@ -47,8 +48,35 @@ func (h *Handler) extractAuthorizeTokenAdmin(next nextHTTP) nextHTTP { } } +// loadProvisioner is a middleware that searches for a provisioner +// by name and stores it in the context. +func (h *Handler) loadProvisionerByName(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + ctx := r.Context() + name := chi.URLParam(r, "provisionerName") + var ( + p provisioner.Interface + err error + ) + if p, err = h.auth.LoadProvisionerByName(name); err != nil { + api.WriteError(w, admin.WrapErrorISE(err, "error loading provisioner %s", name)) + return + } + + prov, err := h.adminDB.GetProvisioner(ctx, p.GetID()) + if err != nil { + api.WriteError(w, err) + return + } + + ctx = linkedca.NewContextWithProvisioner(ctx, prov) + next(w, r.WithContext(ctx)) + } +} + // checkAction checks if an action is supported in standalone or not -func (h *Handler) checkAction(next nextHTTP, supportedInStandalone bool) nextHTTP { +func (h *Handler) checkAction(next http.HandlerFunc, supportedInStandalone bool) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // actions allowed in standalone mode are always supported diff --git a/authority/admin/api/middleware_test.go b/authority/admin/api/middleware_test.go index 158374d0..54732dc6 100644 --- a/authority/admin/api/middleware_test.go +++ b/authority/admin/api/middleware_test.go @@ -12,17 +12,19 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "google.golang.org/protobuf/types/known/timestamppb" + + "go.step.sm/linkedca" + "github.com/smallstep/assert" "github.com/smallstep/certificates/authority/admin" - "go.step.sm/linkedca" - "google.golang.org/protobuf/types/known/timestamppb" ) func TestHandler_requireAPIEnabled(t *testing.T) { type test struct { ctx context.Context auth adminAuthority - next nextHTTP + next http.HandlerFunc err *admin.Error statusCode int } @@ -102,7 +104,7 @@ func TestHandler_extractAuthorizeTokenAdmin(t *testing.T) { ctx context.Context auth adminAuthority req *http.Request - next nextHTTP + next http.HandlerFunc err *admin.Error statusCode int } diff --git a/authority/admin/api/policy.go b/authority/admin/api/policy.go index 6b59803f..eb08e38b 100644 --- a/authority/admin/api/policy.go +++ b/authority/admin/api/policy.go @@ -3,14 +3,11 @@ package api import ( "net/http" - "github.com/go-chi/chi" - "go.step.sm/linkedca" "github.com/smallstep/certificates/api" "github.com/smallstep/certificates/api/read" "github.com/smallstep/certificates/authority/admin" - "github.com/smallstep/certificates/authority/provisioner" ) type policyAdminResponderInterface interface { @@ -54,7 +51,7 @@ func (par *PolicyAdminResponder) GetAuthorityPolicy(w http.ResponseWriter, r *ht } if policy == nil { - api.JSONNotFound(w) + api.WriteError(w, admin.NewError(admin.ErrorNotFoundType, "authority policy does not exist")) return } @@ -117,7 +114,7 @@ func (par *PolicyAdminResponder) UpdateAuthorityPolicy(w http.ResponseWriter, r } if policy == nil { - api.JSONNotFound(w) + api.WriteError(w, admin.NewError(admin.ErrorNotFoundType, "authority policy does not exist")) return } @@ -152,7 +149,7 @@ func (par *PolicyAdminResponder) DeleteAuthorityPolicy(w http.ResponseWriter, r } if policy == nil { - api.JSONNotFound(w) + api.WriteError(w, admin.NewError(admin.ErrorNotFoundType, "authority policy does not exist")) return } @@ -167,27 +164,12 @@ func (par *PolicyAdminResponder) DeleteAuthorityPolicy(w http.ResponseWriter, r // GetProvisionerPolicy handles the GET /admin/provisioners/{name}/policy request func (par *PolicyAdminResponder) GetProvisionerPolicy(w http.ResponseWriter, r *http.Request) { - // TODO: move getting provisioner to middleware? - ctx := r.Context() - name := chi.URLParam(r, "name") - var ( - p provisioner.Interface - err error - ) - if p, err = par.auth.LoadProvisionerByName(name); err != nil { - api.WriteError(w, admin.WrapErrorISE(err, "error loading provisioner %s", name)) - return - } - prov, err := par.adminDB.GetProvisioner(ctx, p.GetID()) - if err != nil { - api.WriteError(w, err) - return - } + prov := linkedca.ProvisionerFromContext(r.Context()) policy := prov.GetPolicy() if policy == nil { - api.JSONNotFound(w) + api.WriteError(w, admin.NewError(admin.ErrorNotFoundType, "provisioner policy does not exist")) return } @@ -196,41 +178,28 @@ func (par *PolicyAdminResponder) GetProvisionerPolicy(w http.ResponseWriter, r * // CreateProvisionerPolicy handles the POST /admin/provisioners/{name}/policy request func (par *PolicyAdminResponder) CreateProvisionerPolicy(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - name := chi.URLParam(r, "name") - var ( - p provisioner.Interface - err error - ) - if p, err = par.auth.LoadProvisionerByName(name); err != nil { - api.WriteError(w, admin.WrapErrorISE(err, "error loading provisioner %s", name)) - return - } - prov, err := par.adminDB.GetProvisioner(ctx, p.GetID()) - if err != nil { - api.WriteError(w, err) - return - } + ctx := r.Context() + prov := linkedca.ProvisionerFromContext(ctx) policy := prov.GetPolicy() if policy != nil { - adminErr := admin.NewError(admin.ErrorBadRequestType, "provisioner %s already has a policy", name) + adminErr := admin.NewError(admin.ErrorBadRequestType, "provisioner %s already has a policy", prov.Name) adminErr.Status = http.StatusConflict api.WriteError(w, adminErr) + return } var newPolicy = new(linkedca.Policy) - if err := read.ProtoJSON(r.Body, newPolicy); err != nil { - api.WriteError(w, err) + if !api.ReadProtoJSONWithCheck(w, r.Body, newPolicy) { return } prov.Policy = newPolicy - err = par.auth.UpdateProvisioner(ctx, prov) + err := par.auth.UpdateProvisioner(ctx, prov) if err != nil { - api.WriteError(w, err) + api.WriteError(w, admin.WrapError(admin.ErrorBadRequestType, err, "error creating provisioner policy")) return } @@ -239,88 +208,65 @@ func (par *PolicyAdminResponder) CreateProvisionerPolicy(w http.ResponseWriter, // UpdateProvisionerPolicy handles the PUT /admin/provisioners/{name}/policy request func (par *PolicyAdminResponder) UpdateProvisionerPolicy(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() - name := chi.URLParam(r, "name") - var ( - p provisioner.Interface - err error - ) - if p, err = par.auth.LoadProvisionerByName(name); err != nil { - api.WriteError(w, admin.WrapErrorISE(err, "error loading provisioner %s", name)) + prov := linkedca.ProvisionerFromContext(ctx) + + if prov.Policy == nil { + api.WriteError(w, admin.NewError(admin.ErrorNotFoundType, "provisioner policy does not exist")) return } - prov, err := par.adminDB.GetProvisioner(ctx, p.GetID()) + var newPolicy = new(linkedca.Policy) + if !api.ReadProtoJSONWithCheck(w, r.Body, newPolicy) { + return + } + + prov.Policy = newPolicy + err := par.auth.UpdateProvisioner(ctx, prov) if err != nil { - api.WriteError(w, err) + api.WriteError(w, admin.WrapError(admin.ErrorBadRequestType, err, "error updating provisioner policy")) return } - var policy = new(linkedca.Policy) - if err := read.ProtoJSON(r.Body, policy); err != nil { - api.WriteError(w, err) - return - } - - prov.Policy = policy - err = par.auth.UpdateProvisioner(ctx, prov) - if err != nil { - api.WriteError(w, err) - return - } - - api.ProtoJSONStatus(w, policy, http.StatusOK) + api.ProtoJSONStatus(w, newPolicy, http.StatusOK) } // DeleteProvisionerPolicy handles the DELETE /admin/provisioners/{name}/policy request func (par *PolicyAdminResponder) DeleteProvisionerPolicy(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - name := chi.URLParam(r, "name") - var ( - p provisioner.Interface - err error - ) - if p, err = par.auth.LoadProvisionerByName(name); err != nil { - api.WriteError(w, admin.WrapErrorISE(err, "error loading provisioner %s", name)) - return - } - - prov, err := par.adminDB.GetProvisioner(ctx, p.GetID()) - if err != nil { - api.WriteError(w, err) - return - } + prov := linkedca.ProvisionerFromContext(ctx) if prov.Policy == nil { - api.JSONNotFound(w) + api.WriteError(w, admin.NewError(admin.ErrorNotFoundType, "provisioner policy does not exist")) return } // remove the policy prov.Policy = nil - err = par.auth.UpdateProvisioner(ctx, prov) + err := par.auth.UpdateProvisioner(ctx, prov) if err != nil { api.WriteError(w, err) return } - api.JSON(w, &DeleteResponse{Status: "ok"}) + api.JSONStatus(w, DeleteResponse{Status: "ok"}, http.StatusOK) } func (par *PolicyAdminResponder) GetACMEAccountPolicy(w http.ResponseWriter, r *http.Request) { - api.JSON(w, "ok") + api.JSON(w, "not implemented yet") } func (par *PolicyAdminResponder) CreateACMEAccountPolicy(w http.ResponseWriter, r *http.Request) { - api.JSON(w, "ok") + api.JSON(w, "not implemented yet") } func (par *PolicyAdminResponder) UpdateACMEAccountPolicy(w http.ResponseWriter, r *http.Request) { - api.JSON(w, "ok") + api.JSON(w, "not implemented yet") } func (par *PolicyAdminResponder) DeleteACMEAccountPolicy(w http.ResponseWriter, r *http.Request) { - api.JSON(w, "ok") + api.JSON(w, "not implemented yet") } diff --git a/authority/authority.go b/authority/authority.go index f77cc876..4352bc23 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -218,6 +218,7 @@ func (a *Authority) reloadPolicyEngines(ctx context.Context) error { err error policyOptions *policy.Options ) + // if admin API is enabled, the CA is running in linked mode if a.config.AuthorityConfig.EnableAdmin { linkedPolicy, err := a.adminDB.GetAuthorityPolicy(ctx) if err != nil { diff --git a/authority/policy.go b/authority/policy.go index 4f93899f..f94cd302 100644 --- a/authority/policy.go +++ b/authority/policy.go @@ -77,6 +77,8 @@ func (a *Authority) RemoveAuthorityPolicy(ctx context.Context) error { return nil } +// checkPolicy checks if a new or updated policy configuration results in the user +// locking themselves or other admins out of the CA. func (a *Authority) checkPolicy(ctx context.Context, adm *linkedca.Admin, p *linkedca.Policy) error { // convert the policy; return early if nil @@ -90,9 +92,15 @@ func (a *Authority) checkPolicy(ctx context.Context, adm *linkedca.Admin, p *lin return admin.WrapErrorISE(err, "error creating temporary policy engine") } + // when an empty policy is provided, the resulting engine is nil + // and there's no policy to evaluate. + if engine == nil { + return nil + } + // TODO(hs): Provide option to force the policy, even when the admin subject would be locked out? - sans := []string{adm.Subject} + sans := []string{adm.GetSubject()} if err := isAllowed(engine, sans); err != nil { return err } @@ -151,16 +159,32 @@ func policyToCertificates(p *linkedca.Policy) *authPolicy.Options { // fill x509 policy configuration if p.X509 != nil { if p.X509.Allow != nil { - opts.X509.AllowedNames.DNSDomains = p.X509.Allow.Dns - opts.X509.AllowedNames.IPRanges = p.X509.Allow.Ips - opts.X509.AllowedNames.EmailAddresses = p.X509.Allow.Emails - opts.X509.AllowedNames.URIDomains = p.X509.Allow.Uris + if p.X509.Allow.Dns != nil { + opts.X509.AllowedNames.DNSDomains = p.X509.Allow.Dns + } + if p.X509.Allow.Ips != nil { + opts.X509.AllowedNames.IPRanges = p.X509.Allow.Ips + } + if p.X509.Allow.Emails != nil { + opts.X509.AllowedNames.EmailAddresses = p.X509.Allow.Emails + } + if p.X509.Allow.Uris != nil { + opts.X509.AllowedNames.URIDomains = p.X509.Allow.Uris + } } if p.X509.Deny != nil { - opts.X509.DeniedNames.DNSDomains = p.X509.Deny.Dns - opts.X509.DeniedNames.IPRanges = p.X509.Deny.Ips - opts.X509.DeniedNames.EmailAddresses = p.X509.Deny.Emails - opts.X509.DeniedNames.URIDomains = p.X509.Deny.Uris + if p.X509.Deny.Dns != nil { + opts.X509.DeniedNames.DNSDomains = p.X509.Deny.Dns + } + if p.X509.Deny.Ips != nil { + opts.X509.DeniedNames.IPRanges = p.X509.Deny.Ips + } + if p.X509.Deny.Emails != nil { + opts.X509.DeniedNames.EmailAddresses = p.X509.Deny.Emails + } + if p.X509.Deny.Uris != nil { + opts.X509.DeniedNames.URIDomains = p.X509.Deny.Uris + } } } @@ -168,24 +192,44 @@ func policyToCertificates(p *linkedca.Policy) *authPolicy.Options { if p.Ssh != nil { if p.Ssh.Host != nil { if p.Ssh.Host.Allow != nil { - opts.SSH.Host.AllowedNames.DNSDomains = p.Ssh.Host.Allow.Dns - opts.SSH.Host.AllowedNames.IPRanges = p.Ssh.Host.Allow.Ips - opts.SSH.Host.AllowedNames.EmailAddresses = p.Ssh.Host.Allow.Principals + if p.Ssh.Host.Allow.Dns != nil { + opts.SSH.Host.AllowedNames.DNSDomains = p.Ssh.Host.Allow.Dns + } + if p.Ssh.Host.Allow.Ips != nil { + opts.SSH.Host.AllowedNames.IPRanges = p.Ssh.Host.Allow.Ips + } + if p.Ssh.Host.Allow.Principals != nil { + opts.SSH.Host.AllowedNames.Principals = p.Ssh.Host.Allow.Principals + } } if p.Ssh.Host.Deny != nil { - opts.SSH.Host.DeniedNames.DNSDomains = p.Ssh.Host.Deny.Dns - opts.SSH.Host.DeniedNames.IPRanges = p.Ssh.Host.Deny.Ips - opts.SSH.Host.DeniedNames.Principals = p.Ssh.Host.Deny.Principals + if p.Ssh.Host.Deny.Dns != nil { + opts.SSH.Host.DeniedNames.DNSDomains = p.Ssh.Host.Deny.Dns + } + if p.Ssh.Host.Deny.Ips != nil { + opts.SSH.Host.DeniedNames.IPRanges = p.Ssh.Host.Deny.Ips + } + if p.Ssh.Host.Deny.Principals != nil { + opts.SSH.Host.DeniedNames.Principals = p.Ssh.Host.Deny.Principals + } } } if p.Ssh.User != nil { if p.Ssh.User.Allow != nil { - opts.SSH.User.AllowedNames.EmailAddresses = p.Ssh.User.Allow.Emails - opts.SSH.User.AllowedNames.Principals = p.Ssh.User.Allow.Principals + if p.Ssh.User.Allow.Emails != nil { + opts.SSH.User.AllowedNames.EmailAddresses = p.Ssh.User.Allow.Emails + } + if p.Ssh.User.Allow.Principals != nil { + opts.SSH.User.AllowedNames.Principals = p.Ssh.User.Allow.Principals + } } if p.Ssh.User.Deny != nil { - opts.SSH.User.DeniedNames.EmailAddresses = p.Ssh.User.Deny.Emails - opts.SSH.User.DeniedNames.Principals = p.Ssh.User.Deny.Principals + if p.Ssh.User.Deny.Emails != nil { + opts.SSH.User.DeniedNames.EmailAddresses = p.Ssh.User.Deny.Emails + } + if p.Ssh.User.Deny.Principals != nil { + opts.SSH.User.DeniedNames.Principals = p.Ssh.User.Deny.Principals + } } } } diff --git a/ca/adminClient.go b/ca/adminClient.go index 5f3993b1..f972f9f8 100644 --- a/ca/adminClient.go +++ b/ca/adminClient.go @@ -4,6 +4,7 @@ import ( "bytes" "crypto/x509" "encoding/json" + "fmt" "io" "net/http" "net/url" @@ -679,6 +680,134 @@ retry: return nil } +func (c *AdminClient) GetAuthorityPolicy() (*linkedca.Policy, error) { + var retried bool + u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "policy")}) + tok, err := c.generateAdminToken(u.Path) + if err != nil { + return nil, fmt.Errorf("error generating admin token: %w", err) + } + req, err := http.NewRequest(http.MethodGet, u.String(), http.NoBody) + if err != nil { + return nil, fmt.Errorf("creating GET %s request failed: %w", u, err) + } + req.Header.Add("Authorization", tok) +retry: + resp, err := c.client.Do(req) + if err != nil { + return nil, fmt.Errorf("client GET %s failed: %w", u, err) + } + if resp.StatusCode >= 400 { + if !retried && c.retryOnError(resp) { + retried = true + goto retry + } + return nil, readAdminError(resp.Body) + } + var policy = new(linkedca.Policy) + if err := readProtoJSON(resp.Body, policy); err != nil { + return nil, fmt.Errorf("error reading %s: %w", u, err) + } + return policy, nil +} + +func (c *AdminClient) CreateAuthorityPolicy(p *linkedca.Policy) (*linkedca.Policy, error) { + var retried bool + body, err := protojson.Marshal(p) + if err != nil { + return nil, fmt.Errorf("error marshaling request: %w", err) + } + u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "policy")}) + tok, err := c.generateAdminToken(u.Path) + if err != nil { + return nil, fmt.Errorf("error generating admin token: %w", err) + } + req, err := http.NewRequest(http.MethodPost, u.String(), bytes.NewReader(body)) + if err != nil { + return nil, fmt.Errorf("creating POST %s request failed: %w", u, err) + } + req.Header.Add("Authorization", tok) +retry: + resp, err := c.client.Do(req) + if err != nil { + return nil, fmt.Errorf("client POST %s failed: %w", u, err) + } + if resp.StatusCode >= 400 { + if !retried && c.retryOnError(resp) { + retried = true + goto retry + } + return nil, readAdminError(resp.Body) + } + var policy = new(linkedca.Policy) + if err := readProtoJSON(resp.Body, policy); err != nil { + return nil, fmt.Errorf("error reading %s: %w", u, err) + } + return policy, nil +} + +func (c *AdminClient) UpdateAuthorityPolicy(p *linkedca.Policy) (*linkedca.Policy, error) { + var retried bool + body, err := protojson.Marshal(p) + if err != nil { + return nil, fmt.Errorf("error marshaling request: %w", err) + } + u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "policy")}) + tok, err := c.generateAdminToken(u.Path) + if err != nil { + return nil, fmt.Errorf("error generating admin token: %w", err) + } + req, err := http.NewRequest(http.MethodPut, u.String(), bytes.NewReader(body)) + if err != nil { + return nil, fmt.Errorf("creating PUT %s request failed: %w", u, err) + } + req.Header.Add("Authorization", tok) +retry: + resp, err := c.client.Do(req) + if err != nil { + return nil, fmt.Errorf("client PUT %s failed: %w", u, err) + } + if resp.StatusCode >= 400 { + if !retried && c.retryOnError(resp) { + retried = true + goto retry + } + return nil, readAdminError(resp.Body) + } + var policy = new(linkedca.Policy) + if err := readProtoJSON(resp.Body, policy); err != nil { + return nil, fmt.Errorf("error reading %s: %w", u, err) + } + return policy, nil +} + +func (c *AdminClient) RemoveAuthorityPolicy() error { + var retried bool + u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "policy")}) + tok, err := c.generateAdminToken(u.Path) + if err != nil { + return fmt.Errorf("error generating admin token: %w", err) + } + req, err := http.NewRequest(http.MethodDelete, u.String(), http.NoBody) + if err != nil { + return fmt.Errorf("creating DELETE %s request failed: %w", u, err) + } + req.Header.Add("Authorization", tok) +retry: + resp, err := c.client.Do(req) + if err != nil { + return fmt.Errorf("client DELETE %s failed: %w", u, err) + } + if resp.StatusCode >= 400 { + if !retried && c.retryOnError(resp) { + retried = true + goto retry + } + return readAdminError(resp.Body) + } + return nil +} + func readAdminError(r io.ReadCloser) error { // TODO: not all errors can be read (i.e. 404); seems to be a bigger issue defer r.Close()