Skip to content

feat: implement DeleteCurrentUserPAT RPC#1460

Merged
AmanGIT07 merged 1 commit intomainfrom
feat/delete-current-user-pat-rpc-implementation
Mar 18, 2026
Merged

feat: implement DeleteCurrentUserPAT RPC#1460
AmanGIT07 merged 1 commit intomainfrom
feat/delete-current-user-pat-rpc-implementation

Conversation

@AmanGIT07
Copy link
Contributor

@AmanGIT07 AmanGIT07 commented Mar 18, 2026

Description:

Summary

  • Add DeleteCurrentUserPAT RPC that soft-deletes a PAT and removes all its SpiceDB policies
  • Soft-delete happens before policy cleanup to prevent concurrent Update from re-creating policies for a deleted PAT (TOCTOU mitigation)
  • Ownership verified before deletion — returns NotFound if PAT belongs to a different user

Changes

  • Proto: DeleteCurrentUserPAT RPC, request (UUID-validated id), empty response
  • Repository: Delete method — sets deleted_at, returns ErrNotFound if already deleted
  • Service: Delete method with deletePolicies helper, audit logging (pat.revoked)
  • Handler: principal auth, validation, error mapping (FailedPrecondition/NotFound/Internal)
  • Authorization: added to skip endpoints (ownership enforced in service layer)
  • Mocks: added core/userpat to .mockery.yaml
  • Tests: 9 service test cases, 6 handler test cases

Manual test verification

  • PAT record soft-deleted in user_pats table (deleted_at is set)
  • All associated policy rows removed from policies table
  • API calls using the deleted PAT token return Unauthenticated
  • No SpiceDB tuples remain for the deleted PAT (verified via script, added in comment)
  • List/Get exclude deleted PAT
  • Active PAT count decremented
  • Audit record created

@vercel
Copy link

vercel bot commented Mar 18, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
frontier Ready Ready Preview, Comment Mar 18, 2026 6:51am

@coderabbitai
Copy link

coderabbitai bot commented Mar 18, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ea53a3af-3674-406e-adf2-6ed11b76d5cd

📥 Commits

Reviewing files that changed from the base of the PR and between 4af281d and 95748af.

⛔ Files ignored due to path filters (1)
  • proto/v1beta1/frontier.pb.go is excluded by !**/*.pb.go
📒 Files selected for processing (15)
  • Makefile
  • core/userpat/mocks/policy_service.go
  • core/userpat/mocks/repository.go
  • core/userpat/service.go
  • core/userpat/service_test.go
  • core/userpat/userpat.go
  • internal/api/v1beta1connect/interfaces.go
  • internal/api/v1beta1connect/mocks/user_pat_service.go
  • internal/api/v1beta1connect/user_pat.go
  • internal/api/v1beta1connect/user_pat_test.go
  • internal/store/postgres/userpat_repository.go
  • pkg/auditrecord/consts.go
  • pkg/server/connect_interceptors/authorization.go
  • proto/v1beta1/frontier.pb.validate.go
  • proto/v1beta1/frontierv1beta1connect/frontier.connect.go

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added the ability to delete and revoke personal access tokens with comprehensive error handling and audit logging for revocation events.
  • Tests

    • Added test coverage for personal access token deletion functionality across multiple layers.
  • Chores

    • Updated dependency version in build configuration.

Walkthrough

This PR introduces a new delete functionality for Personal Access Tokens (PATs) across multiple layers of the system. It adds soft-delete repository operations, service-level deletion with policy cleanup and audit logging, API interface methods and HTTP handlers, generated proto/connect code for the new RPC endpoint, and updates authorization and audit event constants.

Changes

Cohort / File(s) Summary
PAT Service Layer
core/userpat/service.go, core/userpat/service_test.go
Adds Delete method with soft-delete, associated policy cleanup, and audit logging; includes comprehensive test coverage for disabled PAT, not found, permission, and deletion scenarios.
PAT Repository Interfaces
core/userpat/userpat.go, core/userpat/mocks/repository.go
Extends Repository interface with Delete method and adds corresponding mock implementation with Run/Return/RunAndReturn helpers.
Policy Service Mocks
core/userpat/mocks/policy_service.go
Adds Delete method mock with typed call chaining and expectation setup via testify/mock pattern.
Database Implementation
internal/store/postgres/userpat_repository.go
Implements soft-delete via UPDATE with deleted_at timestamp, including row-affected checks and error handling.
API Service Layer
internal/api/v1beta1connect/interfaces.go, internal/api/v1beta1connect/mocks/user_pat_service.go
Adds Delete method to UserPATService interface; updates mocks with Get, Delete, and ListAllowedRoles methods with explicit type handling and call chaining.
HTTP Handler
internal/api/v1beta1connect/user_pat.go, internal/api/v1beta1connect/user_pat_test.go
Implements DeleteCurrentUserPAT handler with authentication, error mapping (ErrDisabled → FailedPrecondition, ErrNotFound → NotFound), and test coverage for auth/permission/error scenarios.
Proto/Connect Generated Code
proto/v1beta1/frontier.pb.validate.go, proto/v1beta1/frontierv1beta1connect/frontier.connect.go
Adds validation methods to proto messages and new DeleteCurrentUserPAT RPC pathway including client method, procedure constant, server handler routing, and unimplemented handler.
Authorization and Audit
pkg/server/connect_interceptors/authorization.go, pkg/auditrecord/consts.go
Adds DeleteCurrentUserPAT to authorization skip list; introduces PATRevokedEvent audit constant.
Build Configuration
Makefile
Updates PROTON_COMMIT hash from "241685584afc362ce445309926b6dbe845899444" to "8c639b3bcf2c458cb42dea00374045d6b9b4d0e9".

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • #1401 — Implements complementary Create PAT endpoint, modifying the same service/repository/mock/handler layers with parallel patterns.
  • #1450 — Adds Get PAT method to UserPATService API and mocks following the same delete-related patterns.
  • #1442 — Extends PAT subsystem with additional repository methods (GetBySecretHash, UpdateLastUsedAt) coordinating with this delete functionality.

Suggested reviewers

  • rohilsurana
  • whoAbhishekSah
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@AmanGIT07
Copy link
Contributor Author

Script used to verify tuples for a pat_id:

#!/usr/bin/env python3
"""
Check if any SpiceDB tuples still exist for a given PAT ID.

Usage:
  python3 scripts/check_pat_tuples.py <pat-id>
  python3 scripts/check_pat_tuples.py 34b04e9e-14ee-4bd6-9175-9787d77c1295
"""

import argparse
import subprocess
import sys


def zed_read(resource: str, relation: str, subject: str, zed_flags: list[str]) -> str:
    """Read relationships from SpiceDB using positional args.

    Usage: zed relationship read <resource_type:optional_id> <optional_relation> <optional_subject_type:optional_id>
    """
    cmd = ["zed", "relationship", "read", resource]
    if relation:
        cmd.append(relation)
    if subject:
        cmd.append(subject)
    cmd += zed_flags
    result = subprocess.run(cmd, capture_output=True, text=True)
    if result.returncode != 0 and result.stderr.strip():
        print(f"  zed error: {result.stderr.strip()}")
    return result.stdout.strip()


def main():
    parser = argparse.ArgumentParser(description="Check SpiceDB tuples for a PAT")
    parser.add_argument("pat_id", help="The PAT UUID to check")
    parser.add_argument("--endpoint", default="localhost:50051", help="SpiceDB gRPC endpoint")
    parser.add_argument("--token", default="frontier", help="SpiceDB preshared key")
    args = parser.parse_args()

    zed_flags = [
        "--endpoint", args.endpoint,
        "--token", args.token,
        "--insecure",
        "--consistency-full",
    ]

    pat_subject = f"app/pat:{args.pat_id}"
    found_any = False

    # 1. Check rolebinding bearer tuples: app/rolebinding:X#bearer@app/pat:<id>
    print(f"\n1. Rolebinding bearer tuples (app/rolebinding where subject = {pat_subject}):")
    rels = zed_read("app/rolebinding", "bearer", pat_subject, zed_flags)
    if rels:
        for line in rels.split("\n"):
            if line.strip():
                print(f"   {line.strip()}")
                found_any = True
    else:
        print("   (none)")

    # 2. Check grant tuples on organizations/projects for each rolebinding
    rolebinding_ids = []
    if rels:
        for line in rels.split("\n"):
            # format: app/rolebinding:<id> bearer app/pat:<pat_id>
            stripped = line.strip()
            if stripped and "app/rolebinding:" in stripped:
                rb_id = stripped.split()[0].replace("app/rolebinding:", "")
                rolebinding_ids.append(rb_id)

    if rolebinding_ids:
        print(f"\n2. Grant tuples for rolebindings ({len(rolebinding_ids)} found):")
        for rb_id in rolebinding_ids:
            rb_subject = f"app/rolebinding:{rb_id}"
            # Check org grants (granted and pat_granted relations)
            for relation in ["granted", "pat_granted"]:
                org_rels = zed_read("app/organization", relation, rb_subject, zed_flags)
                if org_rels:
                    for line in org_rels.split("\n"):
                        if line.strip():
                            print(f"   {line.strip()}")
                            found_any = True
            # Check project grants
            proj_rels = zed_read("app/project", "granted", rb_subject, zed_flags)
            if proj_rels:
                for line in proj_rels.split("\n"):
                    if line.strip():
                        print(f"   {line.strip()}")
                        found_any = True
    else:
        print(f"\n2. Grant tuples: (skipped, no rolebindings found)")

    # 3. Check role tuples: app/rolebinding:X#role@app/role:Y
    if rolebinding_ids:
        print(f"\n3. Role tuples for rolebindings:")
        for rb_id in rolebinding_ids:
            role_rels = zed_read(f"app/rolebinding:{rb_id}", "", "", zed_flags)
            if role_rels:
                for line in role_rels.split("\n"):
                    if line.strip():
                        print(f"   {line.strip()}")
                        found_any = True
    else:
        print(f"\n3. Role tuples: (skipped, no rolebindings found)")

    # Summary
    print(f"\n{'=' * 60}")
    if found_any:
        print(f"RESULT: Tuples STILL EXIST for PAT {args.pat_id}")
        sys.exit(1)
    else:
        print(f"RESULT: No tuples found for PAT {args.pat_id} - deletion successful")


if __name__ == "__main__":
    main()

@coveralls
Copy link

Pull Request Test Coverage Report for Build 23232723279

Details

  • 58 of 87 (66.67%) changed or added relevant lines in 3 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage increased (+0.07%) to 40.845%

Changes Missing Coverage Covered Lines Changed/Added Lines %
internal/api/v1beta1connect/user_pat.go 24 26 92.31%
internal/store/postgres/userpat_repository.go 0 27 0.0%
Totals Coverage Status
Change from base Build 23232431365: 0.07%
Covered Lines: 14381
Relevant Lines: 35209

💛 - Coveralls

@AmanGIT07 AmanGIT07 merged commit 625d802 into main Mar 18, 2026
8 checks passed
@AmanGIT07 AmanGIT07 deleted the feat/delete-current-user-pat-rpc-implementation branch March 18, 2026 10:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants