Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ TAG := $(shell git rev-list --tags --max-count=1)
VERSION := $(shell git describe --tags ${TAG})
.PHONY: build check fmt lint test test-race vet test-cover-html help install proto admin-app compose-up-dev
.DEFAULT_GOAL := build
PROTON_COMMIT := "241685584afc362ce445309926b6dbe845899444"
PROTON_COMMIT := "8c639b3bcf2c458cb42dea00374045d6b9b4d0e9"

admin-app:
@echo " > generating admin build"
Expand Down
47 changes: 47 additions & 0 deletions core/userpat/mocks/policy_service.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

47 changes: 47 additions & 0 deletions core/userpat/mocks/repository.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

49 changes: 49 additions & 0 deletions core/userpat/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ type RoleService interface {
type PolicyService interface {
Create(ctx context.Context, pol policy.Policy) (policy.Policy, error)
List(ctx context.Context, flt policy.Filter) ([]policy.Policy, error)
Delete(ctx context.Context, id string) error
}

type AuditRecordRepository interface {
Expand Down Expand Up @@ -114,6 +115,54 @@ func (s *Service) Get(ctx context.Context, userID, id string) (patmodels.PAT, er
return pat, nil
}

// Delete soft-deletes the PAT first, then removes its SpiceDB policies.
// Soft-delete before policy cleanup prevents concurrent Update from re-creating
// policies for a deleted PAT (TOCTOU mitigation).
func (s *Service) Delete(ctx context.Context, userID, id string) error {
if !s.config.Enabled {
return paterrors.ErrDisabled
}
pat, err := s.repo.GetByID(ctx, id)
if err != nil {
return err
}
if pat.UserID != userID {
return paterrors.ErrNotFound
}

if err := s.repo.Delete(ctx, id); err != nil {
return fmt.Errorf("soft deleting PAT: %w", err)
}

if err := s.deletePolicies(ctx, id); err != nil {
return fmt.Errorf("deleting policies: %w", err)
}

if err := s.createAuditRecord(ctx, pkgAuditRecord.PATRevokedEvent, pat, time.Now().UTC(), nil); err != nil {
s.logger.Error("failed to create audit record for PAT revocation", "pat_id", id, "error", err)
}

return nil
}

// deletePolicies removes all SpiceDB policies associated with a PAT.
// Each policy.Delete call removes SpiceDB relations first, then hard-deletes the Postgres policy row.
func (s *Service) deletePolicies(ctx context.Context, patID string) error {
policies, err := s.policyService.List(ctx, policy.Filter{
PrincipalID: patID,
PrincipalType: schema.PATPrincipal,
})
if err != nil {
return fmt.Errorf("listing policies for PAT %s: %w", patID, err)
}
for _, pol := range policies {
if err := s.policyService.Delete(ctx, pol.ID); err != nil {
return fmt.Errorf("deleting policy %s: %w", pol.ID, err)
}
}
return nil
}

// Create generates a new PAT and returns it with the plaintext value.
// The plaintext value is only available at creation time.
func (s *Service) Create(ctx context.Context, req CreateRequest) (patmodels.PAT, string, error) {
Expand Down
Loading
Loading