package resolver import ( "context" "errors" "fmt" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "edge-infra.dev/pkg/edge/api/graph/mapper" "edge-infra.dev/pkg/edge/api/graph/model" "edge-infra.dev/pkg/edge/api/middleware" "edge-infra.dev/pkg/edge/api/types" "edge-infra.dev/pkg/edge/chariot/client" "edge-infra.dev/pkg/sds/clustersecrets" "edge-infra.dev/pkg/sds/clustersecrets/audit" cc "edge-infra.dev/pkg/sds/clustersecrets/common" ) var ( // ErrBannerOptedOutOfCompliance is thrown when cluster secret services are called // and the banner has opted out of edge security compliance. ErrBannerOptedOutOfCompliance = errors.New("cluster secret lease feature has been turned off as the banner has opted out of edge security compliance") // ErrBannerOptedIntoCompliance is thrown when attempting to update a cluster secret // and the banner has opted into edge security compliance. ErrBannerOptedIntoCompliance = errors.New("updating cluster secrets has been turned off as the banner has opted in to edge security compliance") // ErrInvalidClusterSecretType is returned if the secret type is unknown ErrInvalidClusterSecretType = errors.New("invalid cluster secret type") // ErrLeaseNotObtained is returned if the lease could not be obtained ErrLeaseNotObtained = errors.New("cluster secret lease could not be obtained") // ErrSecretNotRotated is returned if the cluster secret rotation fails ErrSecretNotRotated = errors.New("cluster secret rotation failed") // ErrSecretExpired is returned if the cluster secret expiration time is after now ErrSecretExpired = errors.New("cluster secret expired, please try again shortly") ) // GetBannerEdgeSecurityCompliance gets the edge security compliance set for the banner which the cluster is in func (r *Resolver) GetBannerEdgeSecurityCompliance(ctx context.Context, clusterEdgeID string) (bool, error) { cluster, err := r.StoreClusterService.GetClusterByClusterEdgeID(ctx, clusterEdgeID) if err != nil { return true, err } banner, err := r.BannerService.GetBanner(ctx, cluster.BannerEdgeID) if err != nil { return true, err } return banner.OptInEdgeSecurityCompliance, nil } // updateClusterSecret is the resolver for the updateClusterSecret field. func (r *Resolver) updateClusterSecret(ctx context.Context, clusterEdgeID string, secretType model.ClusterSecretType, secretValue string) (bool, error) { cluster, err := r.StoreClusterService.GetCluster(ctx, clusterEdgeID) if err != nil { return false, err } isSecCompEnabled, err := r.GetBannerEdgeSecurityCompliance(ctx, clusterEdgeID) if err != nil { return false, err } if isSecCompEnabled { return false, ErrBannerOptedIntoCompliance } secretClient, err := r.GCPClientService.GetSecretClient(ctx, cluster.ProjectID) if err != nil { return false, err } // extract username of actor updating cluster secret user := middleware.ForContext(ctx) auditLog := audit.New("api") for _, secret := range clustersecrets.List() { if secret.Type() != secretType { continue } // refresh the secret to use the new plain value if err := secret.Refresh(secretValue); err != nil { return false, err } // apply the cluster secret manager secrets if err := secret.Apply(ctx, secretClient, clusterEdgeID); err != nil { auditLog.Log(secret.Type(), audit.WriteRequest, "username", user.Username, "clusterEdgeID", cluster.ClusterEdgeID, "status", "failed") return false, err } // audit log write request auditLog.Log(secret.Type(), audit.WriteRequest, "username", user.Username, "clusterEdgeID", cluster.ClusterEdgeID, "version", secret.Version()) extSecret, _ := secret.ExternalSecret(cluster.ClusterEdgeID, cluster.ProjectID) // send the external secret object to store cluster externalSecretBase64, err := mapper.ToBase64StringFromExternalSecret(extSecret) if err != nil { return false, err } err = r.sendChariotMessage(ctx, cluster.ProjectID, cluster.ClusterEdgeID, client.Create, externalSecretBase64) return err == nil, err } return false, fmt.Errorf("%w: %s", ErrInvalidClusterSecretType, secretType) } // fetchClusterSecretPlainValue gets the plain value of the secret func (r *Resolver) fetchClusterSecretPlainValue(ctx context.Context, clusterEdgeID string, secretType model.ClusterSecretType, version string) (string, error) { // extract username of actor updating cluster secret user := middleware.ForContext(ctx) auditLog := audit.New("api") // audit log read request auditLog.Log(secretType, audit.ReadRequest, "action_by", user.Username, "clusterEdgeID", clusterEdgeID, "version", version) secret, err := r.fetchClusterSecret(ctx, clusterEdgeID, secretType, version) if err != nil { return "", err } return secret.Plain(), nil } // fetchClusterSecret gets the cluster secret, obtains the lease if edge security compliance is turned on and checks the secret is valid func (r *Resolver) fetchClusterSecret(ctx context.Context, clusterEdgeID string, secretType model.ClusterSecretType, version string) (cc.Secret, error) { secretClient, err := r.getSecretClient(ctx, clusterEdgeID) if err != nil { return nil, err } isSecCompEnabled, err := r.GetBannerEdgeSecurityCompliance(ctx, clusterEdgeID) if err != nil { return nil, err } if isSecCompEnabled { if err := r.obtainClusterSecretLease(ctx, clusterEdgeID, secretType, secretClient); err != nil { return nil, err } } for _, secret := range clustersecrets.List() { if secret.Type() != secretType { continue } fetchErr := secret.Fetch(ctx, secretClient, clusterEdgeID, version) if status.Code(fetchErr) == codes.NotFound { if err := r.handleSecretNotFound(ctx, clusterEdgeID, secret, secretClient); err != nil { return nil, err } } else if status.Code(fetchErr) != codes.OK && !errors.Is(fetchErr, cc.ErrInvalidFormat) { return nil, fetchErr } // rotate and regenerate secrets with invalid formats if errors.Is(fetchErr, cc.ErrInvalidFormat) { if err := r.reformatSecret(ctx, secretClient, secret, clusterEdgeID, fetchErr); err != nil { return nil, err } } return secret, nil } return nil, fmt.Errorf("%w: %s", ErrInvalidClusterSecretType, secretType) } func (r *Resolver) getSecretClient(ctx context.Context, clusterEdgeID string) (types.SecretManagerService, error) { cluster, err := r.StoreClusterService.GetCluster(ctx, clusterEdgeID) if err != nil { return nil, err } secretClient, err := r.GCPClientService.GetSecretClient(ctx, cluster.ProjectID) if err != nil { return nil, err } return secretClient, nil } func (r *Resolver) handleSecretNotFound(ctx context.Context, clusterEdgeID string, secret cc.Secret, secretClient types.SecretManagerService) error { auditLog := audit.New("api") if err := secret.Refresh(""); err != nil { return err } if err := secret.Apply(ctx, secretClient, clusterEdgeID); err != nil { auditLog.Log(secret.Type(), audit.WriteRequest, "action_by", "api", "reason", "registration", "clusterEdgeID", clusterEdgeID) return err } auditLog.Log(secret.Type(), audit.WriteRequest, "action_by", "api", "reason", "registration", "clusterEdgeID", clusterEdgeID, "version", secret.Version()) return nil } // reformatSecret takes an invalid secret and attempts to regenerate it using the existing plain text. // If the plain text is invalid or fails validation, the secret is rotated with a new random plain text credential. func (r *Resolver) reformatSecret(ctx context.Context, sm types.SecretManagerService, secret cc.Secret, clusterEdgeID string, fetchErr error) error { auditLog := audit.New("api") var err error var usedExistingSecretValue bool if errors.Is(fetchErr, cc.ErrSecretExpired) { err = secret.Refresh("") usedExistingSecretValue = false } else { err = secret.Refresh(secret.Plain()) usedExistingSecretValue = true } if err != nil { if err := secret.Refresh(""); err != nil { return err } usedExistingSecretValue = false } if err := secret.Apply(ctx, sm, clusterEdgeID); err != nil { auditLog.Log(secret.Type(), audit.WriteRequest, "action_by", "api", "reason", fetchErr.Error(), "clusterEdgeID", clusterEdgeID, "secretPersisted", usedExistingSecretValue) return err } auditLog.Log(secret.Type(), audit.WriteRequest, "action_by", "api", "reason", fetchErr.Error(), "clusterEdgeID", clusterEdgeID, "version", secret.Version(), "secretPersisted", usedExistingSecretValue) return nil } func (r *Resolver) rotateClusterSecrets(ctx context.Context, secretClient types.SecretManagerService, clusterEdgeID string) error { // rotate both secrets otherwise clusterctl will expire the secret and release the lease in the process for _, secret := range clustersecrets.List() { if err := secret.Fetch(ctx, secretClient, clusterEdgeID, "latest"); err != nil { return err } if err := r.reformatSecret(ctx, secretClient, secret, clusterEdgeID, cc.ErrSecretExpired); err != nil { return err } clusterSecret, err := r.ClusterSecretService.FetchClusterSecret(ctx, clusterEdgeID, secret.Type()) if err != nil { return err } if err := r.ClusterSecretService.UpdateClusterSecret(ctx, clusterSecret.SecretEdgeID, secret.Type(), secret.Version()); err != nil { return err } } return nil } // fetchClusterSecretLease gets the cluster secret lease from the database and returns who owns it, when it expires and what secrets are associated with it func (r *Resolver) fetchClusterSecretLease(ctx context.Context, clusterEdgeID string, secretType model.ClusterSecretType) (*model.ClusterSecretLease, error) { var lease model.ClusterSecretLease isSecCompEnabled, err := r.GetBannerEdgeSecurityCompliance(ctx, clusterEdgeID) if err != nil { return nil, err } if !isSecCompEnabled { return nil, ErrBannerOptedOutOfCompliance } user := middleware.ForContext(ctx) auditLog := audit.New("api") lease, err = r.ClusterSecretService.FetchLease(ctx, clusterEdgeID) if err != nil { return nil, err } auditLog.Log(secretType, audit.ReadClusterSecretLease, "action_by", user.Username, "clusterEdgeID", clusterEdgeID, "fetchClusterSecretLease", lease) return &lease, nil } // removeUserFromClusterSecretLease releases or revokes access to a secret lease for a user and expires the cluster secrets so they are rotated func (r *Resolver) removeUserFromClusterSecretLease(ctx context.Context, clusterEdgeID string, secretType model.ClusterSecretType, username string, removal string) (bool, error) { isSecCompEnabled, err := r.GetBannerEdgeSecurityCompliance(ctx, clusterEdgeID) if err != nil { return false, err } if !isSecCompEnabled { return false, ErrBannerOptedOutOfCompliance } user := middleware.ForContext(ctx) auditLog := audit.New("api") if removal == "release" { if err := r.ClusterSecretService.ReleaseLease(ctx, clusterEdgeID); err != nil { return false, err } auditLog.Log(secretType, audit.ReleaseBreakoutSession, "action_by", user.Username, "clusterEdgeID", clusterEdgeID, "releaseClusterSecretLease", true) } if removal == "revoke" { if err := r.ClusterSecretService.RevokeLease(ctx, clusterEdgeID, username); err != nil { return false, err } auditLog.Log(secretType, audit.RevokeBreakoutSession, "action_by", user.Username, "clusterEdgeID", clusterEdgeID, "revokeClusterSecretLease", true, "user_revoked", username) } leaseID, err := r.ClusterSecretService.FetchLeaseID(ctx, clusterEdgeID) if err != nil { return false, err } if err := r.ClusterSecretService.ExpireClusterSecrets(ctx, leaseID); err != nil { return false, err } return true, nil } // fetchClusterSecretVersions gets the current version and expiry time of a secret func (r *Resolver) fetchClusterSecretVersions(ctx context.Context, clusterEdgeID string, secretType model.ClusterSecretType) ([]*model.ClusterSecretVersionInfo, error) { user := middleware.ForContext(ctx) auditLog := audit.New("api") versions, err := r.ClusterSecretService.FetchClusterSecretVersions(ctx, clusterEdgeID, secretType) if err != nil { return nil, err } auditLog.Log(secretType, audit.GetClusterSecretVersions, "action_by", user.Username, "clusterEdgeID", clusterEdgeID, "fetchClusterSecretVersions", versions) return versions, nil } // obtainClusterSecretLease sets the lease username to the user that has called it func (r *Resolver) obtainClusterSecretLease(ctx context.Context, clusterEdgeID string, secretType model.ClusterSecretType, secretClient types.SecretManagerService) error { user := middleware.ForContext(ctx) auditLog := audit.New("api") rotateSecret, err := r.ClusterSecretService.ObtainLease(ctx, clusterEdgeID) if err != nil { return err } // rotate the secret if the lease is new but still had the old owner attached, i.e. clusterctl hasn't reconciled yet if rotateSecret { if err := r.rotateClusterSecrets(ctx, secretClient, clusterEdgeID); err != nil { return err } } auditLog.Log(secretType, audit.NewBreakoutSession, "action_by", user.Username, "clusterEdgeID", clusterEdgeID, "obtainClusterSecretLease", true) return nil }