...

Source file src/edge-infra.dev/pkg/edge/api/graph/resolver/cluster_secrets.go

Documentation: edge-infra.dev/pkg/edge/api/graph/resolver

     1  package resolver
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  
     8  	"google.golang.org/grpc/codes"
     9  	"google.golang.org/grpc/status"
    10  
    11  	"edge-infra.dev/pkg/edge/api/graph/mapper"
    12  	"edge-infra.dev/pkg/edge/api/graph/model"
    13  	"edge-infra.dev/pkg/edge/api/middleware"
    14  	"edge-infra.dev/pkg/edge/api/types"
    15  	"edge-infra.dev/pkg/edge/chariot/client"
    16  	"edge-infra.dev/pkg/sds/clustersecrets"
    17  	"edge-infra.dev/pkg/sds/clustersecrets/audit"
    18  	cc "edge-infra.dev/pkg/sds/clustersecrets/common"
    19  )
    20  
    21  var (
    22  	// ErrBannerOptedOutOfCompliance is thrown when cluster secret services are called
    23  	// and the banner has opted out of edge security compliance.
    24  	ErrBannerOptedOutOfCompliance = errors.New("cluster secret lease feature has been turned off as the banner has opted out of edge security compliance")
    25  	// ErrBannerOptedIntoCompliance is thrown when attempting to update a cluster secret
    26  	// and the banner has opted into edge security compliance.
    27  	ErrBannerOptedIntoCompliance = errors.New("updating cluster secrets has been turned off as the banner has opted in to edge security compliance")
    28  	// ErrInvalidClusterSecretType is returned if the secret type is unknown
    29  	ErrInvalidClusterSecretType = errors.New("invalid cluster secret type")
    30  	// ErrLeaseNotObtained is returned if the lease could not be obtained
    31  	ErrLeaseNotObtained = errors.New("cluster secret lease could not be obtained")
    32  	// ErrSecretNotRotated is returned if the cluster secret rotation fails
    33  	ErrSecretNotRotated = errors.New("cluster secret rotation failed")
    34  	// ErrSecretExpired is returned if the cluster secret expiration time is after now
    35  	ErrSecretExpired = errors.New("cluster secret expired, please try again shortly")
    36  )
    37  
    38  // GetBannerEdgeSecurityCompliance gets the edge security compliance set for the banner which the cluster is in
    39  func (r *Resolver) GetBannerEdgeSecurityCompliance(ctx context.Context, clusterEdgeID string) (bool, error) {
    40  	cluster, err := r.StoreClusterService.GetClusterByClusterEdgeID(ctx, clusterEdgeID)
    41  	if err != nil {
    42  		return true, err
    43  	}
    44  	banner, err := r.BannerService.GetBanner(ctx, cluster.BannerEdgeID)
    45  	if err != nil {
    46  		return true, err
    47  	}
    48  	return banner.OptInEdgeSecurityCompliance, nil
    49  }
    50  
    51  // updateClusterSecret is the resolver for the updateClusterSecret field.
    52  func (r *Resolver) updateClusterSecret(ctx context.Context, clusterEdgeID string, secretType model.ClusterSecretType, secretValue string) (bool, error) {
    53  	cluster, err := r.StoreClusterService.GetCluster(ctx, clusterEdgeID)
    54  	if err != nil {
    55  		return false, err
    56  	}
    57  
    58  	isSecCompEnabled, err := r.GetBannerEdgeSecurityCompliance(ctx, clusterEdgeID)
    59  	if err != nil {
    60  		return false, err
    61  	}
    62  	if isSecCompEnabled {
    63  		return false, ErrBannerOptedIntoCompliance
    64  	}
    65  
    66  	secretClient, err := r.GCPClientService.GetSecretClient(ctx, cluster.ProjectID)
    67  	if err != nil {
    68  		return false, err
    69  	}
    70  
    71  	// extract username of actor updating cluster secret
    72  	user := middleware.ForContext(ctx)
    73  	auditLog := audit.New("api")
    74  	for _, secret := range clustersecrets.List() {
    75  		if secret.Type() != secretType {
    76  			continue
    77  		}
    78  
    79  		// refresh the secret to use the new plain value
    80  		if err := secret.Refresh(secretValue); err != nil {
    81  			return false, err
    82  		}
    83  
    84  		// apply the cluster secret manager secrets
    85  		if err := secret.Apply(ctx, secretClient, clusterEdgeID); err != nil {
    86  			auditLog.Log(secret.Type(), audit.WriteRequest, "username", user.Username, "clusterEdgeID", cluster.ClusterEdgeID, "status", "failed")
    87  			return false, err
    88  		}
    89  
    90  		// audit log write request
    91  		auditLog.Log(secret.Type(), audit.WriteRequest, "username", user.Username, "clusterEdgeID", cluster.ClusterEdgeID, "version", secret.Version())
    92  
    93  		extSecret, _ := secret.ExternalSecret(cluster.ClusterEdgeID, cluster.ProjectID)
    94  		// send the external secret object to store cluster
    95  		externalSecretBase64, err := mapper.ToBase64StringFromExternalSecret(extSecret)
    96  		if err != nil {
    97  			return false, err
    98  		}
    99  		err = r.sendChariotMessage(ctx, cluster.ProjectID, cluster.ClusterEdgeID, client.Create, externalSecretBase64)
   100  		return err == nil, err
   101  	}
   102  	return false, fmt.Errorf("%w: %s", ErrInvalidClusterSecretType, secretType)
   103  }
   104  
   105  // fetchClusterSecretPlainValue gets the plain value of the secret
   106  func (r *Resolver) fetchClusterSecretPlainValue(ctx context.Context, clusterEdgeID string, secretType model.ClusterSecretType, version string) (string, error) {
   107  	// extract username of actor updating cluster secret
   108  	user := middleware.ForContext(ctx)
   109  	auditLog := audit.New("api")
   110  
   111  	// audit log read request
   112  	auditLog.Log(secretType, audit.ReadRequest, "action_by", user.Username, "clusterEdgeID", clusterEdgeID, "version", version)
   113  
   114  	secret, err := r.fetchClusterSecret(ctx, clusterEdgeID, secretType, version)
   115  	if err != nil {
   116  		return "", err
   117  	}
   118  	return secret.Plain(), nil
   119  }
   120  
   121  // fetchClusterSecret gets the cluster secret, obtains the lease if edge security compliance is turned on and checks the secret is valid
   122  func (r *Resolver) fetchClusterSecret(ctx context.Context, clusterEdgeID string, secretType model.ClusterSecretType, version string) (cc.Secret, error) {
   123  	secretClient, err := r.getSecretClient(ctx, clusterEdgeID)
   124  	if err != nil {
   125  		return nil, err
   126  	}
   127  
   128  	isSecCompEnabled, err := r.GetBannerEdgeSecurityCompliance(ctx, clusterEdgeID)
   129  	if err != nil {
   130  		return nil, err
   131  	}
   132  	if isSecCompEnabled {
   133  		if err := r.obtainClusterSecretLease(ctx, clusterEdgeID, secretType, secretClient); err != nil {
   134  			return nil, err
   135  		}
   136  	}
   137  	for _, secret := range clustersecrets.List() {
   138  		if secret.Type() != secretType {
   139  			continue
   140  		}
   141  		fetchErr := secret.Fetch(ctx, secretClient, clusterEdgeID, version)
   142  
   143  		if status.Code(fetchErr) == codes.NotFound {
   144  			if err := r.handleSecretNotFound(ctx, clusterEdgeID, secret, secretClient); err != nil {
   145  				return nil, err
   146  			}
   147  		} else if status.Code(fetchErr) != codes.OK && !errors.Is(fetchErr, cc.ErrInvalidFormat) {
   148  			return nil, fetchErr
   149  		}
   150  
   151  		// rotate and regenerate secrets with invalid formats
   152  		if errors.Is(fetchErr, cc.ErrInvalidFormat) {
   153  			if err := r.reformatSecret(ctx, secretClient, secret, clusterEdgeID, fetchErr); err != nil {
   154  				return nil, err
   155  			}
   156  		}
   157  		return secret, nil
   158  	}
   159  	return nil, fmt.Errorf("%w: %s", ErrInvalidClusterSecretType, secretType)
   160  }
   161  
   162  func (r *Resolver) getSecretClient(ctx context.Context, clusterEdgeID string) (types.SecretManagerService, error) {
   163  	cluster, err := r.StoreClusterService.GetCluster(ctx, clusterEdgeID)
   164  	if err != nil {
   165  		return nil, err
   166  	}
   167  	secretClient, err := r.GCPClientService.GetSecretClient(ctx, cluster.ProjectID)
   168  	if err != nil {
   169  		return nil, err
   170  	}
   171  	return secretClient, nil
   172  }
   173  
   174  func (r *Resolver) handleSecretNotFound(ctx context.Context, clusterEdgeID string, secret cc.Secret, secretClient types.SecretManagerService) error {
   175  	auditLog := audit.New("api")
   176  	if err := secret.Refresh(""); err != nil {
   177  		return err
   178  	}
   179  	if err := secret.Apply(ctx, secretClient, clusterEdgeID); err != nil {
   180  		auditLog.Log(secret.Type(), audit.WriteRequest, "action_by", "api", "reason", "registration", "clusterEdgeID", clusterEdgeID)
   181  		return err
   182  	}
   183  	auditLog.Log(secret.Type(), audit.WriteRequest, "action_by", "api", "reason", "registration", "clusterEdgeID", clusterEdgeID, "version", secret.Version())
   184  	return nil
   185  }
   186  
   187  // reformatSecret takes an invalid secret and attempts to regenerate it using the existing plain text.
   188  // If the plain text is invalid or fails validation, the secret is rotated with a new random plain text credential.
   189  func (r *Resolver) reformatSecret(ctx context.Context, sm types.SecretManagerService, secret cc.Secret, clusterEdgeID string, fetchErr error) error {
   190  	auditLog := audit.New("api")
   191  	var err error
   192  	var usedExistingSecretValue bool
   193  	if errors.Is(fetchErr, cc.ErrSecretExpired) {
   194  		err = secret.Refresh("")
   195  		usedExistingSecretValue = false
   196  	} else {
   197  		err = secret.Refresh(secret.Plain())
   198  		usedExistingSecretValue = true
   199  	}
   200  	if err != nil {
   201  		if err := secret.Refresh(""); err != nil {
   202  			return err
   203  		}
   204  		usedExistingSecretValue = false
   205  	}
   206  	if err := secret.Apply(ctx, sm, clusterEdgeID); err != nil {
   207  		auditLog.Log(secret.Type(), audit.WriteRequest, "action_by", "api", "reason", fetchErr.Error(), "clusterEdgeID", clusterEdgeID, "secretPersisted", usedExistingSecretValue)
   208  		return err
   209  	}
   210  	auditLog.Log(secret.Type(), audit.WriteRequest, "action_by", "api", "reason", fetchErr.Error(), "clusterEdgeID", clusterEdgeID, "version", secret.Version(), "secretPersisted", usedExistingSecretValue)
   211  	return nil
   212  }
   213  
   214  func (r *Resolver) rotateClusterSecrets(ctx context.Context, secretClient types.SecretManagerService, clusterEdgeID string) error {
   215  	// rotate both secrets otherwise clusterctl will expire the secret and release the lease in the process
   216  	for _, secret := range clustersecrets.List() {
   217  		if err := secret.Fetch(ctx, secretClient, clusterEdgeID, "latest"); err != nil {
   218  			return err
   219  		}
   220  		if err := r.reformatSecret(ctx, secretClient, secret, clusterEdgeID, cc.ErrSecretExpired); err != nil {
   221  			return err
   222  		}
   223  		clusterSecret, err := r.ClusterSecretService.FetchClusterSecret(ctx, clusterEdgeID, secret.Type())
   224  		if err != nil {
   225  			return err
   226  		}
   227  		if err := r.ClusterSecretService.UpdateClusterSecret(ctx, clusterSecret.SecretEdgeID, secret.Type(), secret.Version()); err != nil {
   228  			return err
   229  		}
   230  	}
   231  	return nil
   232  }
   233  
   234  // fetchClusterSecretLease gets the cluster secret lease from the database and returns who owns it, when it expires and what secrets are associated with it
   235  func (r *Resolver) fetchClusterSecretLease(ctx context.Context, clusterEdgeID string, secretType model.ClusterSecretType) (*model.ClusterSecretLease, error) {
   236  	var lease model.ClusterSecretLease
   237  	isSecCompEnabled, err := r.GetBannerEdgeSecurityCompliance(ctx, clusterEdgeID)
   238  	if err != nil {
   239  		return nil, err
   240  	}
   241  	if !isSecCompEnabled {
   242  		return nil, ErrBannerOptedOutOfCompliance
   243  	}
   244  	user := middleware.ForContext(ctx)
   245  	auditLog := audit.New("api")
   246  
   247  	lease, err = r.ClusterSecretService.FetchLease(ctx, clusterEdgeID)
   248  	if err != nil {
   249  		return nil, err
   250  	}
   251  	auditLog.Log(secretType, audit.ReadClusterSecretLease, "action_by", user.Username, "clusterEdgeID", clusterEdgeID, "fetchClusterSecretLease", lease)
   252  	return &lease, nil
   253  }
   254  
   255  // removeUserFromClusterSecretLease releases or revokes access to a secret lease for a user and expires the cluster secrets so they are rotated
   256  func (r *Resolver) removeUserFromClusterSecretLease(ctx context.Context, clusterEdgeID string, secretType model.ClusterSecretType, username string, removal string) (bool, error) {
   257  	isSecCompEnabled, err := r.GetBannerEdgeSecurityCompliance(ctx, clusterEdgeID)
   258  	if err != nil {
   259  		return false, err
   260  	}
   261  	if !isSecCompEnabled {
   262  		return false, ErrBannerOptedOutOfCompliance
   263  	}
   264  	user := middleware.ForContext(ctx)
   265  	auditLog := audit.New("api")
   266  	if removal == "release" {
   267  		if err := r.ClusterSecretService.ReleaseLease(ctx, clusterEdgeID); err != nil {
   268  			return false, err
   269  		}
   270  		auditLog.Log(secretType, audit.ReleaseBreakoutSession, "action_by", user.Username, "clusterEdgeID", clusterEdgeID, "releaseClusterSecretLease", true)
   271  	}
   272  	if removal == "revoke" {
   273  		if err := r.ClusterSecretService.RevokeLease(ctx, clusterEdgeID, username); err != nil {
   274  			return false, err
   275  		}
   276  		auditLog.Log(secretType, audit.RevokeBreakoutSession, "action_by", user.Username, "clusterEdgeID", clusterEdgeID, "revokeClusterSecretLease", true, "user_revoked", username)
   277  	}
   278  	leaseID, err := r.ClusterSecretService.FetchLeaseID(ctx, clusterEdgeID)
   279  	if err != nil {
   280  		return false, err
   281  	}
   282  
   283  	if err := r.ClusterSecretService.ExpireClusterSecrets(ctx, leaseID); err != nil {
   284  		return false, err
   285  	}
   286  	return true, nil
   287  }
   288  
   289  // fetchClusterSecretVersions gets the current version and expiry time of a secret
   290  func (r *Resolver) fetchClusterSecretVersions(ctx context.Context, clusterEdgeID string, secretType model.ClusterSecretType) ([]*model.ClusterSecretVersionInfo, error) {
   291  	user := middleware.ForContext(ctx)
   292  	auditLog := audit.New("api")
   293  
   294  	versions, err := r.ClusterSecretService.FetchClusterSecretVersions(ctx, clusterEdgeID, secretType)
   295  	if err != nil {
   296  		return nil, err
   297  	}
   298  	auditLog.Log(secretType, audit.GetClusterSecretVersions, "action_by", user.Username, "clusterEdgeID", clusterEdgeID, "fetchClusterSecretVersions", versions)
   299  	return versions, nil
   300  }
   301  
   302  // obtainClusterSecretLease sets the lease username to the user that has called it
   303  func (r *Resolver) obtainClusterSecretLease(ctx context.Context, clusterEdgeID string, secretType model.ClusterSecretType, secretClient types.SecretManagerService) error {
   304  	user := middleware.ForContext(ctx)
   305  	auditLog := audit.New("api")
   306  	rotateSecret, err := r.ClusterSecretService.ObtainLease(ctx, clusterEdgeID)
   307  	if err != nil {
   308  		return err
   309  	}
   310  	// rotate the secret if the lease is new but still had the old owner attached, i.e. clusterctl hasn't reconciled yet
   311  	if rotateSecret {
   312  		if err := r.rotateClusterSecrets(ctx, secretClient, clusterEdgeID); err != nil {
   313  			return err
   314  		}
   315  	}
   316  	auditLog.Log(secretType, audit.NewBreakoutSession, "action_by", user.Username, "clusterEdgeID", clusterEdgeID, "obtainClusterSecretLease", true)
   317  	return nil
   318  }
   319  

View as plain text