Skip to content

Commit 72082b0

Browse files
aryan-bhokaredependabot[bot]imrajdasSaranya-jena
authored andcommitted
Multiple project owner backend (litmuschaos#4774)
* 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 (litmuschaos#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 (litmuschaos#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 (litmuschaos#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> * fixed import orders Signed-off-by: aryan <aryan1bhokare@gmail.com> * fixing RoleEditor to RoleExecuter 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> Co-authored-by: Saranya Jena <saranya.jena@harness.io> Signed-off-by: andoriyaprashant <prashantandoriya@gmail.com>
1 parent 97afd21 commit 72082b0

File tree

7 files changed

+246
-5
lines changed

7 files changed

+246
-5
lines changed

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

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,29 @@ func GetActiveProjectMembers(service services.ApplicationService) gin.HandlerFun
210210
}
211211
}
212212

213+
// GetActiveProjectOwners godoc
214+
//
215+
// @Summary Get active project Owners.
216+
// @Description Return list of active project owners.
217+
// @Tags ProjectRouter
218+
// @Param state path string true "State"
219+
// @Accept json
220+
// @Produce json
221+
// @Failure 500 {object} response.ErrServerError
222+
// @Success 200 {object} response.Response{}
223+
// @Router /get_project_owners/:project_id/:state [get]
224+
func GetActiveProjectOwners(service services.ApplicationService) gin.HandlerFunc {
225+
return func(c *gin.Context) {
226+
projectID := c.Param("project_id")
227+
owners, err := service.GetProjectOwners(projectID)
228+
if err != nil {
229+
c.JSON(utils.ErrorStatusCodes[utils.ErrServerError], presenter.CreateErrorResponse(utils.ErrServerError))
230+
return
231+
}
232+
c.JSON(http.StatusOK, gin.H{"data": owners})
233+
}
234+
}
235+
213236
// getInvitation returns the Invitation status
214237
func getInvitation(service services.ApplicationService, member entities.MemberInput) (entities.Invitation, error) {
215238
project, err := service.GetProjectByProjectID(member.ProjectID)
@@ -618,6 +641,20 @@ func LeaveProject(service services.ApplicationService) gin.HandlerFunc {
618641
return
619642
}
620643

644+
if member.Role != nil && *member.Role == entities.RoleOwner {
645+
owners, err := service.GetProjectOwners(member.ProjectID)
646+
if err != nil {
647+
log.Error(err)
648+
c.JSON(utils.ErrorStatusCodes[utils.ErrServerError], presenter.CreateErrorResponse(utils.ErrServerError))
649+
return
650+
}
651+
652+
if len(owners) == 1 {
653+
c.JSON(utils.ErrorStatusCodes[utils.ErrInvalidRequest], gin.H{"message": "Cannot leave project. There must be at least one owner."})
654+
return
655+
}
656+
}
657+
621658
// admin/user shouldn't be able to perform any task if it's default pwd is not changes(initial login is true)
622659
initialLogin, err := CheckInitialLogin(service, c.MustGet("uid").(string))
623660
if err != nil {
@@ -799,6 +836,67 @@ func UpdateProjectName(service services.ApplicationService) gin.HandlerFunc {
799836
}
800837
}
801838

839+
// UpdateMemberRole godoc
840+
//
841+
// @Summary Update member role.
842+
// @Description Return updated member role.
843+
// @Tags ProjectRouter
844+
// @Accept json
845+
// @Produce json
846+
// @Failure 400 {object} response.ErrInvalidRequest
847+
// @Failure 401 {object} response.ErrUnauthorized
848+
// @Failure 500 {object} response.ErrServerError
849+
// @Success 200 {object} response.Response{}
850+
// @Router /update_member_role [post]
851+
//
852+
// UpdateMemberRole is used to update a member role in the project
853+
func UpdateMemberRole(service services.ApplicationService) gin.HandlerFunc {
854+
return func(c *gin.Context) {
855+
var member entities.MemberInput
856+
err := c.BindJSON(&member)
857+
if err != nil {
858+
log.Warn(err)
859+
c.JSON(utils.ErrorStatusCodes[utils.ErrInvalidRequest], presenter.CreateErrorResponse(utils.ErrInvalidRequest))
860+
return
861+
}
862+
863+
// Validating member role
864+
if member.Role == nil || (*member.Role != entities.RoleExecutor && *member.Role != entities.RoleViewer && *member.Role != entities.RoleOwner) {
865+
c.JSON(utils.ErrorStatusCodes[utils.ErrInvalidRole], presenter.CreateErrorResponse(utils.ErrInvalidRole))
866+
return
867+
}
868+
869+
err = validations.RbacValidator(c.MustGet("uid").(string),
870+
member.ProjectID,
871+
validations.MutationRbacRules["updateMemberRole"],
872+
string(entities.AcceptedInvitation),
873+
service)
874+
if err != nil {
875+
log.Warn(err)
876+
c.JSON(utils.ErrorStatusCodes[utils.ErrUnauthorized],
877+
presenter.CreateErrorResponse(utils.ErrUnauthorized))
878+
return
879+
}
880+
881+
uid := c.MustGet("uid").(string)
882+
if uid == member.UserID {
883+
c.JSON(http.StatusBadRequest, gin.H{"message": "User cannot change their own role."})
884+
return
885+
}
886+
887+
err = service.UpdateMemberRole(member.ProjectID, member.UserID, member.Role)
888+
if err != nil {
889+
log.Error(err)
890+
c.JSON(utils.ErrorStatusCodes[utils.ErrServerError], presenter.CreateErrorResponse(utils.ErrServerError))
891+
return
892+
}
893+
894+
c.JSON(http.StatusOK, gin.H{
895+
"message": "Successfully updated Role",
896+
})
897+
}
898+
}
899+
802900
// GetOwnerProjects godoc
803901
//
804902
// @Summary Get projects owner.
@@ -869,3 +967,44 @@ func GetProjectRole(service services.ApplicationService) gin.HandlerFunc {
869967

870968
}
871969
}
970+
971+
// DeleteProject godoc
972+
//
973+
// @Description Delete a project.
974+
// @Tags ProjectRouter
975+
// @Accept json
976+
// @Produce json
977+
// @Failure 400 {object} response.ErrProjectNotFound
978+
// @Failure 500 {object} response.ErrServerError
979+
// @Success 200 {object} response.Response{}
980+
// @Router /delete_project/{project_id} [post]
981+
//
982+
// DeleteProject is used to delete a project.
983+
func DeleteProject(service services.ApplicationService) gin.HandlerFunc {
984+
return func(c *gin.Context) {
985+
projectID := c.Param("project_id")
986+
987+
err := validations.RbacValidator(c.MustGet("uid").(string),
988+
projectID,
989+
validations.MutationRbacRules["deleteProject"],
990+
string(entities.AcceptedInvitation),
991+
service)
992+
if err != nil {
993+
log.Warn(err)
994+
c.JSON(utils.ErrorStatusCodes[utils.ErrUnauthorized],
995+
presenter.CreateErrorResponse(utils.ErrUnauthorized))
996+
return
997+
}
998+
999+
err = service.DeleteProject(projectID)
1000+
if err != nil {
1001+
log.Error(err)
1002+
c.JSON(utils.ErrorStatusCodes[utils.ErrServerError], presenter.CreateErrorResponse(utils.ErrServerError))
1003+
return
1004+
}
1005+
1006+
c.JSON(http.StatusOK, gin.H{
1007+
"message": "Successfully deleted project.",
1008+
})
1009+
}
1010+
}

chaoscenter/authentication/api/mocks/rest_mocks.go

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

130+
func (m *MockedApplicationService) UpdateMemberRole(projectID, userID string, role *entities.MemberRole) error {
131+
args := m.Called(projectID, userID, role)
132+
return args.Error(0)
133+
}
134+
130135
func (m *MockedApplicationService) GetAggregateProjects(pipeline mongo.Pipeline, opts *options.AggregateOptions) (*mongo.Cursor, error) {
131136
args := m.Called(pipeline, opts)
132137
return args.Get(0).(*mongo.Cursor), args.Error(1)
@@ -152,6 +157,11 @@ func (m *MockedApplicationService) GetProjectMembers(projectID, state string) ([
152157
return args.Get(0).([]*entities.Member), args.Error(1)
153158
}
154159

160+
func (m *MockedApplicationService) GetProjectOwners(projectID string) ([]*entities.Member, error) {
161+
args := m.Called(projectID)
162+
return args.Get(0).([]*entities.Member), args.Error(1)
163+
}
164+
155165
func (m *MockedApplicationService) ListInvitations(userID string, invitationState entities.Invitation) ([]*entities.Project, error) {
156166
args := m.Called(userID, invitationState)
157167
return args.Get(0).([]*entities.Project), args.Error(1)
@@ -207,6 +217,11 @@ func (m *MockedApplicationService) RbacValidator(userID, resourceID string, rule
207217
return args.Error(0)
208218
}
209219

220+
func (m *MockedApplicationService) DeleteProject(projectID string) error {
221+
args := m.Called(projectID)
222+
return args.Error(0)
223+
}
224+
210225
func (m *MockedApplicationService) CreateConfig(config authConfig.AuthConfig) error {
211226
args := m.Called(config)
212227
return args.Error(0)

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.RoleExecutor)},
8-
"declineInvitation": {string(entities.RoleViewer),
7+
"acceptInvitation": {string(entities.RoleOwner), string(entities.RoleViewer), string(entities.RoleExecutor)},
8+
"declineInvitation": {string(entities.RoleOwner), string(entities.RoleViewer),
99
string(entities.RoleExecutor)},
1010
"removeInvitation": {string(entities.RoleOwner)},
11-
"leaveProject": {string(entities.RoleViewer), string(entities.RoleExecutor)},
11+
"leaveProject": {string(entities.RoleOwner), string(entities.RoleViewer), string(entities.RoleExecutor)},
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.RoleExecutor)},
1416
}

0 commit comments

Comments
 (0)