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
74 changes: 54 additions & 20 deletions internal/api/v1beta1connect/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,42 +131,76 @@ func (h *ConnectHandler) UpdateProjectResource(ctx context.Context, request *con
}

func (h *ConnectHandler) DeleteProjectResource(ctx context.Context, request *connect.Request[frontierv1beta1.DeleteProjectResourceRequest]) (*connect.Response[frontierv1beta1.DeleteProjectResourceResponse], error) {
errorLogger := NewErrorLogger()
resourceID := request.Msg.GetId()

resourceToDel, err := h.resourceService.Get(ctx, resourceID)
resourceToDel, err := h.getResourceToDelete(ctx, request, "DeleteProjectResource", resourceID)
if err != nil {
errorLogger.LogServiceError(ctx, request, "DeleteProjectResource.GetResource", err,
"resource_id", resourceID)
return nil, err
}

parentProject, err := h.projectService.Get(ctx, resourceToDel.ProjectID)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("DeleteProjectResource.GetProject: resource_id=%s project_id=%s: %w", resourceID, resourceToDel.ProjectID, err))
}

if err := h.deleteResource(ctx, request, "DeleteProjectResource", resourceID, resourceToDel,
"project_id", resourceToDel.ProjectID, parentProject.Organization.ID); err != nil {
return nil, err
}
return connect.NewResponse(&frontierv1beta1.DeleteProjectResourceResponse{}), nil
}

func (h *ConnectHandler) DeleteOrganizationResource(ctx context.Context, request *connect.Request[frontierv1beta1.DeleteOrganizationResourceRequest]) (*connect.Response[frontierv1beta1.DeleteOrganizationResourceResponse], error) {
resourceID := request.Msg.GetId()

resourceToDel, err := h.getResourceToDelete(ctx, request, "DeleteOrganizationResource", resourceID)
if err != nil {
return nil, err
}

if err := h.deleteResource(ctx, request, "DeleteOrganizationResource", resourceID, resourceToDel,
"org_id", resourceToDel.OrgID, resourceToDel.OrgID); err != nil {
return nil, err
}
return connect.NewResponse(&frontierv1beta1.DeleteOrganizationResourceResponse{}), nil
}

func (h *ConnectHandler) getResourceToDelete(ctx context.Context, request connect.AnyRequest, method, id string) (resource.Resource, error) {
res, err := h.resourceService.Get(ctx, id)
if err != nil {
NewErrorLogger().LogServiceError(ctx, request, method+".GetResource", err,
"resource_id", id)

switch {
case errors.Is(err, resource.ErrNotExist),
errors.Is(err, resource.ErrInvalidID),
errors.Is(err, resource.ErrInvalidUUID):
return nil, connect.NewError(connect.CodeNotFound, ErrResourceNotFound)
return resource.Resource{}, connect.NewError(connect.CodeNotFound, ErrResourceNotFound)
default:
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("DeleteProjectResource.GetResource: resource_id=%s: %w", resourceID, err))
return resource.Resource{}, connect.NewError(connect.CodeInternal, fmt.Errorf("%s.GetResource: resource_id=%s: %w", method, id, err))
}
}
return res, nil
}

parentProject, err := h.projectService.Get(ctx, resourceToDel.ProjectID)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("DeleteProjectResource.GetProject: resource_id=%s project_id=%s: %w", resourceID, resourceToDel.ProjectID, err))
}

err = h.resourceService.Delete(ctx, resourceToDel.NamespaceID, resourceToDel.ID)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("DeleteProjectResource: resource_id=%s project_id=%s namespace=%s: %w", resourceID, resourceToDel.ProjectID, resourceToDel.NamespaceID, err))
func (h *ConnectHandler) deleteResource(ctx context.Context, request connect.AnyRequest, method, id string,
res resource.Resource, parentKey, parentValue, auditOrgID string,
) error {
if err := h.resourceService.Delete(ctx, res.NamespaceID, res.ID); err != nil {
if errors.Is(err, resource.ErrNotExist) {
return connect.NewError(connect.CodeNotFound, ErrResourceNotFound)
}
return connect.NewError(connect.CodeInternal, fmt.Errorf("%s: resource_id=%s %s=%s namespace=%s: %w", method, id, parentKey, parentValue, res.NamespaceID, err))
Comment thread
rohilsurana marked this conversation as resolved.
Resolved
}

if err := audit.GetAuditor(ctx, parentProject.Organization.ID).Log(audit.ResourceDeletedEvent, audit.Target{
ID: resourceID,
Type: resourceToDel.NamespaceID,
if err := audit.GetAuditor(ctx, auditOrgID).Log(audit.ResourceDeletedEvent, audit.Target{
ID: id,
Type: res.NamespaceID,
}); err != nil {
errorLogger.LogServiceError(ctx, request, "DeleteProjectResource.AuditLog", err,
"resource_id", resourceID)
NewErrorLogger().LogServiceError(ctx, request, method+".AuditLog", err,
"resource_id", id)
}
return connect.NewResponse(&frontierv1beta1.DeleteProjectResourceResponse{}), nil
return nil
}

func (h *ConnectHandler) CreateOrganizationResource(ctx context.Context, request *connect.Request[frontierv1beta1.CreateOrganizationResourceRequest]) (*connect.Response[frontierv1beta1.CreateOrganizationResourceResponse], error) {
Expand Down
94 changes: 94 additions & 0 deletions internal/api/v1beta1connect/resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,82 @@ func TestConnectHandler_UpdateOrganizationResource(t *testing.T) {
}
}

func TestConnectHandler_DeleteOrganizationResource(t *testing.T) {
orgResource := resource.Resource{
ID: utils.NewString(),
Name: "store-1",
OrgID: testOrgID,
NamespaceID: testNSID,
PrincipalID: testUserID,
PrincipalType: schema.UserPrincipal,
}
request := connect.NewRequest(&frontierv1beta1.DeleteOrganizationResourceRequest{
Id: orgResource.ID,
OrgId: testOrgID,
})

tests := []struct {
name string
setup func(rs *mocks.ResourceService)
wantErr error
}{
{
name: "should return not found if the resource does not exist",
setup: func(rs *mocks.ResourceService) {
rs.EXPECT().Get(mock.Anything, orgResource.ID).Return(resource.Resource{}, resource.ErrNotExist)
},
wantErr: connect.NewError(connect.CodeNotFound, ErrResourceNotFound),
},
{
name: "should return internal error if the resource lookup fails",
setup: func(rs *mocks.ResourceService) {
rs.EXPECT().Get(mock.Anything, orgResource.ID).Return(resource.Resource{}, errors.New("test error"))
},
wantErr: connect.NewError(connect.CodeInternal, fmt.Errorf("DeleteOrganizationResource.GetResource: resource_id=%s: %w", orgResource.ID, errors.New("test error"))),
},
{
name: "should return internal error if the delete fails",
setup: func(rs *mocks.ResourceService) {
rs.EXPECT().Get(mock.Anything, orgResource.ID).Return(orgResource, nil)
rs.EXPECT().Delete(mock.Anything, testNSID, orgResource.ID).Return(errors.New("delete error"))
},
wantErr: connect.NewError(connect.CodeInternal, fmt.Errorf("DeleteOrganizationResource: resource_id=%s org_id=%s namespace=%s: %w", orgResource.ID, testOrgID, testNSID, errors.New("delete error"))),
},
{
name: "should return not found if the resource is deleted after the lookup",
setup: func(rs *mocks.ResourceService) {
rs.EXPECT().Get(mock.Anything, orgResource.ID).Return(orgResource, nil)
rs.EXPECT().Delete(mock.Anything, testNSID, orgResource.ID).Return(resource.ErrNotExist)
},
wantErr: connect.NewError(connect.CodeNotFound, ErrResourceNotFound),
},
{
name: "should delete the resource",
setup: func(rs *mocks.ResourceService) {
rs.EXPECT().Get(mock.Anything, orgResource.ID).Return(orgResource, nil)
rs.EXPECT().Delete(mock.Anything, testNSID, orgResource.ID).Return(nil)
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockResourceSrv := mocks.NewResourceService(t)
tt.setup(mockResourceSrv)
h := ConnectHandler{resourceService: mockResourceSrv}
resp, err := h.DeleteOrganizationResource(context.Background(), request)
if tt.wantErr != nil {
assert.Error(t, err)
assert.Equal(t, tt.wantErr.(*connect.Error).Code(), err.(*connect.Error).Code())
assert.Equal(t, tt.wantErr.(*connect.Error).Message(), err.(*connect.Error).Message())
} else {
assert.NoError(t, err)
assert.EqualValues(t, connect.NewResponse(&frontierv1beta1.DeleteOrganizationResourceResponse{}), resp)
}
})
}
}

func TestConnectHandler_GetProjectResource(t *testing.T) {
tests := []struct {
name string
Expand Down Expand Up @@ -1107,6 +1183,24 @@ func TestConnectHandler_DeleteProjectResource(t *testing.T) {
want: nil,
wantErr: connect.NewError(connect.CodeInternal, fmt.Errorf("DeleteProjectResource: resource_id=%s project_id=%s namespace=%s: %w", testResource.ID, testResource.ProjectID, testResource.NamespaceID, errors.New("delete error"))),
},
{
name: "should return not found if the resource is deleted after the lookup",
setup: func(rs *mocks.ResourceService, ps *mocks.ProjectService) {
rs.EXPECT().Get(mock.AnythingOfType("context.backgroundCtx"), testResource.ID).Return(testResource, nil)
ps.EXPECT().Get(mock.AnythingOfType("context.backgroundCtx"), testResource.ProjectID).Return(project.Project{
ID: testResource.ProjectID,
Organization: organization.Organization{
ID: "test-org-id",
},
}, nil)
rs.EXPECT().Delete(mock.AnythingOfType("context.backgroundCtx"), testResource.NamespaceID, testResource.ID).Return(resource.ErrNotExist)
},
request: connect.NewRequest(&frontierv1beta1.DeleteProjectResourceRequest{
Id: testResource.ID,
}),
want: nil,
wantErr: connect.NewError(connect.CodeNotFound, ErrResourceNotFound),
},
{
name: "should return success if resource is deleted successfully",
setup: func(rs *mocks.ResourceService, ps *mocks.ProjectService) {
Expand Down
18 changes: 18 additions & 0 deletions pkg/server/connect_interceptors/authorization.go
Original file line number Diff line number Diff line change
Expand Up @@ -809,6 +809,24 @@ var authorizationValidationMap = map[string]func(ctx context.Context, handler *v
ID: pbreq.Msg.GetOrgId(),
}, req)
},
"/raystack.frontier.v1beta1.FrontierService/DeleteOrganizationResource": func(ctx context.Context, handler *v1beta1connect.ConnectHandler, req connect.AnyRequest) error {
pbreq := req.(*connect.Request[frontierv1beta1.DeleteOrganizationResourceRequest])
res, err := ensureResourceBelongToOrg(ctx, handler, pbreq.Msg.GetOrgId(), pbreq.Msg.GetId())
if err != nil {
return err
}
isAuthed := handler.IsAuthorized(ctx, relation.Object{
Namespace: res.GetNamespace(), ID: res.GetId(),
}, schema.DeletePermission, req)
if isAuthed != nil {
return isAuthed
}

return handler.CheckPlanEntitlement(ctx, relation.Object{
Namespace: schema.OrganizationNamespace,
ID: pbreq.Msg.GetOrgId(),
}, req)
},

// audit records
frontierv1beta1connect.FrontierServiceCreateAuditRecordProcedure: func(ctx context.Context, handler *v1beta1connect.ConnectHandler, req connect.AnyRequest) error {
Expand Down
79 changes: 79 additions & 0 deletions test/e2e/regression/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2557,6 +2557,85 @@ func (s *APIRegressionTestSuite) TestResourceAPI() {
_, err = s.testBench.Client.UpdateOrganizationResource(ctxOrgAdminAuth, updateRequest(resourceID, orgID, "Frozen"))
s.Assert().NoError(err)
})
s.Run("7. deleting an org resource removes access and frees its name", func() {
createOrgResp, err := s.testBench.Client.CreateOrganization(ctxOrgAdminAuth, connect.NewRequest(&frontierv1beta1.CreateOrganizationRequest{
Body: &frontierv1beta1.OrganizationRequestBody{Title: "org 7", Name: "org-resource-7"},
}))
s.Require().NoError(err)
orgID := createOrgResp.Msg.GetOrganization().GetId()

creatorEmail := "user-org-resource-7-creator@raystack.org"
memberEmail := "user-org-resource-7-member@raystack.org"
var memberIDs []string
for _, u := range []struct{ email, name string }{
{creatorEmail, "user_org_resource_7_creator"},
{memberEmail, "user_org_resource_7_member"},
} {
userResp, err := s.testBench.Client.CreateUser(ctxOrgAdminAuth, connect.NewRequest(&frontierv1beta1.CreateUserRequest{Body: &frontierv1beta1.UserRequestBody{
Title: u.name, Email: u.email, Name: u.name,
}}))
s.Require().NoError(err)
memberIDs = append(memberIDs, userResp.Msg.GetUser().GetId())
}
addMembersResp, err := s.testBench.AdminClient.AddOrganizationMembers(ctxOrgAdminAuth, connect.NewRequest(&frontierv1beta1.AddOrganizationMembersRequest{
OrgId: orgID,
Members: []*frontierv1beta1.OrgMemberEntry{
{UserId: memberIDs[0], RoleId: s.orgViewerRole},
{UserId: memberIDs[1], RoleId: s.orgViewerRole},
},
}))
requireAddOrgMembersSuccess(s.T(), addMembersResp, err)

creatorCookie, err := testbench.AuthenticateUser(context.Background(), s.testBench.Client, creatorEmail)
s.Require().NoError(err)
ctxCreator := testbench.ContextWithAuth(context.Background(), creatorCookie)
memberCookie, err := testbench.AuthenticateUser(context.Background(), s.testBench.Client, memberEmail)
s.Require().NoError(err)
ctxMember := testbench.ContextWithAuth(context.Background(), memberCookie)

createRequest := func() *connect.Request[frontierv1beta1.CreateOrganizationResourceRequest] {
return connect.NewRequest(&frontierv1beta1.CreateOrganizationResourceRequest{
OrgId: orgID,
Body: &frontierv1beta1.ResourceRequestBody{Name: "store-1", Namespace: computeOrderNamespace},
})
}
createResp, err := s.testBench.Client.CreateOrganizationResource(ctxCreator, createRequest())
s.Require().NoError(err)
resourceID := createResp.Msg.GetResource().GetId()

deleteRequest := func(id string) *connect.Request[frontierv1beta1.DeleteOrganizationResourceRequest] {
return connect.NewRequest(&frontierv1beta1.DeleteOrganizationResourceRequest{Id: id, OrgId: orgID})
}

_, err = s.testBench.Client.DeleteOrganizationResource(ctxMember, deleteRequest(resourceID))
s.Assert().Equal(connect.CodePermissionDenied, connect.CodeOf(err))

_, err = s.testBench.Client.DeleteOrganizationResource(ctxCreator, deleteRequest(resourceID))
s.Require().NoError(err)

_, err = s.testBench.Client.DeleteOrganizationResource(ctxCreator, deleteRequest(resourceID))
s.Assert().Equal(connect.CodeNotFound, connect.CodeOf(err))

checkResp, err := s.testBench.AdminClient.CheckFederatedResourcePermission(ctxOrgAdminAuth, connect.NewRequest(&frontierv1beta1.CheckFederatedResourcePermissionRequest{
Resource: schema.JoinNamespaceAndResourceID(computeOrderNamespace, resourceID),
Permission: schema.UpdatePermission,
Subject: schema.JoinNamespaceAndResourceID(schema.UserPrincipal, memberIDs[0]),
}))
s.Require().NoError(err)
s.Assert().False(checkResp.Msg.GetStatus())

recreateResp, err := s.testBench.Client.CreateOrganizationResource(ctxCreator, createRequest())
s.Require().NoError(err)
recreatedID := recreateResp.Msg.GetResource().GetId()
s.Assert().NotEqual(resourceID, recreatedID)

_, err = s.testBench.Client.DisableOrganization(ctxOrgAdminAuth, connect.NewRequest(&frontierv1beta1.DisableOrganizationRequest{Id: orgID}))
s.Require().NoError(err)
_, err = s.testBench.Client.DeleteOrganizationResource(ctxCreator, deleteRequest(recreatedID))
s.Assert().Equal(connect.CodeFailedPrecondition, connect.CodeOf(err))
_, err = s.testBench.Client.DeleteOrganizationResource(ctxOrgAdminAuth, deleteRequest(recreatedID))
s.Assert().NoError(err)
})
}

func (s *APIRegressionTestSuite) TestPolicyAPI() {
Expand Down
Loading