package clustersecrets import ( "context" "database/sql" "errors" "time" "github.com/google/uuid" sqlerr "edge-infra.dev/pkg/edge/api/apierror/sql" "edge-infra.dev/pkg/edge/api/graph/model" "edge-infra.dev/pkg/edge/api/middleware" ) var ( // ErrLeaseAlreadyInUse is thrown if the cluster secret lease already has a lease owner ErrLeaseAlreadyInUse = errors.New("lease is already owned by another user") // ErrUserDoesNotOwnLease is thrown if the current user attempting to release the lease does not have // ownership over the cluster secret lease ErrUserDoesNotOwnLease = errors.New("user does not own the lease") // ErrCannotRevokeUser is thrown if a lease is being revoked but user to be revoked does not own the lease ErrCannotRevokeUser = errors.New("cannot revoke user from lease as they do not own it") // ErrFailedToUpdateDatabase is thrown if no rows are returned from an SQL query where we expect rows to be updated ErrFailedToUpdateDatabase = errors.New("failed to update database") ) // Obtain lease will attempt to obtain the cluster secret lease func (s *clusterSecretService) ObtainLease(ctx context.Context, clusterEdgeID string) (bool, error) { lease, err := s.FetchLease(ctx, clusterEdgeID) if err != nil { return false, err } currentLeaseExpirationTime, err := time.Parse(time.RFC3339, lease.ExpiresAt) if err != nil { return false, err } user := middleware.ForContext(ctx) currentTime := time.Now().UTC() if currentLeaseExpirationTime.After(currentTime) { if lease.Owner == user.Username { // user already owns the lease so no need to obtain the lease return false, nil } return false, ErrLeaseAlreadyInUse } maxLeaseValidityPeriod, err := time.ParseDuration(s.Config.EdgeMaxLeaseValidityPeriod) if err != nil { return false, err } newExpirationTime := currentTime.Add(maxLeaseValidityPeriod).Format(time.RFC3339) result, err := s.SQLDB.ExecContext(ctx, ObtainLeaseQuery, newExpirationTime, user.Username, currentTime.Format(time.RFC3339), clusterEdgeID) if err != nil { return false, err } rows, err := result.RowsAffected() if err != nil { return false, err } if rows == 0 { return false, ErrFailedToUpdateDatabase } if lease.Owner != "" { return true, nil } return false, nil } // ReleaseLease will remove the current user from lease ownership and trigger secret rotation by expiring the secret lease func (s *clusterSecretService) ReleaseLease(ctx context.Context, clusterEdgeID string) error { user := middleware.ForContext(ctx) currentTime := time.Now().UTC().Format(time.RFC3339) result, err := s.SQLDB.ExecContext(ctx, ExpireLeaseQuery, currentTime, currentTime, clusterEdgeID, user.Username) if err != nil { return err } rows, err := result.RowsAffected() if err != nil { return err } if rows == 0 { return ErrUserDoesNotOwnLease } return nil } // RevokeLease will remove the owner from a lease and trigger secret rotation by expiring the secret lease func (s *clusterSecretService) RevokeLease(ctx context.Context, clusterEdgeID string, username string) error { currentTime := time.Now().UTC().Format(time.RFC3339) result, err := s.SQLDB.ExecContext(ctx, ExpireLeaseQuery, currentTime, currentTime, clusterEdgeID, username) if err != nil { return err } rows, err := result.RowsAffected() if err != nil { return err } if rows == 0 { return ErrCannotRevokeUser } return nil } // RemoveUserFromLease deletes the username from an expired lease func (s *clusterSecretService) RemoveUserFromLease(ctx context.Context, clusterSecretLeaseEdgeID string) error { currentTime := time.Now().UTC().Format(time.RFC3339) result, err := s.SQLDB.ExecContext(ctx, RemoveUserFromLeaseQuery, currentTime, clusterSecretLeaseEdgeID) if err != nil { return err } rows, err := result.RowsAffected() if err != nil { return err } if rows == 0 { return ErrFailedToUpdateDatabase } return nil } // FetchLease will attempt to query for a cluster secrets lease including the lease owner and expiration date func (s *clusterSecretService) FetchLease(ctx context.Context, clusterEdgeID string) (model.ClusterSecretLease, error) { secretLease := model.ClusterSecretLease{} row := s.SQLDB.QueryRowContext(ctx, FetchLeaseQuery, clusterEdgeID) var leaseID, expiration, owner string if err := row.Scan(&leaseID, &expiration, &owner); err != nil { return secretLease, err } var secretTypes []string rows, err := s.SQLDB.QueryContext(ctx, FetchLeaseSecretTypesQuery, clusterEdgeID, leaseID) if err != nil { return secretLease, err } defer rows.Close() for rows.Next() { var secretType string if err := rows.Scan(&secretType); err != nil { return secretLease, err } secretTypes = append(secretTypes, secretType) } if err := rows.Err(); err != nil { return secretLease, sqlerr.Wrap(err) } return model.ClusterSecretLease{ ExpiresAt: expiration, Owner: owner, SecretTypes: secretTypes, }, nil } // CreateLease will create a new lease for a cluster with no user set func (s *clusterSecretService) CreateLease(ctx context.Context, clusterEdgeID string) (string, error) { leaseID := uuid.NewString() currentTime := time.Now().UTC().Format(time.RFC3339) result, err := s.SQLDB.ExecContext(ctx, CreateClusterSecretLeaseQuery, leaseID, clusterEdgeID, currentTime, currentTime, currentTime) if err != nil { return "", err } rows, err := result.RowsAffected() if err != nil { return "", err } if rows == 0 { return "", ErrFailedToUpdateDatabase } return leaseID, nil } // FetchLeaseID will get the clusterSecretLeaseID for a lease func (s *clusterSecretService) FetchLeaseID(ctx context.Context, clusterEdgeID string) (string, error) { row := s.SQLDB.QueryRowContext(ctx, FetchLeaseIDQuery, clusterEdgeID) var leaseID string if err := row.Scan(&leaseID); err != nil { return "", err } return leaseID, nil } // VerifyLeaseExists checks that a lease exists for a cluster, and if not creates one func (s *clusterSecretService) VerifyLeaseExists(ctx context.Context, clusterEdgeID string) (string, error) { var leaseID string _, err := s.FetchLease(ctx, clusterEdgeID) if errors.Is(err, sql.ErrNoRows) { leaseID, err = s.CreateLease(ctx, clusterEdgeID) if err != nil { return "", err } } else if err != nil { return "", err } return leaseID, nil }