Skip to content

feat: implement SearchOrganizationPATs admin RPC#1482

Merged
AmanGIT07 merged 3 commits intomainfrom
feat/admin-search-org-pats
Mar 27, 2026
Merged

feat: implement SearchOrganizationPATs admin RPC#1482
AmanGIT07 merged 3 commits intomainfrom
feat/admin-search-org-pats

Conversation

@AmanGIT07
Copy link
Copy Markdown
Contributor

Summary

  • Add SearchOrganizationPATs admin RPC to list all PATs in an org with enriched data
  • Returns PAT details with created_by info (user title, email), scopes (role + resources), and timestamps
  • Supports RQL filtering, search, sorting, and pagination
  • For "all projects" scope PATs, resolves actual project IDs from SpiceDB (grouped by user to minimize calls)
  • Requires superadmin auth (IsSuperUser)

How it works

SQL (2 queries)

Uses subquery pagination — inner subquery selects paginated PATs with RQL applied, outer query joins users + policies for enrichment:

SELECT p.id, p.title, u.title, u.email, pol.role_id, pol.resource_type, ...                                                                                                                                                      
FROM (                                                                                                                                                                                                                           
    SELECT ... FROM user_pats p
    LEFT JOIN users u ON p.user_id = u.id                                                                                                                                                                                        
    WHERE p.org_id = ? AND p.deleted_at IS NULL
    -- RQL filters/search/sort here                                                                                                                                                                                              
    LIMIT 30 OFFSET 0                                                                                                                                                                                                            
) p                                                                                                                                                                                                                              
LEFT JOIN users u ON p.user_id = u.id                                                                                                                                                                                            
LEFT JOIN policies pol ON pol.principal_id = p.id AND pol.principal_type = 'app/pat'                                                                                                                                             
ORDER BY p.created_at DESC, p.id, pol.role_id                                                                                                                                                                                    
 

Returns multiple rows per PAT (one per policy). Go code groups rows by PAT ID to build nested scopes.

Scope resolution

  • Specific project scope: resource_ids populated directly from policies table
  • All-projects scope (pat_granted): resolved via SpiceDB LookupResources per unique user, populating actual project IDs the user has access to
  • Org scope: resource_ids contains the org ID

RQL support

┌──────────────────┬────────┬────────┬──────┐                                                                                                                                                                                    
│      Field       │ Filter │ Search │ Sort │
├──────────────────┼────────┼────────┼──────┤
│ id               │ ✓      │ ✓      │      │
├──────────────────┼────────┼────────┼──────┤
│ title            │ ✓      │ ✓      │ ✓    │                                                                                                                                                                                    
├──────────────────┼────────┼────────┼──────┤
│ created_by_title │ ✓      │ ✓      │ ✓    │                                                                                                                                                                                    
├──────────────────┼────────┼────────┼──────┤                                                                                                                                                                                    
│ created_by_email │ ✓      │ ✓      │ ✓    │
├──────────────────┼────────┼────────┼──────┤                                                                                                                                                                                    
│ created_at       │ ✓      │        │ ✓    │
├──────────────────┼────────┼────────┼──────┤                                                                                                                                                                                    
│ expires_at       │ ✓      │        │ ✓    │
├──────────────────┼────────┼────────┼──────┤                                                                                                                                                                                    
│ last_used_at     │ ✓      │        │ ✓    │
└──────────────────┴────────┴────────┴──────┘

Supported filter operators: eq, neq, gt, gte, lt, lte, like, notlike, ilike, notilike, empty, notempty, in, notin

Default and max limit: 30 (reduced from standard 50 due to SpiceDB enrichment cost).

Sample request/response

Request

  {                                                                                                                                                                                                                                
    "org_id": "67801432-d115-4a03-9fb0-839341c56632",
    "query": {                                                                                                                                                                                                                     
      "search": "aman",
      "limit": 10,                                                                                                                                                                                                                 
      "offset": 0,
      "sort": [{"name": "created_at", "order": "desc"}]
    }                                                                                                                                                                                                                              
  }

Response

  {
    "organization_pats": [
      {
        "id": "b1186e25-0332-41b6-a85c-67ba17612e52",                                                                                                                                                                              
        "title": "ci-pipeline-token",                                                                                                                                                                                              
        "created_by": {                                                                                                                                                                                                            
          "id": "9bd455fb-1957-4fa9-b663-0eb1872c4baf",                                                                                                                                                                            
          "title": "Aman Prasad",                                                                                                                                                                                                  
          "email": "aman@pixxel.co.in"
        },                                                                                                                                                                                                                         
        "scopes": [
          {                                                                                                                                                                                                                        
            "role_id": "b5763e25-fc5b-42fc-8657-85e03e197395",
            "resource_type": "app/organization",                                                                                                                                                                                   
            "resource_ids": ["67801432-d115-4a03-9fb0-839341c56632"]                                                                                                                                                               
          },                                                                                                                                                                                                                       
          {                                                                                                                                                                                                                        
            "role_id": "c4095deb-0cfd-4f24-9259-a022079f181b",
            "resource_type": "app/project",                                                                                                                                                                                        
            "resource_ids": ["f06fa1f6-6adf-4a6d-a12c-e22a8ab78529"]                                                                                                                                                               
          }                                                                                                                                                                                                                        
        ],                                                                                                                                                                                                                         
        "created_at": "2026-03-24T16:15:28.164Z",                                                                                                                                                                                  
        "expires_at": "2026-04-29T15:15:37.870Z",                                                                                                                                                                                  
        "last_used_at": "2026-03-25T10:30:00Z"                                                                                                                                                                                     
      }            
...                                                                                                                                                                                                                
    ],                                                                                                                                                                                                                             
    "pagination": {                                                                                                                                                                                                                
      "offset": 0,
      "limit": 10,
      "total_count": 42
    }                                                                                                                                                                                                                              
  }

Filter example

  {
    "org_id": "67801432-...",
    "query": {                                                                                                                                                                                                                     
      "filters": [
        {"name": "created_by_email", "operator": "ilike", "string_value": "%@pixxel%"},                                                                                                                                            
        {"name": "expires_at", "operator": "lt", "string_value": "2026-04-01T00:00:00Z"}                                                                                                                                           
      ]                                                                                                                                                                                                                            
    }                                                                                                                                                                                                                              
  }                                                                          

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 27, 2026

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

Project Deployment Actions Updated (UTC)
frontier Ready Ready Preview, Comment Mar 27, 2026 8:42am

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 27, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Search and filter organization personal access tokens with advanced query capabilities and pagination.
  • Chores

    • Updated build dependencies and tool versions.

Walkthrough

Adds organization-level PAT search: new orgpats service (with scope enrichment), PostgreSQL repository with RQL support, Connect RPC handler and mocks/tests, DI wiring and authorization rule, and a Makefile/go.mod dependency update.

Changes

Cohort / File(s) Summary
Build & Dependencies
Makefile, go.mod
Updated PROTON_COMMIT hash; moved grpc-gateway from direct to indirect and bumped github.com/raystack/salt.
Core Service Layer
core/aggregates/orgpats/service.go, core/aggregates/orgpats/service_test.go, core/aggregates/orgpats/mocks/*
Added orgpats Service with Repository and ProjectService interfaces, implemented scope enrichment (resolve all-projects → project IDs), and unit tests with autogenerated mocks.
API Dependency Injection
internal/api/api.go, cmd/serve.go, internal/api/v1beta1connect/v1beta1connect.go
Wired OrgPATsService into global deps and ConnectHandler via server startup.
API Endpoints & Interfaces
internal/api/v1beta1connect/interfaces.go, internal/api/v1beta1connect/organization_pats.go, internal/api/v1beta1connect/organization_pats_test.go, internal/api/v1beta1connect/mocks/org_pa_ts_service.go
Added OrgPATsService interface, Connect RPC handler SearchOrganizationPATs with RQL validation/pagination enforcement, request→RQL conversion, error mapping, response transformation, and comprehensive handler tests and mocks.
Data Access Layer
internal/store/postgres/org_pats_repository.go, internal/store/postgres/org_pats_repository_test.go
New Postgres OrgPATsRepository with RQL-driven count/data queries, custom filter/search/sort handling, grouping logic to assemble AggregatedPATs from rows, and extensive query/aggregation tests.
Authorization
pkg/server/connect_interceptors/authorization.go
Added authorization validation for AdminService/SearchOrganizationPATs requiring superuser check.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • whoAbhishekSah
  • rsbh

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.

@coveralls
Copy link
Copy Markdown

coveralls commented Mar 27, 2026

Pull Request Test Coverage Report for Build 23638162435

Warning: This coverage report may be inaccurate.

This pull request's base commit is no longer the HEAD commit of its target branch. This means it includes changes from outside the original pull request, including, potentially, unrelated coverage changes.

Details

  • 290 of 358 (81.01%) changed or added relevant lines in 6 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage increased (+0.4%) to 41.628%

Changes Missing Coverage Covered Lines Changed/Added Lines %
internal/api/v1beta1connect/v1beta1connect.go 0 1 0.0%
pkg/server/connect_interceptors/authorization.go 0 3 0.0%
cmd/serve.go 0 4 0.0%
core/aggregates/orgpats/service.go 46 50 92.0%
internal/api/v1beta1connect/organization_pats.go 60 66 90.91%
internal/store/postgres/org_pats_repository.go 184 234 78.63%
Totals Coverage Status
Change from base Build 23540166648: 0.4%
Covered Lines: 15014
Relevant Lines: 36067

💛 - Coveralls

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (6)
internal/api/v1beta1connect/organization_pats_test.go (2)

165-204: Missing mock expectation assertion in standalone test.

The standalone test "should transform scopes correctly" creates a new mock but doesn't call AssertExpectations(t) at the end. While this may pass, it won't verify that the expected mock calls were made.

Proposed fix
 		assert.True(t, scopeTypes[schema.OrganizationNamespace])
 		assert.True(t, scopeTypes[schema.ProjectNamespace])
+
+		mockOrgPATsSrv.AssertExpectations(t)
 	})

206-224: Missing mock expectation assertion in pagination test.

Similar to the scope transformation test, this test should call AssertExpectations(t) for consistency with the table-driven tests.

Proposed fix
 		assert.Equal(t, uint32(10), resp.Msg.GetPagination().GetOffset())
 		assert.Equal(t, uint32(30), resp.Msg.GetPagination().GetLimit())
 		assert.Equal(t, uint32(100), resp.Msg.GetPagination().GetTotalCount())
+
+		mockOrgPATsSrv.AssertExpectations(t)
 	})
core/aggregates/orgpats/service_test.go (1)

93-103: Weak assertion in "all-projects" resolution test.

The combination of .Maybe() on the mock expectation and the conditional if len(result.PATs[0].Scopes[0].ResourceIDs) > 0 means this test will pass even if ListByUser is never called and resolution doesn't happen. This undermines the test's purpose of verifying SpiceDB resolution.

Consider making the assertion unconditional and removing .Maybe() to ensure the resolution behavior is actually tested:

Proposed fix
 		repo.EXPECT().Search(mock.Anything, orgID, query).Return(repoResult, nil)
-		projSvc.EXPECT().ListByUser(mock.Anything, mock.Anything, mock.Anything).
-			Return([]project.Project{{ID: "proj-1"}, {ID: "proj-2"}}, nil).Maybe()
+		projSvc.EXPECT().ListByUser(mock.Anything, mock.Anything, mock.Anything).
+			Return([]project.Project{{ID: "proj-1"}, {ID: "proj-2"}}, nil)
 
 		svc := orgpats.NewService(repo, projSvc)
 		result, err := svc.Search(ctx, orgID, query)
 		assert.NoError(t, err)
 		assert.Len(t, result.PATs, 1)
 		// After resolution, the all-projects scope should have project IDs
-		if len(result.PATs[0].Scopes[0].ResourceIDs) > 0 {
-			assert.Contains(t, result.PATs[0].Scopes[0].ResourceIDs, "proj-1")
-		}
+		assert.ElementsMatch(t, []string{"proj-1", "proj-2"}, result.PATs[0].Scopes[0].ResourceIDs)
internal/api/v1beta1connect/organization_pats.go (1)

60-64: Potential integer truncation when casting TotalCount.

result.Pagination.TotalCount is int64 but is cast to uint32 for the protobuf response. For organizations with more than ~4.2 billion PATs (uint32 max), this would silently truncate. While unlikely in practice, this is a latent issue.

Consider documenting this limitation or using a larger type in the proto if needed in the future.

internal/store/postgres/org_pats_repository.go (2)

319-329: Edge case: rows with unexpected ResourceType/GrantRelation combinations are silently skipped.

The switch statement (lines 319-329) has a default: continue that silently skips rows that don't match expected patterns. While this is defensive, it could mask data inconsistencies in the policies table.

Consider logging a warning for unexpected policy row patterns to aid debugging:


29-57: Filter fields include "id" but sort fields do not.

orgPATFilterFields includes "id": "p.id" but orgPATSortFields does not include "id". Users can filter by ID but cannot sort by ID, which may be unexpected.

Proposed fix if sorting by ID should be supported
 var orgPATSortFields = map[string]string{
+	"id":               "p.id",
 	"title":            "p.title",
 	"created_by_title": "u.title",

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 15e7d3be-73e6-4481-b1f6-73e689411f71

📥 Commits

Reviewing files that changed from the base of the PR and between d05d9e5 and fbd8f44.

⛔ Files ignored due to path filters (4)
  • go.sum is excluded by !**/*.sum
  • proto/v1beta1/admin.pb.go is excluded by !**/*.pb.go, !proto/**
  • proto/v1beta1/admin.pb.validate.go is excluded by !proto/**
  • proto/v1beta1/frontierv1beta1connect/admin.connect.go is excluded by !proto/**
📒 Files selected for processing (16)
  • Makefile
  • cmd/serve.go
  • core/aggregates/orgpats/mocks/project_service.go
  • core/aggregates/orgpats/mocks/repository.go
  • core/aggregates/orgpats/service.go
  • core/aggregates/orgpats/service_test.go
  • go.mod
  • internal/api/api.go
  • internal/api/v1beta1connect/interfaces.go
  • internal/api/v1beta1connect/mocks/org_pa_ts_service.go
  • internal/api/v1beta1connect/organization_pats.go
  • internal/api/v1beta1connect/organization_pats_test.go
  • internal/api/v1beta1connect/v1beta1connect.go
  • internal/store/postgres/org_pats_repository.go
  • internal/store/postgres/org_pats_repository_test.go
  • pkg/server/connect_interceptors/authorization.go

@AmanGIT07 AmanGIT07 enabled auto-merge (squash) March 27, 2026 08:43
@AmanGIT07 AmanGIT07 merged commit 5417d82 into main Mar 27, 2026
7 of 8 checks passed
@AmanGIT07 AmanGIT07 deleted the feat/admin-search-org-pats branch March 27, 2026 08:46
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