-
Notifications
You must be signed in to change notification settings - Fork 45
refactor(api): require resource in CheckResourcePermission #1886
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
7105710
27ee7b2
e5241f5
f00e7d3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -72,12 +72,12 @@ func (h *ConnectHandler) CheckFederatedResourcePermission(ctx context.Context, r | |
|
|
||
| objectNamespace, objectID, err := schema.SplitNamespaceAndResourceID(req.Msg.GetResource()) | ||
| if err != nil || objectNamespace == "" || objectID == "" { | ||
| return nil, connect.NewError(connect.CodeInvalidArgument, ErrBadRequest) | ||
| return nil, connect.NewError(connect.CodeInvalidArgument, ErrNamespaceSplitNotation) | ||
| } | ||
|
|
||
| principalNamespace, principalID, err := schema.SplitNamespaceAndResourceID(req.Msg.GetSubject()) | ||
| if err != nil || principalNamespace == "" || principalID == "" { | ||
| return nil, connect.NewError(connect.CodeInvalidArgument, ErrBadRequest) | ||
| return nil, connect.NewError(connect.CodeInvalidArgument, ErrNamespaceSplitNotation) | ||
| } | ||
|
|
||
| permissionName, err := h.getPermissionName(ctx, objectNamespace, req.Msg.GetPermission()) | ||
|
|
@@ -162,13 +162,8 @@ func (h *ConnectHandler) CheckResourcePermission(ctx context.Context, req *conne | |
| errorLogger := NewErrorLogger() | ||
|
|
||
| objectNamespace, objectID, err := schema.SplitNamespaceAndResourceID(req.Msg.GetResource()) | ||
| //nolint:staticcheck | ||
| if len(req.Msg.GetResource()) == 0 || err != nil { | ||
| objectNamespace = schema.ParseNamespaceAliasIfRequired(req.Msg.GetObjectNamespace()) | ||
| objectID = req.Msg.GetObjectId() | ||
| } | ||
| if objectNamespace == "" || objectID == "" { | ||
| return nil, connect.NewError(connect.CodeInvalidArgument, ErrBadRequest) | ||
| if err != nil || objectNamespace == "" || objectID == "" { | ||
| return nil, connect.NewError(connect.CodeInvalidArgument, ErrNamespaceSplitNotation) | ||
| } | ||
|
|
||
| permissionName, err := h.getPermissionName(ctx, objectNamespace, req.Msg.GetPermission()) | ||
|
|
@@ -201,8 +196,8 @@ func (h *ConnectHandler) BatchCheckPermission(ctx context.Context, req *connect. | |
| checks := make([]resource.Check, 0, len(req.Msg.GetBodies())) | ||
| for _, body := range req.Msg.GetBodies() { | ||
| objectNamespace, objectID, err := schema.SplitNamespaceAndResourceID(body.GetResource()) | ||
| if len(body.GetResource()) == 0 || err != nil { | ||
| return nil, connect.NewError(connect.CodeInvalidArgument, ErrBadRequest) | ||
| if err != nil || objectNamespace == "" || objectID == "" { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One malformed or empty-part resource in any body now makes the whole batch return InvalidArgument with zero results. For a batch endpoint that is surprising: 49 valid checks are dropped because of 1 bad body. Consider failing just that item, or returning a per-item error, rather than the whole call.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Keeping whole-batch rejection in this PR: that's the pre-existing contract for a malformed body here — before this change a malformed resource already failed the whole call, and an empty-part one reached SpiceDB and failed the whole call as internal. This PR only turns that into a clean InvalidArgument. Failing just the bad item needs a response-shape change (BatchCheckPermissionResponsePair has no per-item error field), so it's a proto addition rather than a handler tweak. Happy to take that as a follow-up if we want the per-item contract. |
||
| return nil, connect.NewError(connect.CodeInvalidArgument, ErrNamespaceSplitNotation) | ||
| } | ||
|
|
||
| permissionName, err := h.getPermissionName(ctx, objectNamespace, body.GetPermission()) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -33,12 +33,48 @@ func TestHandler_CheckResourcePermission(t *testing.T) { | |
| wantErr error | ||
| }{ | ||
| { | ||
| name: "should return bad request error if object id is empty or namespace is empty", | ||
| name: "should return bad request error if resource is malformed", | ||
| request: connect.NewRequest(&frontierv1beta1.CheckResourcePermissionRequest{ | ||
| Resource: "not-namespace-uuid-format", | ||
| }), | ||
| want: nil, | ||
| wantErr: connect.NewError(connect.CodeInvalidArgument, ErrBadRequest), | ||
| wantErr: connect.NewError(connect.CodeInvalidArgument, ErrNamespaceSplitNotation), | ||
| }, | ||
| { | ||
| name: "should return bad request error if resource is missing", | ||
| request: connect.NewRequest(&frontierv1beta1.CheckResourcePermissionRequest{ | ||
| Permission: schema.UpdatePermission, | ||
| }), | ||
| want: nil, | ||
| wantErr: connect.NewError(connect.CodeInvalidArgument, ErrNamespaceSplitNotation), | ||
| }, | ||
| { | ||
| name: "should return bad request error if resource id part is empty", | ||
| request: connect.NewRequest(&frontierv1beta1.CheckResourcePermissionRequest{ | ||
| Resource: "organization:", | ||
| Permission: schema.UpdatePermission, | ||
| }), | ||
| want: nil, | ||
| wantErr: connect.NewError(connect.CodeInvalidArgument, ErrNamespaceSplitNotation), | ||
| }, | ||
| { | ||
| name: "should return bad request error if resource namespace part is empty", | ||
| request: connect.NewRequest(&frontierv1beta1.CheckResourcePermissionRequest{ | ||
| Resource: ":" + testRelationV2.Object.ID, | ||
| Permission: schema.UpdatePermission, | ||
| }), | ||
| want: nil, | ||
| wantErr: connect.NewError(connect.CodeInvalidArgument, ErrNamespaceSplitNotation), | ||
| }, | ||
| { | ||
| name: "should return bad request error if only the removed split fields are sent", | ||
| request: connect.NewRequest(&frontierv1beta1.CheckResourcePermissionRequest{ | ||
| ObjectId: testRelationV2.Object.ID, | ||
| ObjectNamespace: testRelationV2.Object.Namespace, | ||
| Permission: schema.UpdatePermission, | ||
| }), | ||
| want: nil, | ||
| wantErr: connect.NewError(connect.CodeInvalidArgument, ErrNamespaceSplitNotation), | ||
| }, | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| { | ||
| name: "should return user unauthenticated error if CheckAuthz function returns ErrUnauthenticated", | ||
|
|
@@ -91,9 +127,8 @@ func TestHandler_CheckResourcePermission(t *testing.T) { | |
| Return(testPermission, nil) | ||
| }, | ||
| request: connect.NewRequest(&frontierv1beta1.CheckResourcePermissionRequest{ | ||
| ObjectId: testRelationV2.Object.ID, | ||
| ObjectNamespace: testRelationV2.Object.Namespace, | ||
| Permission: schema.UpdatePermission, | ||
| Permission: schema.UpdatePermission, | ||
| Resource: schema.JoinNamespaceAndResourceID(testRelationV2.Object.Namespace, testRelationV2.Object.ID), | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The success cases use canonical namespaces via JoinNamespaceAndResourceID, and none pass a real alias in the resource field. The PR says aliases still work there, so a case with resource set to an alias (e.g. "org:") would lock that promise in and catch any regression in ParseNamespaceAliasIfRequired for this path.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added in f00e7d3 — a unit case now sends |
||
| }), | ||
| want: connect.NewResponse(&frontierv1beta1.CheckResourcePermissionResponse{ | ||
| Status: true, | ||
|
|
@@ -113,15 +148,35 @@ func TestHandler_CheckResourcePermission(t *testing.T) { | |
| Return(testPermission, nil) | ||
| }, | ||
| request: connect.NewRequest(&frontierv1beta1.CheckResourcePermissionRequest{ | ||
| ObjectId: testRelationV2.Object.ID, | ||
| ObjectNamespace: testRelationV2.Object.Namespace, | ||
| Permission: schema.UpdatePermission, | ||
| Permission: schema.UpdatePermission, | ||
| Resource: schema.JoinNamespaceAndResourceID(testRelationV2.Object.Namespace, testRelationV2.Object.ID), | ||
| }), | ||
| want: connect.NewResponse(&frontierv1beta1.CheckResourcePermissionResponse{ | ||
| Status: false, | ||
| }), | ||
| wantErr: nil, | ||
| }, | ||
| { | ||
| name: "should resolve a namespace alias in the resource field", | ||
| setup: func(res *mocks.ResourceService, perm *mocks.PermissionService) { | ||
| res.EXPECT().CheckAuthz(mock.AnythingOfType("context.backgroundCtx"), resource.Check{ | ||
| Object: relation.Object{ | ||
| ID: testRelationV2.Object.ID, | ||
| Namespace: schema.OrganizationNamespace, | ||
| }, Permission: schema.UpdatePermission, | ||
| }).Return(true, nil) | ||
| perm.EXPECT().Get(mock.Anything, schema.JoinNamespaceAndResourceID(schema.OrganizationNamespace, schema.UpdatePermission)). | ||
| Return(permission.Permission{Name: schema.UpdatePermission, NamespaceID: schema.OrganizationNamespace}, nil) | ||
| }, | ||
| request: connect.NewRequest(&frontierv1beta1.CheckResourcePermissionRequest{ | ||
| Permission: schema.UpdatePermission, | ||
| Resource: "org:" + testRelationV2.Object.ID, | ||
| }), | ||
| want: connect.NewResponse(&frontierv1beta1.CheckResourcePermissionResponse{ | ||
| Status: true, | ||
| }), | ||
| wantErr: nil, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
|
|
@@ -138,3 +193,54 @@ func TestHandler_CheckResourcePermission(t *testing.T) { | |
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestHandler_BatchCheckPermission(t *testing.T) { | ||
| tests := []struct { | ||
| name string | ||
| setup func(res *mocks.ResourceService, perm *mocks.PermissionService) | ||
| request *connect.Request[frontierv1beta1.BatchCheckPermissionRequest] | ||
| wantErr error | ||
| }{ | ||
| { | ||
| name: "should return bad request error if a body resource is malformed", | ||
| request: connect.NewRequest(&frontierv1beta1.BatchCheckPermissionRequest{ | ||
| Bodies: []*frontierv1beta1.BatchCheckPermissionBody{ | ||
| {Resource: "not-namespace-uuid-format", Permission: schema.UpdatePermission}, | ||
| }, | ||
| }), | ||
| wantErr: connect.NewError(connect.CodeInvalidArgument, ErrNamespaceSplitNotation), | ||
| }, | ||
| { | ||
| name: "should return bad request error if a body resource id part is empty", | ||
| request: connect.NewRequest(&frontierv1beta1.BatchCheckPermissionRequest{ | ||
| Bodies: []*frontierv1beta1.BatchCheckPermissionBody{ | ||
| {Resource: "organization:", Permission: schema.UpdatePermission}, | ||
| }, | ||
| }), | ||
| wantErr: connect.NewError(connect.CodeInvalidArgument, ErrNamespaceSplitNotation), | ||
| }, | ||
| { | ||
| name: "should return bad request error if a body resource namespace part is empty", | ||
| request: connect.NewRequest(&frontierv1beta1.BatchCheckPermissionRequest{ | ||
| Bodies: []*frontierv1beta1.BatchCheckPermissionBody{ | ||
| {Resource: ":" + testRelationV2.Object.ID, Permission: schema.UpdatePermission}, | ||
| }, | ||
| }), | ||
| wantErr: connect.NewError(connect.CodeInvalidArgument, ErrNamespaceSplitNotation), | ||
| }, | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| mockResourceSrv := new(mocks.ResourceService) | ||
| mockPermissionSrv := new(mocks.PermissionService) | ||
| if tt.setup != nil { | ||
| tt.setup(mockResourceSrv, mockPermissionSrv) | ||
| } | ||
|
|
||
| mockDep := &ConnectHandler{resourceService: mockResourceSrv, permissionService: mockPermissionSrv} | ||
| resp, err := mockDep.BatchCheckPermission(context.Background(), tt.request) | ||
| assert.Equal(t, tt.wantErr, err) | ||
| assert.Nil(t, resp) | ||
| }) | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This drops the fallback to object_id / object_namespace, so a client sending only those (with no resource) now gets InvalidArgument. That is the intended tightening, but those fields are still in the .proto, so the schema still advertises support the server no longer provides. A clear deprecation note in the proto, or a planned removal, would keep integrations from being surprised at runtime.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Proto will be cleaned up later
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding to that: the fields already carry
deprecated = truein the proto, so the schema does flag it — the generated getters are marked deprecated too. The removal itself rides a later proton sync.