From 828b76880f3b23a44b1f5afa1b23f4186917bb53 Mon Sep 17 00:00:00 2001 From: Mark Laing Date: Mon, 26 Feb 2024 17:46:34 +0000 Subject: [PATCH 1/6] shared/api: Add lifecycle events for identity create/update. Signed-off-by: Mark Laing --- shared/api/event_lifecycle.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/shared/api/event_lifecycle.go b/shared/api/event_lifecycle.go index 4e3a5fe5c142..e51c7b376ad4 100644 --- a/shared/api/event_lifecycle.go +++ b/shared/api/event_lifecycle.go @@ -119,4 +119,6 @@ const ( EventLifecycleWarningAcknowledged = "warning-acknowledged" EventLifecycleWarningDeleted = "warning-deleted" EventLifecycleWarningReset = "warning-reset" + EventLifecycleIdentityCreated = "identity-created" + EventLifecycleIdentityUpdated = "identity-updated" ) From 87f61504e1e8647dcc0c7c32e4bb0fe2e41ee5a5 Mon Sep 17 00:00:00 2001 From: Mark Laing Date: Mon, 26 Feb 2024 17:47:13 +0000 Subject: [PATCH 2/6] lxd/lifecycle: Add lifecycle actions for identity create/update. Signed-off-by: Mark Laing --- lxd/lifecycle/identity.go | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 lxd/lifecycle/identity.go diff --git a/lxd/lifecycle/identity.go b/lxd/lifecycle/identity.go new file mode 100644 index 000000000000..888a7aa007f8 --- /dev/null +++ b/lxd/lifecycle/identity.go @@ -0,0 +1,27 @@ +package lifecycle + +import ( + "github.com/canonical/lxd/shared/api" + "github.com/canonical/lxd/shared/version" +) + +// IdentityAction represents a lifecycle event action for identities. +type IdentityAction string + +// All supported lifecycle events for identities. +const ( + IdentityCreated = IdentityAction(api.EventLifecycleIdentityCreated) + IdentityUpdated = IdentityAction(api.EventLifecycleIdentityUpdated) +) + +// Event creates the lifecycle event for an action on a Certificate. +func (a IdentityAction) Event(authenticationMethod string, identifier string, requestor *api.EventLifecycleRequestor, ctx map[string]any) api.EventLifecycle { + u := api.NewURL().Path(version.APIVersion, "auth", "identities", authenticationMethod, identifier) + + return api.EventLifecycle{ + Action: string(a), + Source: u.String(), + Context: ctx, + Requestor: requestor, + } +} From 72ed94b70ec13f7bcf5e4ed83cb887b6830e7a45 Mon Sep 17 00:00:00 2001 From: Mark Laing Date: Mon, 26 Feb 2024 17:47:47 +0000 Subject: [PATCH 3/6] lxd: Add internal endpoint for identity cache refresh. Signed-off-by: Mark Laing --- lxd/api_internal.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lxd/api_internal.go b/lxd/api_internal.go index b674f1152fed..bc499dfdac0c 100644 --- a/lxd/api_internal.go +++ b/lxd/api_internal.go @@ -62,6 +62,7 @@ var apiInternal = []APIEndpoint{ internalShutdownCmd, internalSQLCmd, internalWarningCreateCmd, + internalIdentityCacheRefreshCmd, } var internalShutdownCmd = APIEndpoint{ @@ -137,6 +138,12 @@ var internalBGPStateCmd = APIEndpoint{ Get: APIEndpointAction{Handler: internalBGPState, AccessHandler: allowPermission(entity.TypeServer, auth.EntitlementCanEdit)}, } +var internalIdentityCacheRefreshCmd = APIEndpoint{ + Path: "identity-cache-refresh", + + Post: APIEndpointAction{Handler: internalIdentityCacheRefresh, AccessHandler: allowPermission(entity.TypeServer, auth.EntitlementCanEdit)}, +} + type internalImageOptimizePost struct { Image api.Image `json:"image" yaml:"image"` Pool string `json:"pool" yaml:"pool"` @@ -1083,3 +1090,9 @@ func internalBGPState(d *Daemon, r *http.Request) response.Response { return response.SyncResponse(true, s.BGP.Debug()) } + +func internalIdentityCacheRefresh(d *Daemon, r *http.Request) response.Response { + logger.Debug("Received identity cache update notification - refreshing cache") + d.State().UpdateIdentityCache() + return response.EmptySyncResponse +} From ce044cc833e47534eff1d8ba6a2cc66c65b40715 Mon Sep 17 00:00:00 2001 From: Mark Laing Date: Mon, 26 Feb 2024 17:48:44 +0000 Subject: [PATCH 4/6] lxd: Notify cluster members of new or updated identities. Signed-off-by: Mark Laing --- lxd/daemon.go | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/lxd/daemon.go b/lxd/daemon.go index 50f2624abfc3..cc6e57e369a4 100644 --- a/lxd/daemon.go +++ b/lxd/daemon.go @@ -25,6 +25,7 @@ import ( liblxc "github.com/lxc/go-lxc" "golang.org/x/sys/unix" + "github.com/canonical/lxd/client" "github.com/canonical/lxd/lxd/acme" "github.com/canonical/lxd/lxd/apparmor" "github.com/canonical/lxd/lxd/auth" @@ -46,6 +47,7 @@ import ( "github.com/canonical/lxd/lxd/instance" instanceDrivers "github.com/canonical/lxd/lxd/instance/drivers" "github.com/canonical/lxd/lxd/instance/instancetype" + "github.com/canonical/lxd/lxd/lifecycle" "github.com/canonical/lxd/lxd/loki" "github.com/canonical/lxd/lxd/maas" networkZone "github.com/canonical/lxd/lxd/network/zone" @@ -363,7 +365,7 @@ func (d *Daemon) Authenticate(w http.ResponseWriter, r *http.Request) (trusted b return false, "", "", nil, fmt.Errorf("Failed OIDC Authentication: %w", err) } - err = d.handleOIDCAuthenticationResult(result) + err = d.handleOIDCAuthenticationResult(r, result) if err != nil { return false, "", "", nil, fmt.Errorf("Failed to process OIDC authentication result: %w", err) } @@ -398,7 +400,9 @@ func (d *Daemon) Authenticate(w http.ResponseWriter, r *http.Request) (trusted b // handleOIDCAuthenticationResult checks the identity cache for the OIDC identity by their email address. If no identity // is found, an identity is added with that email. If an identity is found but the OIDC subject is different to the // expected value, the identity is updated with the new subject. -func (d *Daemon) handleOIDCAuthenticationResult(result *oidc.AuthenticationResult) error { +func (d *Daemon) handleOIDCAuthenticationResult(r *http.Request, result *oidc.AuthenticationResult) error { + var action lifecycle.IdentityAction + id, err := d.identityCache.Get(api.AuthenticationMethodOIDC, result.Email) if err != nil && !api.StatusErrorCheck(err, http.StatusNotFound) { return fmt.Errorf("Failed getting OIDC identity from cache: %w", err) @@ -424,7 +428,7 @@ func (d *Daemon) handleOIDCAuthenticationResult(result *oidc.AuthenticationResul return fmt.Errorf("Failed to add new OIDC identity to database: %w", err) } - updateIdentityCache(d) + action = lifecycle.IdentityCreated } else if id.Subject != result.Subject || id.Name != result.Name { // The OIDC subject of the user with this email address has changed (this should be rare). Replace the // subject in the identity metadata and refresh the cache. @@ -447,7 +451,29 @@ func (d *Daemon) handleOIDCAuthenticationResult(result *oidc.AuthenticationResul return fmt.Errorf("Failed to update OIDC identity information: %w", err) } - updateIdentityCache(d) + action = lifecycle.IdentityUpdated + } + + if action != "" { + // Notify other nodes about the new identity. + s := d.State() + notifier, err := cluster.NewNotifier(s, s.Endpoints.NetworkCert(), s.ServerCert(), cluster.NotifyAlive) + if err != nil { + return fmt.Errorf("Failed to notify cluster members of new or updated OIDC identity: %w", err) + } + + err = notifier(func(client lxd.InstanceServer) error { + _, _, err := client.RawQuery(http.MethodPost, "/internal/identity-cache-refresh", nil, "") + return err + }) + if err != nil { + return fmt.Errorf("Failed to notify cluster members of new or updated OIDC identity: %w", err) + } + + lc := action.Event(api.AuthenticationMethodOIDC, result.Email, request.CreateRequestor(r), nil) + s.Events.SendLifecycle(api.ProjectDefaultName, lc) + + s.UpdateIdentityCache() } return nil From 613fb2c3f22dc7e45cc7ff4fede0140b7955f368 Mon Sep 17 00:00:00 2001 From: Mark Laing Date: Tue, 27 Feb 2024 10:56:27 +0000 Subject: [PATCH 5/6] lxc: Remove unnecessary client import alias. Signed-off-by: Mark Laing --- lxc/storage_volume.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lxc/storage_volume.go b/lxc/storage_volume.go index 27fa3589191f..727d1c119711 100644 --- a/lxc/storage_volume.go +++ b/lxc/storage_volume.go @@ -14,7 +14,7 @@ import ( "github.com/spf13/cobra" "gopkg.in/yaml.v2" - lxd "github.com/canonical/lxd/client" + "github.com/canonical/lxd/client" "github.com/canonical/lxd/shared" "github.com/canonical/lxd/shared/api" cli "github.com/canonical/lxd/shared/cmd" From 350469edfc2c3f0511513c3b974d26e13fccd8d3 Mon Sep 17 00:00:00 2001 From: Mark Laing Date: Tue, 27 Feb 2024 13:12:57 +0000 Subject: [PATCH 6/6] lxd: Use internal endpoint to refresh cache on certificate change. Signed-off-by: Mark Laing --- lxd/certificates.go | 416 ++++++++++++++++++++++---------------------- 1 file changed, 204 insertions(+), 212 deletions(-) diff --git a/lxd/certificates.go b/lxd/certificates.go index 7c90b5931e63..1340233ae6a1 100644 --- a/lxd/certificates.go +++ b/lxd/certificates.go @@ -20,7 +20,6 @@ import ( "github.com/canonical/lxd/lxd/certificate" "github.com/canonical/lxd/lxd/cluster" clusterConfig "github.com/canonical/lxd/lxd/cluster/config" - clusterRequest "github.com/canonical/lxd/lxd/cluster/request" "github.com/canonical/lxd/lxd/db" dbCluster "github.com/canonical/lxd/lxd/db/cluster" "github.com/canonical/lxd/lxd/db/operationtype" @@ -601,59 +600,58 @@ func certificatesPost(d *Daemon, r *http.Request) response.Response { } } - if !isClusterNotification(r) { - err := s.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error { - // Check if we already have the certificate. - existingCert, _ := dbCluster.GetCertificateByFingerprintPrefix(ctx, tx.Tx(), fingerprint) - if existingCert != nil { - return api.StatusErrorf(http.StatusConflict, "Certificate already in trust store") - } - - // Store the certificate in the cluster database. - dbCert := dbCluster.Certificate{ - Fingerprint: shared.CertFingerprint(cert), - Type: dbReqType, - Name: name, - Certificate: string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw})), - Restricted: req.Restricted, - } - - _, err := dbCluster.CreateCertificateWithProjects(ctx, tx.Tx(), dbCert, req.Projects) - return err - }) - if err != nil { - return response.SmartError(err) + err = s.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error { + // Check if we already have the certificate. + existingCert, _ := dbCluster.GetCertificateByFingerprintPrefix(ctx, tx.Tx(), fingerprint) + if existingCert != nil { + return api.StatusErrorf(http.StatusConflict, "Certificate already in trust store") } - // Notify other nodes about the new certificate. - notifier, err := cluster.NewNotifier(s, s.Endpoints.NetworkCert(), s.ServerCert(), cluster.NotifyAlive) - if err != nil { - return response.SmartError(err) + // Store the certificate in the cluster database. + dbCert := dbCluster.Certificate{ + Fingerprint: shared.CertFingerprint(cert), + Type: dbReqType, + Name: name, + Certificate: string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw})), + Restricted: req.Restricted, } - req := api.CertificatesPost{ - CertificatePut: api.CertificatePut{ - Certificate: base64.StdEncoding.EncodeToString(cert.Raw), - Name: name, - Type: api.CertificateTypeClient, - }, - } + _, err := dbCluster.CreateCertificateWithProjects(ctx, tx.Tx(), dbCert, req.Projects) + return err + }) + if err != nil { + return response.SmartError(err) + } - err = notifier(func(client lxd.InstanceServer) error { - return client.CreateCertificate(req) - }) - if err != nil { - return response.SmartError(err) - } + // Send a notification to other cluster members to refresh their identity cache. + notifier, err := cluster.NewNotifier(s, s.Endpoints.NetworkCert(), s.ServerCert(), cluster.NotifyAlive) + if err != nil { + return response.SmartError(err) + } - // Add the certificate resource to the authorizer. - err = s.Authorizer.AddCertificate(r.Context(), fingerprint) - if err != nil { - logger.Error("Failed to add certificate to authorizer", logger.Ctx{"fingerprint": fingerprint, "error": err}) - } + req = api.CertificatesPost{ + CertificatePut: api.CertificatePut{ + Certificate: base64.StdEncoding.EncodeToString(cert.Raw), + Name: name, + Type: api.CertificateTypeClient, + }, } - // Reload the cache. + err = notifier(func(client lxd.InstanceServer) error { + _, _, err := client.RawQuery(http.MethodPost, "/internal/identity-cache-refresh", nil, "") + return err + }) + if err != nil { + return response.SmartError(err) + } + + // Add the certificate resource to the authorizer. + err = s.Authorizer.AddCertificate(r.Context(), fingerprint) + if err != nil { + logger.Error("Failed to add certificate to authorizer", logger.Ctx{"fingerprint": fingerprint, "error": err}) + } + + // Reload the identity cache to add the new certificate. s.UpdateIdentityCache() lc := lifecycle.CertificateCreated.Event(fingerprint, request.CreateRequestor(r), nil) @@ -788,10 +786,8 @@ func certificatePut(d *Daemon, r *http.Request) response.Response { return response.BadRequest(err) } - clientType := clusterRequest.UserAgentClientType(r.Header.Get("User-Agent")) - // Apply the update. - return doCertificateUpdate(d, *apiEntry, req, clientType, r) + return doCertificateUpdate(d, *apiEntry, req, r) } // swagger:operation PATCH /1.0/certificates/{fingerprint} certificates certificate_patch @@ -857,136 +853,133 @@ func certificatePatch(d *Daemon, r *http.Request) response.Response { return response.BadRequest(err) } - clientType := clusterRequest.UserAgentClientType(r.Header.Get("User-Agent")) - - return doCertificateUpdate(d, *apiEntry, req.Writable(), clientType, r) + return doCertificateUpdate(d, *apiEntry, req.Writable(), r) } -func doCertificateUpdate(d *Daemon, dbInfo api.Certificate, req api.CertificatePut, clientType clusterRequest.ClientType, r *http.Request) response.Response { +func doCertificateUpdate(d *Daemon, dbInfo api.Certificate, req api.CertificatePut, r *http.Request) response.Response { s := d.State() - if clientType == clusterRequest.ClientTypeNormal { - reqDBType, err := certificate.FromAPIType(req.Type) - if err != nil { - return response.BadRequest(err) - } + reqDBType, err := certificate.FromAPIType(req.Type) + if err != nil { + return response.BadRequest(err) + } - // Convert to the database type. - dbCert := dbCluster.Certificate{ - Certificate: dbInfo.Certificate, - Fingerprint: dbInfo.Fingerprint, - Restricted: req.Restricted, - Name: req.Name, - Type: reqDBType, - } + // Convert to the database type. + dbCert := dbCluster.Certificate{ + Certificate: dbInfo.Certificate, + Fingerprint: dbInfo.Fingerprint, + Restricted: req.Restricted, + Name: req.Name, + Type: reqDBType, + } - var userCanEditCertificate bool - err = s.Authorizer.CheckPermission(r.Context(), r, entity.CertificateURL(dbInfo.Fingerprint), auth.EntitlementCanEdit) - if err == nil { - userCanEditCertificate = true - } else if !api.StatusErrorCheck(err, http.StatusForbidden) { - return response.SmartError(err) + var userCanEditCertificate bool + err = s.Authorizer.CheckPermission(r.Context(), r, entity.CertificateURL(dbInfo.Fingerprint), auth.EntitlementCanEdit) + if err == nil { + userCanEditCertificate = true + } else if !api.StatusErrorCheck(err, http.StatusForbidden) { + return response.SmartError(err) + } + + // Non-admins are able to change their own certificate but no other fields. + // In order to prevent possible future security issues, the certificate information is + // reset in case a non-admin user is performing the update. + certProjects := req.Projects + if !userCanEditCertificate { + if r.TLS == nil { + response.Forbidden(fmt.Errorf("Cannot update certificate information")) } - // Non-admins are able to change their own certificate but no other fields. - // In order to prevent possible future security issues, the certificate information is - // reset in case a non-admin user is performing the update. - certProjects := req.Projects - if !userCanEditCertificate { - if r.TLS == nil { - response.Forbidden(fmt.Errorf("Cannot update certificate information")) - } + // Ensure the user in not trying to change fields other than the certificate. + if dbInfo.Restricted != req.Restricted || dbInfo.Name != req.Name || len(dbInfo.Projects) != len(req.Projects) { + return response.Forbidden(fmt.Errorf("Only the certificate can be changed")) + } - // Ensure the user in not trying to change fields other than the certificate. - if dbInfo.Restricted != req.Restricted || dbInfo.Name != req.Name || len(dbInfo.Projects) != len(req.Projects) { + for i := 0; i < len(dbInfo.Projects); i++ { + if dbInfo.Projects[i] != req.Projects[i] { return response.Forbidden(fmt.Errorf("Only the certificate can be changed")) } + } - for i := 0; i < len(dbInfo.Projects); i++ { - if dbInfo.Projects[i] != req.Projects[i] { - return response.Forbidden(fmt.Errorf("Only the certificate can be changed")) - } - } - - // Reset dbCert in order to prevent possible future security issues. - dbCert = dbCluster.Certificate{ - Certificate: dbInfo.Certificate, - Fingerprint: dbInfo.Fingerprint, - Restricted: dbInfo.Restricted, - Name: dbInfo.Name, - Type: reqDBType, - } - - certProjects = dbInfo.Projects - - if req.Certificate != "" && dbInfo.Certificate != req.Certificate { - certBlock, _ := pem.Decode([]byte(dbInfo.Certificate)) - - oldCert, err := x509.ParseCertificate(certBlock.Bytes) - if err != nil { - // This should not happen - return response.InternalError(err) - } + // Reset dbCert in order to prevent possible future security issues. + dbCert = dbCluster.Certificate{ + Certificate: dbInfo.Certificate, + Fingerprint: dbInfo.Fingerprint, + Restricted: dbInfo.Restricted, + Name: dbInfo.Name, + Type: reqDBType, + } - trustedCerts := map[string]x509.Certificate{ - dbInfo.Name: *oldCert, - } + certProjects = dbInfo.Projects - trusted := false - for _, i := range r.TLS.PeerCertificates { - trusted, _ = util.CheckTrustState(*i, trustedCerts, s.Endpoints.NetworkCert(), false) + if req.Certificate != "" && dbInfo.Certificate != req.Certificate { + certBlock, _ := pem.Decode([]byte(dbInfo.Certificate)) - if trusted { - break - } - } + oldCert, err := x509.ParseCertificate(certBlock.Bytes) + if err != nil { + // This should not happen + return response.InternalError(err) + } - if !trusted { - return response.Forbidden(fmt.Errorf("Certificate cannot be changed")) - } + trustedCerts := map[string]x509.Certificate{ + dbInfo.Name: *oldCert, } - } - if req.Certificate != "" && dbInfo.Certificate != req.Certificate { - // Add supplied certificate. - block, _ := pem.Decode([]byte(req.Certificate)) + trusted := false + for _, i := range r.TLS.PeerCertificates { + trusted, _ = util.CheckTrustState(*i, trustedCerts, s.Endpoints.NetworkCert(), false) - cert, err := x509.ParseCertificate(block.Bytes) - if err != nil { - return response.BadRequest(fmt.Errorf("Invalid certificate material: %w", err)) + if trusted { + break + } } - dbCert.Certificate = string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw})) - dbCert.Fingerprint = shared.CertFingerprint(cert) - - // Check validity. - err = certificateValidate(cert) - if err != nil { - return response.BadRequest(err) + if !trusted { + return response.Forbidden(fmt.Errorf("Certificate cannot be changed")) } } + } - // Update the database record. - err = s.DB.UpdateCertificate(context.Background(), dbInfo.Fingerprint, dbCert, certProjects) - if err != nil { - return response.SmartError(err) - } + if req.Certificate != "" && dbInfo.Certificate != req.Certificate { + // Add supplied certificate. + block, _ := pem.Decode([]byte(req.Certificate)) - // Notify other nodes about the new certificate. - notifier, err := cluster.NewNotifier(s, s.Endpoints.NetworkCert(), s.ServerCert(), cluster.NotifyAlive) + cert, err := x509.ParseCertificate(block.Bytes) if err != nil { - return response.SmartError(err) + return response.BadRequest(fmt.Errorf("Invalid certificate material: %w", err)) } - err = notifier(func(client lxd.InstanceServer) error { - return client.UpdateCertificate(dbCert.Fingerprint, req, "") - }) + dbCert.Certificate = string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw})) + dbCert.Fingerprint = shared.CertFingerprint(cert) + + // Check validity. + err = certificateValidate(cert) if err != nil { - return response.SmartError(err) + return response.BadRequest(err) } } - // Reload the cache. + // Update the database record. + err = s.DB.UpdateCertificate(context.Background(), dbInfo.Fingerprint, dbCert, certProjects) + if err != nil { + return response.SmartError(err) + } + + // Notify other cluster members to update their identity cache. + notifier, err := cluster.NewNotifier(s, s.Endpoints.NetworkCert(), s.ServerCert(), cluster.NotifyAlive) + if err != nil { + return response.SmartError(err) + } + + err = notifier(func(client lxd.InstanceServer) error { + _, _, err := client.RawQuery(http.MethodPost, "/internal/identity-cache-refresh", nil, "") + return err + }) + if err != nil { + return response.SmartError(err) + } + + // Reload the identity cache. s.UpdateIdentityCache() s.Events.SendLifecycle(api.ProjectDefaultName, lifecycle.CertificateUpdated.Event(dbInfo.Fingerprint, request.CreateRequestor(r), nil)) @@ -1020,88 +1013,87 @@ func certificateDelete(d *Daemon, r *http.Request) response.Response { return response.SmartError(err) } - if !isClusterNotification(r) { - var certInfo *dbCluster.Certificate - err := s.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error { - // Get current database record. - var err error - certInfo, err = dbCluster.GetCertificateByFingerprintPrefix(ctx, tx.Tx(), fingerprint) - if err != nil { - return err - } - - return nil - }) + var certInfo *dbCluster.Certificate + err = s.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error { + // Get current database record. + var err error + certInfo, err = dbCluster.GetCertificateByFingerprintPrefix(ctx, tx.Tx(), fingerprint) if err != nil { - return response.SmartError(err) + return err } - var userCanEditCertificate bool - err = s.Authorizer.CheckPermission(r.Context(), r, entity.CertificateURL(certInfo.Fingerprint), auth.EntitlementCanDelete) - if err == nil { - userCanEditCertificate = true - } else if api.StatusErrorCheck(err, http.StatusForbidden) { - return response.SmartError(err) - } + return nil + }) + if err != nil { + return response.SmartError(err) + } - // Non-admins are able to delete only their own certificate. - if !userCanEditCertificate { - if r.TLS == nil { - response.Forbidden(fmt.Errorf("Cannot delete certificate")) - } + var userCanEditCertificate bool + err = s.Authorizer.CheckPermission(r.Context(), r, entity.CertificateURL(certInfo.Fingerprint), auth.EntitlementCanDelete) + if err == nil { + userCanEditCertificate = true + } else if api.StatusErrorCheck(err, http.StatusForbidden) { + return response.SmartError(err) + } - certBlock, _ := pem.Decode([]byte(certInfo.Certificate)) + // Non-admins are able to delete only their own certificate. + if !userCanEditCertificate { + if r.TLS == nil { + response.Forbidden(fmt.Errorf("Cannot delete certificate")) + } - cert, err := x509.ParseCertificate(certBlock.Bytes) - if err != nil { - // This should not happen - return response.InternalError(err) - } + certBlock, _ := pem.Decode([]byte(certInfo.Certificate)) - trustedCerts := map[string]x509.Certificate{ - certInfo.Name: *cert, - } + cert, err := x509.ParseCertificate(certBlock.Bytes) + if err != nil { + // This should not happen + return response.InternalError(err) + } - trusted := false - for _, i := range r.TLS.PeerCertificates { - trusted, _ = util.CheckTrustState(*i, trustedCerts, s.Endpoints.NetworkCert(), false) + trustedCerts := map[string]x509.Certificate{ + certInfo.Name: *cert, + } - if trusted { - break - } - } + trusted := false + for _, i := range r.TLS.PeerCertificates { + trusted, _ = util.CheckTrustState(*i, trustedCerts, s.Endpoints.NetworkCert(), false) - if !trusted { - return response.Forbidden(fmt.Errorf("Certificate cannot be deleted")) + if trusted { + break } } - err = s.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error { - // Perform the delete with the expanded fingerprint. - return dbCluster.DeleteCertificate(ctx, tx.Tx(), certInfo.Fingerprint) - }) - if err != nil { - return response.SmartError(err) + if !trusted { + return response.Forbidden(fmt.Errorf("Certificate cannot be deleted")) } + } - // Notify other nodes about the new certificate. - notifier, err := cluster.NewNotifier(s, s.Endpoints.NetworkCert(), s.ServerCert(), cluster.NotifyAlive) - if err != nil { - return response.SmartError(err) - } + err = s.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error { + // Perform the delete with the expanded fingerprint. + return dbCluster.DeleteCertificate(ctx, tx.Tx(), certInfo.Fingerprint) + }) + if err != nil { + return response.SmartError(err) + } - err = notifier(func(client lxd.InstanceServer) error { - return client.DeleteCertificate(certInfo.Fingerprint) - }) - if err != nil { - return response.SmartError(err) - } + // Notify other cluster members so that they update their identity cache. + notifier, err := cluster.NewNotifier(s, s.Endpoints.NetworkCert(), s.ServerCert(), cluster.NotifyAlive) + if err != nil { + return response.SmartError(err) + } - // Remove the certificate from the authorizer. - err = s.Authorizer.DeleteCertificate(r.Context(), certInfo.Fingerprint) - if err != nil { - logger.Error("Failed to remove certificate from authorizer", logger.Ctx{"fingerprint": certInfo.Fingerprint, "error": err}) - } + err = notifier(func(client lxd.InstanceServer) error { + _, _, err := client.RawQuery(http.MethodPost, "/internal/identity-cache-refresh", nil, "") + return err + }) + if err != nil { + return response.SmartError(err) + } + + // Remove the certificate from the authorizer. + err = s.Authorizer.DeleteCertificate(r.Context(), certInfo.Fingerprint) + if err != nil { + logger.Error("Failed to remove certificate from authorizer", logger.Ctx{"fingerprint": certInfo.Fingerprint, "error": err}) } // Reload the cache.