Skip to content

feat: implement RemoveProjectMember RPC#1504

Merged
whoAbhishekSah merged 1 commit intomainfrom
feat/remove-project-member
Apr 3, 2026
Merged

feat: implement RemoveProjectMember RPC#1504
whoAbhishekSah merged 1 commit intomainfrom
feat/remove-project-member

Conversation

@whoAbhishekSah
Copy link
Copy Markdown
Member

@whoAbhishekSah whoAbhishekSah commented Apr 2, 2026

Summary

  • Add RemoveProjectMember RPC for removing a principal from a project
  • Supports user, service user, and group principals via principal_id + principal_type
  • Deletes all project-level policies for the principal
  • Validates principal_type (returns invalid_argument for unknown types)
  • Returns not found if principal has no project policies
  • Authorization checks update permission on the project (consistent with SetProjectMemberRole)
  • Add audit logging for both SetProjectMemberRole and RemoveProjectMember

Closes #1461

Proton PR: raystack/proton#465

Test plan

  • Unit tests for service layer (6 cases)
  • E2E tests — 33/33 pass on clean database

🤖 Generated with Claude Code

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 2, 2026

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

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

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 2, 2026

Warning

Rate limit exceeded

@whoAbhishekSah has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 17 minutes and 49 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 17 minutes and 49 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1dea4786-8825-4e4d-a010-d2fc70371526

📥 Commits

Reviewing files that changed from the base of the PR and between 7b3dfa9 and 575d7d8.

⛔ Files ignored due to path filters (2)
  • proto/v1beta1/frontier.pb.go is excluded by !**/*.pb.go, !proto/**
  • proto/v1beta1/frontierv1beta1connect/frontier.connect.go is excluded by !proto/**
📒 Files selected for processing (9)
  • Makefile
  • core/audit/audit.go
  • core/project/errors.go
  • core/project/service.go
  • core/project/service_test.go
  • internal/api/v1beta1connect/interfaces.go
  • internal/api/v1beta1connect/mocks/project_service.go
  • internal/api/v1beta1connect/project.go
  • pkg/server/connect_interceptors/authorization.go
📝 Walkthrough

Walkthrough

Adds a server-side RemoveProjectMember RPC and flow: service method, interface/mocks, handler, authorization entry, error handling, audit events; includes Makefile PROTON_COMMIT update and new sentinel error ErrNotMember. (33 words)

Changes

Cohort / File(s) Summary
Proton Dependency
Makefile
Updated PROTON_COMMIT hash from 22a0b96c3608ee3d360b12b360581fcccd96fadc to 4f1eb42907547a0b11064ea2dda035b0be58bf30.
Project Service Core
core/project/errors.go, core/project/service.go, core/project/service_test.go
Added exported sentinel ErrNotMember. Implemented Service.RemoveMember(ctx, projectID, principalID, principalType) which validates principal type, lists project-scoped policies for the principal, returns ErrNotMember if none, and deletes matching policies. Added tests covering error and success scenarios for multiple principal types.
API Interface & Mocks
internal/api/v1beta1connect/interfaces.go, internal/api/v1beta1connect/mocks/project_service.go
Extended ProjectService interface with RemoveMember(...) error. Added corresponding mock method, expecter, and typed call helper.
API Handler & Authorization
internal/api/v1beta1connect/project.go, pkg/server/connect_interceptors/authorization.go
Added ConnectHandler.RemoveProjectMember handler calling the service, request-scoped error logging, explicit error→Connect-code mapping (ErrNotExist/ErrNotMember→NotFound, ErrInvalidPrincipalType→InvalidArgument), and audit logging on success. Registered RPC in authorizationValidationMap requiring project UpdatePermission.
Audit Events
core/audit/audit.go
Added ProjectMemberRoleSetEvent and ProjectMemberRemovedEvent event names; reformatted existing project event constants.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Suggested reviewers

  • rohilsurana
  • AmanGIT07
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The pull request comprehensively implements both RemoveProjectMember and SetProjectMemberRole RPCs with proper validation, authorization checks, error handling, audit logging, and extensive test coverage.
Out of Scope Changes check ✅ Passed All changes are directly aligned with implementing RemoveProjectMember and SetProjectMemberRole RPCs. The Makefile update is a supporting dependency (Proton PR #465) required for protobuf generation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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.

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.

🧹 Nitpick comments (2)
core/project/service_test.go (1)

1247-1353: Good test coverage for the core RemoveMember functionality.

The tests cover the main paths: project not found, principal not a member, successful deletion, and different principal types.

Consider adding tests for error propagation from policyService.List and partial failure scenarios where policyService.Delete fails mid-iteration. These would help ensure error handling is robust, though they're not blocking.

internal/api/v1beta1connect/project.go (1)

420-427: Consider validating principalType for clearer error responses.

Currently, an invalid principalType (e.g., "invalid") will result in an empty policy list and return ErrNotMember mapped to CodeNotFound. While functionally acceptable, this could be confusing for API consumers.

For consistency with SetProjectMemberRole (which validates principal type and returns ErrInvalidPrincipalTypeCodeInvalidArgument), consider adding similar validation to the service layer.

♻️ Suggested service-layer validation

In core/project/service.go, add validation at the start of RemoveMember:

 func (s Service) RemoveMember(ctx context.Context, projectID, principalID, principalType string) error {
+	switch principalType {
+	case schema.UserPrincipal, schema.ServiceUserPrincipal, schema.GroupPrincipal:
+		// valid
+	default:
+		return ErrInvalidPrincipalType
+	}
+
 	_, err := s.Get(ctx, projectID)

Then add error mapping in the handler:

 	switch {
 	case errors.Is(err, project.ErrNotExist):
 		return nil, connect.NewError(connect.CodeNotFound, ErrNotFound)
 	case errors.Is(err, project.ErrNotMember):
 		return nil, connect.NewError(connect.CodeNotFound, ErrNotFound)
+	case errors.Is(err, project.ErrInvalidPrincipalType):
+		return nil, connect.NewError(connect.CodeInvalidArgument, ErrBadRequest)
 	default:

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2bdc25d8-cf3e-405b-8cc9-62d7d73098fd

📥 Commits

Reviewing files that changed from the base of the PR and between 7bb0650 and 7168c24.

⛔ Files ignored due to path filters (2)
  • proto/v1beta1/frontier.pb.go is excluded by !**/*.pb.go, !proto/**
  • proto/v1beta1/frontierv1beta1connect/frontier.connect.go is excluded by !proto/**
📒 Files selected for processing (8)
  • Makefile
  • core/project/errors.go
  • core/project/service.go
  • core/project/service_test.go
  • internal/api/v1beta1connect/interfaces.go
  • internal/api/v1beta1connect/mocks/project_service.go
  • internal/api/v1beta1connect/project.go
  • pkg/server/connect_interceptors/authorization.go

@coveralls
Copy link
Copy Markdown

coveralls commented Apr 2, 2026

Pull Request Test Coverage Report for Build 23936358393

Details

  • 21 of 61 (34.43%) changed or added relevant lines in 3 files are covered.
  • 1 unchanged line in 1 file lost coverage.
  • Overall coverage decreased (-0.01%) to 41.238%

Changes Missing Coverage Covered Lines Changed/Added Lines %
core/project/service.go 21 25 84.0%
pkg/server/connect_interceptors/authorization.go 0 4 0.0%
internal/api/v1beta1connect/project.go 0 32 0.0%
Files with Coverage Reduction New Missed Lines %
internal/api/v1beta1connect/project.go 1 44.68%
Totals Coverage Status
Change from base Build 23898444727: -0.01%
Covered Lines: 14931
Relevant Lines: 36207

💛 - 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.

🧹 Nitpick comments (1)
internal/api/v1beta1connect/project.go (1)

407-433: Consider adding audit logging for member removal.

Other mutating project operations like CreateProject and UpdateProject emit audit events. For compliance and operational visibility, consider adding an audit log entry when a member is removed.

Example audit logging addition
 func (h *ConnectHandler) RemoveProjectMember(ctx context.Context, request *connect.Request[frontierv1beta1.RemoveProjectMemberRequest]) (*connect.Response[frontierv1beta1.RemoveProjectMemberResponse], error) {
 	errorLogger := NewErrorLogger()
+	auditor := audit.GetAuditor(ctx, "")
 
 	projectID := request.Msg.GetProjectId()
 	principalID := request.Msg.GetPrincipalId()
 	principalType := request.Msg.GetPrincipalType()
 
 	if err := h.projectService.RemoveMember(ctx, projectID, principalID, principalType); err != nil {
 		// ... error handling ...
 	}
 
+	auditor.Log(audit.ProjectMemberRemovedEvent, audit.ProjectTarget(projectID),
+		audit.WithMetadata(map[string]string{
+			"principal_id":   principalID,
+			"principal_type": principalType,
+		}))
 	return connect.NewResponse(&frontierv1beta1.RemoveProjectMemberResponse{}), nil
 }

Note: This would require defining audit.ProjectMemberRemovedEvent if it doesn't exist.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 259d821d-7444-4493-9a04-185dc1856806

📥 Commits

Reviewing files that changed from the base of the PR and between 7168c24 and 447d34e.

📒 Files selected for processing (3)
  • core/project/service.go
  • core/project/service_test.go
  • internal/api/v1beta1connect/project.go

@whoAbhishekSah
Copy link
Copy Markdown
Member Author

E2E Test Results — 33/33 PASS

Tested on clean database (fresh Frontier + SpiceDB).

Happy Path — 7/7 PASS

# Test Result
H1 Add user as project viewer, then remove PASS
H2 Verify user is gone from ListProjectUsers after removal PASS
H3 Add user with owner role, then remove PASS
H4 Add service user (app/serviceuser), then remove PASS
H5 Add group (app/group), then remove PASS
H6 Add user, change role (viewer→owner), then remove — should clean all policies PASS
H7 Verify user gone after role-change + removal PASS

Authorization — 7/7 PASS

# Caller Org Role Project Role Expected Result
A1 alice owner owner allowed PASS
A2 bob admin none (implicit) allowed PASS
A3 charlie member none permission_denied PASS
A4 charlie member viewer permission_denied PASS
A5 charlie member owner allowed PASS
A6 eve none none permission_denied PASS
A7 (no cookie) unauthenticated PASS

Edge Cases — 7/7 PASS

# Test Expected Result
E1 Remove user who was never added to project not_found PASS
E2a Remove user (first time) success PASS
E2b Remove same user again (double removal) not_found PASS
E3 Remove org member who has no project policies not_found PASS
E4 Remove service user with no project policies not_found PASS
E5 Remove group with no project policies not_found PASS
E6 Self-removal (alice removes herself from project) success PASS

Validation — 9/9 PASS

# Test Expected Result
V1 Invalid project_id (not UUID) invalid_argument PASS
V2 Non-existent project_id permission_denied PASS
V3 Invalid principal_id (not UUID) invalid_argument PASS
V4 Non-existent user principal_id not_found PASS
V5 Non-existent service user principal_id not_found PASS
V6 Non-existent group principal_id not_found PASS
V7 Invalid principal_type (app/invalid) invalid_argument PASS
V8 Empty principal_type invalid_argument PASS
V9 Empty body invalid_argument PASS

Org Admin Implicit Access — 3/3 PASS

# Test Result
I1 Org admin (implicit access) calls RemoveProjectMember PASS (allowed)
I2 Org admin (implicit access) calls SetProjectMemberRole PASS (allowed)
I3 Both RPCs consistent for org admin implicit access PASS

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.

🧹 Nitpick comments (1)
internal/api/v1beta1connect/project.go (1)

425-434: Consider using a handler-level error constant for consistency.

Line 429 uses project.ErrNotMember directly while other cases use handler-level constants (ErrProjectNotFound, ErrBadRequest). This leaks the service layer error to the client response.

For consistency with the rest of the handler and to maintain separation between service and API layers, consider defining a handler-level constant (e.g., ErrMemberNotFound).

♻️ Suggested fix

Define a constant (likely in errors.go or similar):

var ErrMemberNotFound = errors.New("principal is not a member of the project")

Then update the handler:

 case errors.Is(err, project.ErrNotMember):
-	return nil, connect.NewError(connect.CodeNotFound, project.ErrNotMember)
+	return nil, connect.NewError(connect.CodeNotFound, ErrMemberNotFound)

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6ed71a76-0ac3-462c-b9e0-23ce78506b0e

📥 Commits

Reviewing files that changed from the base of the PR and between 447d34e and 7b3dfa9.

📒 Files selected for processing (2)
  • core/audit/audit.go
  • internal/api/v1beta1connect/project.go

Add handler, service, and authorization for removing a principal
from a project. Supports user, service user, and group principals.

- Deletes all project-level policies for the principal
- Validates principal_type (returns invalid_argument for unknown types)
- Returns distinct errors for project not found vs member not found
- Authorization checks update permission on the project
- Audit logging for both SetProjectMemberRole and RemoveProjectMember
- 6 unit tests covering validation and success paths

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@whoAbhishekSah whoAbhishekSah force-pushed the feat/remove-project-member branch from d214d07 to 575d7d8 Compare April 3, 2026 06:17
@whoAbhishekSah whoAbhishekSah merged commit 1b73eaf into main Apr 3, 2026
8 checks passed
@whoAbhishekSah whoAbhishekSah deleted the feat/remove-project-member branch April 3, 2026 06:23
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.

Add high-level RPCs for project member management to remove client-side policy manipulation

3 participants