Skip to content

Commit 0e51dbf

Browse files
aryan-bhokaredependabot[bot]imrajdas
authored
[WIP] : Multiple project owner backend. (#4536)
* Modified db schema of Owner. Signed-off-by: aryan <aryan1bhokare@gmail.com> * Added new API GetProjectOwners. Signed-off-by: aryan <aryan1bhokare@gmail.com> * fix: return type error. Signed-off-by: aryan <aryan1bhokare@gmail.com> * chore(deps): Bump golang.org/x/crypto in /chaoscenter/authentication (#4527) Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.18.0 to 0.21.0. - [Commits](golang/crypto@v0.18.0...v0.21.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): Bump follow-redirects in /chaoscenter/web (#4529) Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.5 to 1.15.6. - [Release notes](https://github.com/follow-redirects/follow-redirects/releases) - [Commits](follow-redirects/follow-redirects@v1.15.5...v1.15.6) --- updated-dependencies: - dependency-name: follow-redirects dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): Bump github.com/golang/protobuf (#4493) Bumps [github.com/golang/protobuf](https://github.com/golang/protobuf) from 1.5.3 to 1.5.4. - [Release notes](https://github.com/golang/protobuf/releases) - [Commits](golang/protobuf@v1.5.3...v1.5.4) --- updated-dependencies: - dependency-name: github.com/golang/protobuf dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Raj Das <mail.rajdas@gmail.com> * Modified SendInvitation API. This modification unables to send invite with the role as owner. Signed-off-by: aryan <aryan1bhokare@gmail.com> * Modified LeaveProject API. This modification checks if the User is the last owner of the project and if not User can leave the project. Signed-off-by: aryan <aryan1bhokare@gmail.com> * RBAC modification `LeaveProject`. Allows Owner to be able to leave the project. Signed-off-by: aryan <aryan1bhokare@gmail.com> * Added `UpdateMemberRole` API. This API is used for updating role of the member in the project. Signed-off-by: aryan <aryan1bhokare@gmail.com> * Fixed some syntax errors. Signed-off-by: aryan <aryan1bhokare@gmail.com> * Updated roles for owner. Signed-off-by: aryan <aryan1bhokare@gmail.com> * Added new API `DeleteProject`. Owner can delete project with help of this API. Signed-off-by: aryan <aryan1bhokare@gmail.com> * Added mocks. Signed-off-by: aryan <aryan1bhokare@gmail.com> * modified go.sum Signed-off-by: aryan <aryan1bhokare@gmail.com> * Added condition `UpdateMemberRole`. User cannot change role of their own, so that it will avoid edge cases like 1. User is the last owner of the project. 2. User accidentally losing owner access to the projects. Signed-off-by: aryan <aryan1bhokare@gmail.com> * made suggested changes. Signed-off-by: aryan <aryan1bhokare@gmail.com> * Changed DeleteProject endpoint to have url parameter. Signed-off-by: aryan <aryan1bhokare@gmail.com> * Minor fixes. Signed-off-by: aryan <aryan1bhokare@gmail.com> --------- Signed-off-by: aryan <aryan1bhokare@gmail.com> Signed-off-by: dependabot[bot] <support@github.com> Signed-off-by: Aryan Bhokare <92683836+aryan-bhokare@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Raj Das <mail.rajdas@gmail.com>
1 parent cea879c commit 0e51dbf

File tree

7 files changed

+248
-6
lines changed

7 files changed

+248
-6
lines changed

chaoscenter/authentication/api/handlers/rest/project_handler.go

Lines changed: 141 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,29 @@ func GetActiveProjectMembers(service services.ApplicationService) gin.HandlerFun
195195
}
196196
}
197197

198+
// GetActiveProjectOwners godoc
199+
//
200+
// @Summary Get active project Owners.
201+
// @Description Return list of active project owners.
202+
// @Tags ProjectRouter
203+
// @Param state path string true "State"
204+
// @Accept json
205+
// @Produce json
206+
// @Failure 500 {object} response.ErrServerError
207+
// @Success 200 {object} response.Response{}
208+
// @Router /get_project_owners/:project_id/:state [get]
209+
func GetActiveProjectOwners(service services.ApplicationService) gin.HandlerFunc {
210+
return func(c *gin.Context) {
211+
projectID := c.Param("project_id")
212+
owners, err := service.GetProjectOwners(projectID)
213+
if err != nil {
214+
c.JSON(utils.ErrorStatusCodes[utils.ErrServerError], presenter.CreateErrorResponse(utils.ErrServerError))
215+
return
216+
}
217+
c.JSON(http.StatusOK, gin.H{"data": owners})
218+
}
219+
}
220+
198221
// getInvitation returns the Invitation status
199222
func getInvitation(service services.ApplicationService, member entities.MemberInput) (entities.Invitation, error) {
200223
project, err := service.GetProjectByProjectID(member.ProjectID)
@@ -380,7 +403,7 @@ func SendInvitation(service services.ApplicationService) gin.HandlerFunc {
380403
return
381404
}
382405
// Validating member role
383-
if member.Role == nil || (*member.Role != entities.RoleEditor && *member.Role != entities.RoleViewer) {
406+
if member.Role == nil || (*member.Role != entities.RoleEditor && *member.Role != entities.RoleViewer && *member.Role != entities.RoleOwner) {
384407
c.JSON(utils.ErrorStatusCodes[utils.ErrInvalidRole], presenter.CreateErrorResponse(utils.ErrInvalidRole))
385408
return
386409
}
@@ -568,7 +591,21 @@ func LeaveProject(service services.ApplicationService) gin.HandlerFunc {
568591
c.JSON(utils.ErrorStatusCodes[utils.ErrInvalidRequest], presenter.CreateErrorResponse(utils.ErrInvalidRequest))
569592
return
570593
}
594+
595+
if member.Role != nil && *member.Role == entities.RoleOwner {
596+
owners, err := service.GetProjectOwners(member.ProjectID)
597+
if err != nil {
598+
log.Error(err)
599+
c.JSON(utils.ErrorStatusCodes[utils.ErrServerError], presenter.CreateErrorResponse(utils.ErrServerError))
600+
return
601+
}
571602

603+
if len(owners) == 1 {
604+
c.JSON(utils.ErrorStatusCodes[utils.ErrInvalidRequest], gin.H{"message": "Cannot leave project. There must be at least one owner."})
605+
return
606+
}
607+
}
608+
572609
err = validations.RbacValidator(c.MustGet("uid").(string), member.ProjectID,
573610
validations.MutationRbacRules["leaveProject"],
574611
string(entities.AcceptedInvitation),
@@ -726,6 +763,68 @@ func UpdateProjectName(service services.ApplicationService) gin.HandlerFunc {
726763
}
727764
}
728765

766+
// UpdateMemberRole godoc
767+
//
768+
// @Summary Update member role.
769+
// @Description Return updated member role.
770+
// @Tags ProjectRouter
771+
// @Accept json
772+
// @Produce json
773+
// @Failure 400 {object} response.ErrInvalidRequest
774+
// @Failure 401 {object} response.ErrUnauthorized
775+
// @Failure 500 {object} response.ErrServerError
776+
// @Success 200 {object} response.Response{}
777+
// @Router /update_member_role [post]
778+
//
779+
// UpdateMemberRole is used to update a member role in the project
780+
func UpdateMemberRole(service services.ApplicationService) gin.HandlerFunc {
781+
return func(c *gin.Context) {
782+
var member entities.MemberInput
783+
err := c.BindJSON(&member)
784+
if err != nil {
785+
log.Warn(err)
786+
c.JSON(utils.ErrorStatusCodes[utils.ErrInvalidRequest], presenter.CreateErrorResponse(utils.ErrInvalidRequest))
787+
return
788+
}
789+
790+
791+
// Validating member role
792+
if member.Role == nil || (*member.Role != entities.RoleEditor && *member.Role != entities.RoleViewer && *member.Role != entities.RoleOwner) {
793+
c.JSON(utils.ErrorStatusCodes[utils.ErrInvalidRole], presenter.CreateErrorResponse(utils.ErrInvalidRole))
794+
return
795+
}
796+
797+
err = validations.RbacValidator(c.MustGet("uid").(string),
798+
member.ProjectID,
799+
validations.MutationRbacRules["updateMemberRole"],
800+
string(entities.AcceptedInvitation),
801+
service)
802+
if err != nil {
803+
log.Warn(err)
804+
c.JSON(utils.ErrorStatusCodes[utils.ErrUnauthorized],
805+
presenter.CreateErrorResponse(utils.ErrUnauthorized))
806+
return
807+
}
808+
809+
uid := c.MustGet("uid").(string)
810+
if uid == member.UserID {
811+
c.JSON(http.StatusBadRequest, gin.H{"message": "User cannot change their own role."})
812+
return
813+
}
814+
815+
err = service.UpdateMemberRole(member.ProjectID, member.UserID, member.Role)
816+
if err != nil {
817+
log.Error(err)
818+
c.JSON(utils.ErrorStatusCodes[utils.ErrServerError], presenter.CreateErrorResponse(utils.ErrServerError))
819+
return
820+
}
821+
822+
c.JSON(http.StatusOK, gin.H{
823+
"message": "Successfully updated Role",
824+
})
825+
}
826+
}
827+
729828
// GetOwnerProjects godoc
730829
//
731830
// @Summary Get projects owner.
@@ -796,3 +895,44 @@ func GetProjectRole(service services.ApplicationService) gin.HandlerFunc {
796895

797896
}
798897
}
898+
899+
// DeleteProject godoc
900+
//
901+
// @Description Delete a project.
902+
// @Tags ProjectRouter
903+
// @Accept json
904+
// @Produce json
905+
// @Failure 400 {object} response.ErrProjectNotFound
906+
// @Failure 500 {object} response.ErrServerError
907+
// @Success 200 {object} response.Response{}
908+
// @Router /delete_project/{project_id} [post]
909+
//
910+
// DeleteProject is used to delete a project.
911+
func DeleteProject (service services.ApplicationService) gin.HandlerFunc {
912+
return func(c *gin.Context) {
913+
projectID := c.Param("project_id")
914+
915+
err := validations.RbacValidator(c.MustGet("uid").(string),
916+
projectID,
917+
validations.MutationRbacRules["deleteProject"],
918+
string(entities.AcceptedInvitation),
919+
service)
920+
if err != nil {
921+
log.Warn(err)
922+
c.JSON(utils.ErrorStatusCodes[utils.ErrUnauthorized],
923+
presenter.CreateErrorResponse(utils.ErrUnauthorized))
924+
return
925+
}
926+
927+
err = service.DeleteProject(projectID)
928+
if err != nil {
929+
log.Error(err)
930+
c.JSON(utils.ErrorStatusCodes[utils.ErrServerError], presenter.CreateErrorResponse(utils.ErrServerError))
931+
return
932+
}
933+
934+
c.JSON(http.StatusOK, gin.H{
935+
"message": "Successfully deleted project.",
936+
})
937+
}
938+
}

chaoscenter/authentication/api/mocks/rest_mocks.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,11 @@ func (m *MockedApplicationService) UpdateProjectName(projectID, projectName stri
120120
return args.Error(0)
121121
}
122122

123+
func (m *MockedApplicationService) UpdateMemberRole(projectID, userID string, role *entities.MemberRole) error {
124+
args := m.Called(projectID, userID, role)
125+
return args.Error(0)
126+
}
127+
123128
func (m *MockedApplicationService) GetAggregateProjects(pipeline mongo.Pipeline, opts *options.AggregateOptions) (*mongo.Cursor, error) {
124129
args := m.Called(pipeline, opts)
125130
return args.Get(0).(*mongo.Cursor), args.Error(1)
@@ -145,6 +150,11 @@ func (m *MockedApplicationService) GetProjectMembers(projectID, state string) ([
145150
return args.Get(0).([]*entities.Member), args.Error(1)
146151
}
147152

153+
func (m *MockedApplicationService) GetProjectOwners(projectID string) ([]*entities.Member, error) {
154+
args := m.Called(projectID)
155+
return args.Get(0).([]*entities.Member), args.Error(1)
156+
}
157+
148158
func (m *MockedApplicationService) ListInvitations(userID string, invitationState entities.Invitation) ([]*entities.Project, error) {
149159
args := m.Called(userID, invitationState)
150160
return args.Get(0).([]*entities.Project), args.Error(1)
@@ -199,3 +209,8 @@ func (m *MockedApplicationService) RbacValidator(userID, resourceID string, rule
199209
args := m.Called(userID, resourceID, rules, invitationStatus)
200210
return args.Error(0)
201211
}
212+
213+
func (m *MockedApplicationService) DeleteProject(projectID string) error {
214+
args := m.Called(projectID)
215+
return args.Error(0)
216+
}

chaoscenter/authentication/api/routes/project_router.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ func ProjectRouter(router *gin.Engine, service services.ApplicationService) {
1313
router.Use(middleware.JwtMiddleware(service))
1414
router.GET("/get_project/:project_id", rest.GetProject(service))
1515
router.GET("/get_project_members/:project_id/:state", rest.GetActiveProjectMembers(service))
16+
router.GET("/get_project_owners/:project_id", rest.GetActiveProjectOwners(service))
1617
router.GET("/get_user_with_project/:username", rest.GetUserWithProject(service))
1718
router.GET("/get_owner_projects", rest.GetOwnerProjects(service))
1819
router.GET("/get_project_role/:project_id", rest.GetProjectRole(service))
@@ -26,4 +27,6 @@ func ProjectRouter(router *gin.Engine, service services.ApplicationService) {
2627
router.POST("/remove_invitation", rest.RemoveInvitation(service))
2728
router.POST("/leave_project", rest.LeaveProject(service))
2829
router.POST("/update_project_name", rest.UpdateProjectName(service))
30+
router.POST("/update_member_role", rest.UpdateMemberRole(service))
31+
router.POST("/delete_project/:project_id", rest.DeleteProject(service))
2932
}

chaoscenter/authentication/pkg/entities/project.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,13 @@ type Project struct {
1010
}
1111

1212
type Owner struct {
13-
UserID string `bson:"user_id" json:"userID"`
14-
Username string `bson:"username" json:"username"`
13+
UserID string `bson:"user_id" json:"userID"`
14+
Username string `bson:"username" json:"username"`
15+
Invitation Invitation `bson:"invitation" json:"invitation"`
16+
JoinedAt int64 `bson:"joined_at" json:"joinedAt"`
17+
DeactivatedAt *int64 `bson:"deactivated_at,omitempty" json:"deactivatedAt,omitempty"`
1518
}
19+
1620
type MemberStat struct {
1721
Owner *[]Owner `bson:"owner" json:"owner"`
1822
Total int `bson:"total" json:"total"`
@@ -50,6 +54,10 @@ type CreateProjectInput struct {
5054
UserID string `bson:"user_id" json:"userID"`
5155
}
5256

57+
type DeleteProjectInput struct {
58+
ProjectID string `json:"projectID"`
59+
}
60+
5361
type MemberInput struct {
5462
ProjectID string `json:"projectID"`
5563
UserID string `json:"userID"`

chaoscenter/authentication/pkg/project/repository.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,14 @@ type Repository interface {
2525
RemoveInvitation(projectID string, userID string, invitation entities.Invitation) error
2626
UpdateInvite(projectID string, userID string, invitation entities.Invitation, role *entities.MemberRole) error
2727
UpdateProjectName(projectID string, projectName string) error
28+
UpdateMemberRole(projectID string, userID string, role *entities.MemberRole) error
2829
GetAggregateProjects(pipeline mongo.Pipeline, opts *options.AggregateOptions) (*mongo.Cursor, error)
2930
UpdateProjectState(ctx context.Context, userID string, deactivateTime int64, isDeactivate bool) error
3031
GetOwnerProjects(ctx context.Context, userID string) ([]*entities.Project, error)
3132
GetProjectRole(projectID string, userID string) (*entities.MemberRole, error)
3233
GetProjectMembers(projectID string, state string) ([]*entities.Member, error)
34+
GetProjectOwners(projectID string) ([]*entities.Member, error)
35+
DeleteProject(projectID string) error
3336
ListInvitations(userID string, invitationState entities.Invitation) ([]*entities.Project, error)
3437
}
3538

@@ -277,6 +280,24 @@ func (r repository) UpdateProjectName(projectID string, projectName string) erro
277280
return nil
278281
}
279282

283+
// UpdateMemberRole : Updates Role of the member in the project.
284+
func (r repository) UpdateMemberRole(projectID string, userID string, role *entities.MemberRole) error {
285+
opts := options.Update().SetArrayFilters(options.ArrayFilters{
286+
Filters: []interface{}{
287+
bson.D{{"elem.user_id", userID}},
288+
},
289+
})
290+
query := bson.D{{"_id", projectID}}
291+
update := bson.D{{"$set", bson.M{"members.$[elem].role": role}}}
292+
293+
_, err := r.Collection.UpdateOne(context.TODO(), query, update, opts)
294+
if err != nil {
295+
return err
296+
}
297+
298+
return nil
299+
}
300+
280301
// GetAggregateProjects takes a mongo pipeline to retrieve the project details from the database
281302
func (r repository) GetAggregateProjects(pipeline mongo.Pipeline, opts *options.AggregateOptions) (*mongo.Cursor, error) {
282303
results, err := r.Collection.Aggregate(context.TODO(), pipeline, opts)
@@ -381,6 +402,28 @@ func (r repository) GetOwnerProjects(ctx context.Context, userID string) ([]*ent
381402
return projects, nil
382403
}
383404

405+
// GetProjectOwners takes projectID and returns the owners
406+
func (r repository) GetProjectOwners(projectID string) ([]*entities.Member, error) {
407+
filter := bson.D{{"_id", projectID}}
408+
409+
var project struct {
410+
Members []*entities.Member `bson:"members"`
411+
}
412+
err := r.Collection.FindOne(context.TODO(), filter).Decode(&project)
413+
if err != nil {
414+
return nil, err
415+
}
416+
417+
// Filter the members to include only the owners
418+
var owners []*entities.Member
419+
for _, member := range project.Members {
420+
if member.Role == entities.RoleOwner && member.Invitation == entities.AcceptedInvitation {
421+
owners = append(owners, member)
422+
}
423+
}
424+
return owners, nil
425+
}
426+
384427
// GetProjectRole returns the role of a user in the project
385428
func (r repository) GetProjectRole(projectID string, userID string) (*entities.MemberRole, error) {
386429
filter := bson.D{
@@ -556,3 +599,19 @@ func NewRepo(collection *mongo.Collection) Repository {
556599
Collection: collection,
557600
}
558601
}
602+
603+
// DeleteProject deletes the project with given projectID
604+
func (r repository) DeleteProject(projectID string) error {
605+
query := bson.D{{"_id", projectID}}
606+
607+
result, err := r.Collection.DeleteOne(context.TODO(), query)
608+
if err != nil {
609+
return err
610+
}
611+
612+
if result.DeletedCount == 0 {
613+
return errors.New("no project found with the given projectID")
614+
}
615+
616+
return nil
617+
}

chaoscenter/authentication/pkg/services/project_service.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,14 @@ type projectService interface {
2020
RemoveInvitation(projectID string, userID string, invitation entities.Invitation) error
2121
UpdateInvite(projectID string, userID string, invitation entities.Invitation, role *entities.MemberRole) error
2222
UpdateProjectName(projectID string, projectName string) error
23+
UpdateMemberRole(projectID string, userID string, role *entities.MemberRole) error
2324
GetAggregateProjects(pipeline mongo.Pipeline, opts *options.AggregateOptions) (*mongo.Cursor, error)
2425
UpdateProjectState(ctx context.Context, userID string, deactivateTime int64, isDeactivate bool) error
2526
GetOwnerProjectIDs(ctx context.Context, userID string) ([]*entities.Project, error)
2627
GetProjectRole(projectID string, userID string) (*entities.MemberRole, error)
2728
GetProjectMembers(projectID string, state string) ([]*entities.Member, error)
29+
GetProjectOwners(projectID string) ([]*entities.Member, error)
30+
DeleteProject(projectID string) error
2831
ListInvitations(userID string, invitationState entities.Invitation) ([]*entities.Project, error)
2932
}
3033

@@ -64,6 +67,10 @@ func (a applicationService) UpdateProjectName(projectID string, projectName stri
6467
return a.projectRepository.UpdateProjectName(projectID, projectName)
6568
}
6669

70+
func (a applicationService) UpdateMemberRole(projectID string, userID string, role *entities.MemberRole) error {
71+
return a.projectRepository.UpdateMemberRole(projectID, userID, role)
72+
}
73+
6774
func (a applicationService) GetAggregateProjects(pipeline mongo.Pipeline, opts *options.AggregateOptions) (*mongo.Cursor, error) {
6875
return a.projectRepository.GetAggregateProjects(pipeline, opts)
6976
}
@@ -82,6 +89,14 @@ func (a applicationService) GetProjectMembers(projectID string, state string) ([
8289
return a.projectRepository.GetProjectMembers(projectID, state)
8390
}
8491

92+
func (a applicationService) GetProjectOwners(projectID string) ([]*entities.Member, error) {
93+
return a.projectRepository.GetProjectOwners(projectID)
94+
}
95+
8596
func (a applicationService) ListInvitations(userID string, invitationState entities.Invitation) ([]*entities.Project, error) {
8697
return a.projectRepository.ListInvitations(userID, invitationState)
8798
}
99+
100+
func (a applicationService) DeleteProject(projectID string) error {
101+
return a.projectRepository.DeleteProject(projectID)
102+
}

chaoscenter/authentication/pkg/validations/roles.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ import "github.com/litmuschaos/litmus/chaoscenter/authentication/pkg/entities"
44

55
var MutationRbacRules = map[string][]string{
66
"sendInvitation": {string(entities.RoleOwner)},
7-
"acceptInvitation": {string(entities.RoleViewer), string(entities.RoleEditor)},
8-
"declineInvitation": {string(entities.RoleViewer),
7+
"acceptInvitation": {string(entities.RoleOwner), string(entities.RoleViewer), string(entities.RoleEditor)},
8+
"declineInvitation": {string(entities.RoleOwner), string(entities.RoleViewer),
99
string(entities.RoleEditor)},
1010
"removeInvitation": {string(entities.RoleOwner)},
11-
"leaveProject": {string(entities.RoleViewer), string(entities.RoleEditor)},
11+
"leaveProject": {string(entities.RoleOwner), string(entities.RoleViewer), string(entities.RoleEditor)},
1212
"updateProjectName": {string(entities.RoleOwner)},
13+
"updateMemberRole": {string(entities.RoleOwner)},
14+
"deleteProject": {string(entities.RoleOwner)},
1315
"getProject": {string(entities.RoleOwner), string(entities.RoleViewer), string(entities.RoleEditor)},
1416
}

0 commit comments

Comments
 (0)