Skip to content

Commit 6d04de0

Browse files
authored
feat: add api to list service user projects (#862)
* chore: update protos * feat: add api to list service user projects * tests: add basic test for list service user api * test: add test for withPermission query * chore: update proto commit from main branch
1 parent 9ddd599 commit 6d04de0

File tree

9 files changed

+5979
-4766
lines changed

9 files changed

+5979
-4766
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ TAG := $(shell git rev-list --tags --max-count=1)
44
VERSION := $(shell git describe --tags ${TAG})
55
.PHONY: build check fmt lint test test-race vet test-cover-html help install proto ui compose-up-dev
66
.DEFAULT_GOAL := build
7-
PROTON_COMMIT := "251281aa0c311904eb263eb9bfa7e3b9c41f99b0"
7+
PROTON_COMMIT := "2750c7e5fe37ace10c463068089b57244d6245b9"
88

99
ui:
1010
@echo " > generating ui build"

internal/api/v1beta1/serviceuser.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,12 @@ import (
55
"encoding/json"
66

77
"github.com/raystack/frontier/core/audit"
8+
"github.com/raystack/frontier/core/project"
9+
"github.com/raystack/frontier/core/relation"
810
"github.com/raystack/frontier/core/serviceuser"
11+
"github.com/raystack/frontier/internal/bootstrap/schema"
912
"github.com/raystack/frontier/pkg/metadata"
13+
"github.com/raystack/frontier/pkg/utils"
1014
frontierv1beta1 "github.com/raystack/frontier/proto/v1beta1"
1115
"google.golang.org/grpc/codes"
1216
"google.golang.org/grpc/status"
@@ -303,6 +307,56 @@ func (h Handler) DeleteServiceUserToken(ctx context.Context, request *frontierv1
303307
return &frontierv1beta1.DeleteServiceUserTokenResponse{}, nil
304308
}
305309

310+
func (h Handler) ListServiceUserProjects(ctx context.Context, request *frontierv1beta1.ListServiceUserProjectsRequest) (*frontierv1beta1.ListServiceUserProjectsResponse, error) {
311+
projList, err := h.projectService.ListByUser(ctx, request.GetId(), schema.ServiceUserPrincipal, project.Filter{
312+
OrgID: request.GetOrgId(),
313+
})
314+
if err != nil {
315+
return nil, err
316+
}
317+
318+
var projects []*frontierv1beta1.Project
319+
var accessPairsPb []*frontierv1beta1.ListServiceUserProjectsResponse_AccessPair
320+
for _, v := range projList {
321+
projPB, err := transformProjectToPB(v)
322+
if err != nil {
323+
return nil, err
324+
}
325+
projects = append(projects, projPB)
326+
}
327+
328+
if len(request.GetWithPermissions()) > 0 {
329+
resourceIds := utils.Map(projList, func(res project.Project) string {
330+
return res.ID
331+
})
332+
successCheckPairs, err := h.fetchAccessPairsOnResource(ctx, schema.ProjectNamespace, resourceIds, request.GetWithPermissions())
333+
if err != nil {
334+
return nil, err
335+
}
336+
for _, successCheck := range successCheckPairs {
337+
resID := successCheck.Relation.Object.ID
338+
339+
// find all permission checks on same resource
340+
pairsForCurrentGroup := utils.Filter(successCheckPairs, func(pair relation.CheckPair) bool {
341+
return pair.Relation.Object.ID == resID
342+
})
343+
// fetch permissions
344+
permissions := utils.Map(pairsForCurrentGroup, func(pair relation.CheckPair) string {
345+
return pair.Relation.RelationName
346+
})
347+
accessPairsPb = append(accessPairsPb, &frontierv1beta1.ListServiceUserProjectsResponse_AccessPair{
348+
ProjectId: resID,
349+
Permissions: permissions,
350+
})
351+
}
352+
}
353+
354+
return &frontierv1beta1.ListServiceUserProjectsResponse{
355+
Projects: projects,
356+
AccessPairs: accessPairsPb,
357+
}, nil
358+
}
359+
306360
func transformServiceUserToPB(usr serviceuser.ServiceUser) (*frontierv1beta1.ServiceUser, error) {
307361
metaData, err := usr.Metadata.ToStructPB()
308362
if err != nil {

internal/api/v1beta1/serviceuser_test.go

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,16 @@ import (
66
"testing"
77
"time"
88

9+
"github.com/google/uuid"
910
"github.com/lestrrat-go/jwx/v2/jwk"
11+
"github.com/raystack/frontier/core/organization"
12+
"github.com/raystack/frontier/core/permission"
13+
"github.com/raystack/frontier/core/project"
14+
"github.com/raystack/frontier/core/relation"
15+
"github.com/raystack/frontier/core/resource"
1016
"github.com/raystack/frontier/core/serviceuser"
1117
"github.com/raystack/frontier/internal/api/v1beta1/mocks"
18+
"github.com/raystack/frontier/internal/bootstrap/schema"
1219
"github.com/raystack/frontier/pkg/metadata"
1320
frontierv1beta1 "github.com/raystack/frontier/proto/v1beta1"
1421
"github.com/stretchr/testify/assert"
@@ -722,3 +729,217 @@ func TestHandler_CreateServiceUserCredential(t *testing.T) {
722729
})
723730
}
724731
}
732+
733+
func TestHandler_ListServiceUserProjects(t *testing.T) {
734+
testProjectMap := map[string]project.Project{
735+
"ab657ae7-8c9e-45eb-9862-dd9ceb6d5c71": {
736+
ID: "ab657ae7-8c9e-45eb-9862-dd9ceb6d5c71",
737+
Name: "prj-1",
738+
Metadata: metadata.Metadata{
739+
"email": "[email protected]",
740+
},
741+
Organization: organization.Organization{
742+
ID: testOrgID,
743+
},
744+
CreatedAt: time.Time{},
745+
UpdatedAt: time.Time{},
746+
},
747+
"c7772c63-fca4-4c7c-bf93-c8f85115de4b": {
748+
ID: "c7772c63-fca4-4c7c-bf93-c8f85115de4b",
749+
Name: "prj-2",
750+
Metadata: metadata.Metadata{
751+
"email": "[email protected]",
752+
},
753+
Organization: organization.Organization{
754+
ID: testOrgID,
755+
},
756+
CreatedAt: time.Time{},
757+
UpdatedAt: time.Time{},
758+
},
759+
}
760+
761+
tests := []struct {
762+
name string
763+
setup func(projSvc *mocks.ProjectService, permSvc *mocks.PermissionService, resourceSvc *mocks.ResourceService)
764+
request *frontierv1beta1.ListServiceUserProjectsRequest
765+
want *frontierv1beta1.ListServiceUserProjectsResponse
766+
wantErr error
767+
}{
768+
{
769+
name: "should return internal server error when list service user project returns error",
770+
request: &frontierv1beta1.ListServiceUserProjectsRequest{
771+
Id: "1",
772+
},
773+
setup: func(projSvc *mocks.ProjectService, permSvc *mocks.PermissionService, resourceSvc *mocks.ResourceService) {
774+
projSvc.EXPECT().ListByUser(mock.AnythingOfType("context.backgroundCtx"), "1", schema.ServiceUserPrincipal, project.Filter{}).Return(nil, errors.New("test error"))
775+
},
776+
want: nil,
777+
wantErr: errors.New("test error"),
778+
},
779+
{
780+
name: "should return project list when there is no error",
781+
request: &frontierv1beta1.ListServiceUserProjectsRequest{
782+
Id: "1",
783+
},
784+
setup: func(projSvc *mocks.ProjectService, permSvc *mocks.PermissionService, resourceSvc *mocks.ResourceService) {
785+
var projects []project.Project
786+
787+
for _, projectID := range testProjectIDList {
788+
projects = append(projects, testProjectMap[projectID])
789+
}
790+
projSvc.EXPECT().ListByUser(mock.AnythingOfType("context.backgroundCtx"), "1", schema.ServiceUserPrincipal, project.Filter{}).Return(projects, nil)
791+
},
792+
want: &frontierv1beta1.ListServiceUserProjectsResponse{
793+
Projects: []*frontierv1beta1.Project{{
794+
Id: "ab657ae7-8c9e-45eb-9862-dd9ceb6d5c71",
795+
Name: "prj-1",
796+
Metadata: &structpb.Struct{
797+
Fields: map[string]*structpb.Value{
798+
"email": structpb.NewStringValue("[email protected]"),
799+
},
800+
},
801+
OrgId: "9f256f86-31a3-11ec-8d3d-0242ac130003",
802+
CreatedAt: timestamppb.New(time.Time{}),
803+
UpdatedAt: timestamppb.New(time.Time{}),
804+
},
805+
{
806+
Id: "c7772c63-fca4-4c7c-bf93-c8f85115de4b",
807+
Name: "prj-2",
808+
Metadata: &structpb.Struct{
809+
Fields: map[string]*structpb.Value{
810+
"email": structpb.NewStringValue("[email protected]"),
811+
},
812+
},
813+
OrgId: "9f256f86-31a3-11ec-8d3d-0242ac130003",
814+
CreatedAt: timestamppb.New(time.Time{}),
815+
UpdatedAt: timestamppb.New(time.Time{}),
816+
},
817+
},
818+
},
819+
wantErr: nil,
820+
},
821+
{
822+
name: "should return project list with access pairs if withPermission is passed",
823+
request: &frontierv1beta1.ListServiceUserProjectsRequest{
824+
Id: "1",
825+
WithPermissions: []string{"get"},
826+
},
827+
setup: func(projSvc *mocks.ProjectService, permSvc *mocks.PermissionService, resourceSvc *mocks.ResourceService) {
828+
var projects []project.Project
829+
830+
for _, projectID := range testProjectIDList {
831+
projects = append(projects, testProjectMap[projectID])
832+
}
833+
834+
ctx := mock.AnythingOfType("context.backgroundCtx")
835+
projSvc.EXPECT().ListByUser(ctx, "1", schema.ServiceUserPrincipal, project.Filter{}).Return(projects, nil)
836+
837+
permSvc.EXPECT().Get(ctx, "app/project:get").Return(
838+
permission.Permission{
839+
ID: uuid.New().String(),
840+
Name: "get",
841+
NamespaceID: "app/project",
842+
Metadata: map[string]any{},
843+
CreatedAt: time.Time{},
844+
UpdatedAt: time.Time{},
845+
}, nil)
846+
847+
resourceSvc.EXPECT().BatchCheck(ctx, []resource.Check{
848+
{
849+
Object: relation.Object{
850+
ID: "ab657ae7-8c9e-45eb-9862-dd9ceb6d5c71",
851+
Namespace: "app/project",
852+
},
853+
Permission: "get",
854+
},
855+
{
856+
Object: relation.Object{
857+
ID: "c7772c63-fca4-4c7c-bf93-c8f85115de4b",
858+
Namespace: "app/project",
859+
},
860+
Permission: "get",
861+
},
862+
}).Return([]relation.CheckPair{
863+
{
864+
Relation: relation.Relation{
865+
Object: relation.Object{
866+
ID: "ab657ae7-8c9e-45eb-9862-dd9ceb6d5c71",
867+
Namespace: "app/project",
868+
},
869+
RelationName: "get",
870+
},
871+
Status: true,
872+
},
873+
{
874+
Relation: relation.Relation{
875+
Object: relation.Object{
876+
ID: "c7772c63-fca4-4c7c-bf93-c8f85115de4b",
877+
Namespace: "app/project",
878+
},
879+
RelationName: "get",
880+
},
881+
Status: true,
882+
},
883+
}, nil)
884+
},
885+
want: &frontierv1beta1.ListServiceUserProjectsResponse{
886+
Projects: []*frontierv1beta1.Project{{
887+
Id: "ab657ae7-8c9e-45eb-9862-dd9ceb6d5c71",
888+
Name: "prj-1",
889+
Metadata: &structpb.Struct{
890+
Fields: map[string]*structpb.Value{
891+
"email": structpb.NewStringValue("[email protected]"),
892+
},
893+
},
894+
OrgId: "9f256f86-31a3-11ec-8d3d-0242ac130003",
895+
CreatedAt: timestamppb.New(time.Time{}),
896+
UpdatedAt: timestamppb.New(time.Time{}),
897+
},
898+
{
899+
Id: "c7772c63-fca4-4c7c-bf93-c8f85115de4b",
900+
Name: "prj-2",
901+
Metadata: &structpb.Struct{
902+
Fields: map[string]*structpb.Value{
903+
"email": structpb.NewStringValue("[email protected]"),
904+
},
905+
},
906+
OrgId: "9f256f86-31a3-11ec-8d3d-0242ac130003",
907+
CreatedAt: timestamppb.New(time.Time{}),
908+
UpdatedAt: timestamppb.New(time.Time{}),
909+
},
910+
},
911+
AccessPairs: []*frontierv1beta1.ListServiceUserProjectsResponse_AccessPair{
912+
{
913+
ProjectId: "ab657ae7-8c9e-45eb-9862-dd9ceb6d5c71",
914+
Permissions: []string{"get"},
915+
},
916+
{
917+
ProjectId: "c7772c63-fca4-4c7c-bf93-c8f85115de4b",
918+
Permissions: []string{"get"},
919+
},
920+
},
921+
},
922+
wantErr: nil,
923+
},
924+
}
925+
926+
for _, tt := range tests {
927+
t.Run(tt.name, func(t *testing.T) {
928+
mockProjectSvc := new(mocks.ProjectService)
929+
mockPermssionSvc := new(mocks.PermissionService)
930+
mockResourceSvc := new(mocks.ResourceService)
931+
932+
if tt.setup != nil {
933+
tt.setup(mockProjectSvc, mockPermssionSvc, mockResourceSvc)
934+
}
935+
h := Handler{
936+
projectService: mockProjectSvc,
937+
permissionService: mockPermssionSvc,
938+
resourceService: mockResourceSvc,
939+
}
940+
got, err := h.ListServiceUserProjects(context.Background(), tt.request)
941+
assert.EqualValues(t, tt.want, got)
942+
assert.EqualValues(t, tt.wantErr, err)
943+
})
944+
}
945+
}

pkg/server/interceptors/authorization.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,19 @@ var authorizationValidationMap = map[string]func(ctx context.Context, handler *v
213213
}
214214
return handler.IsAuthorized(ctx, relation.Object{Namespace: schema.OrganizationNamespace, ID: pbreq.GetOrgId()}, schema.ServiceUserManagePermission)
215215
},
216+
"/raystack.frontier.v1beta1.FrontierService/ListServiceUserProjects": func(ctx context.Context, handler *v1beta1.Handler, req any) error {
217+
pbreq := req.(*frontierv1beta1.ListServiceUserProjectsRequest)
218+
svuser, err := handler.GetServiceUser(ctx, &frontierv1beta1.GetServiceUserRequest{
219+
Id: pbreq.GetId(),
220+
})
221+
if err != nil {
222+
return err
223+
}
224+
if pbreq.GetOrgId() != svuser.GetServiceuser().GetOrgId() {
225+
return ErrDeniedInvalidArgs
226+
}
227+
return handler.IsAuthorized(ctx, relation.Object{Namespace: schema.OrganizationNamespace, ID: pbreq.GetOrgId()}, schema.ServiceUserManagePermission)
228+
},
216229
"/raystack.frontier.v1beta1.FrontierService/ListServiceUserJWKs": func(ctx context.Context, handler *v1beta1.Handler, req any) error {
217230
pbreq := req.(*frontierv1beta1.ListServiceUserJWKsRequest)
218231
return handler.IsAuthorized(ctx, relation.Object{Namespace: schema.ServiceUserPrincipal, ID: pbreq.GetId()}, schema.ManagePermission)

0 commit comments

Comments
 (0)