diff --git a/chaoscenter/authentication/api/handlers/rest/project_handler.go b/chaoscenter/authentication/api/handlers/rest/project_handler.go index 731f1d926b7..d15c93c8270 100644 --- a/chaoscenter/authentication/api/handlers/rest/project_handler.go +++ b/chaoscenter/authentication/api/handlers/rest/project_handler.go @@ -195,6 +195,29 @@ func GetActiveProjectMembers(service services.ApplicationService) gin.HandlerFun } } +// GetActiveProjectOwners godoc +// +// @Summary Get active project Owners. +// @Description Return list of active project owners. +// @Tags ProjectRouter +// @Param state path string true "State" +// @Accept json +// @Produce json +// @Failure 500 {object} response.ErrServerError +// @Success 200 {object} response.Response{} +// @Router /get_project_owners/:project_id/:state [get] +func GetActiveProjectOwners(service services.ApplicationService) gin.HandlerFunc { + return func(c *gin.Context) { + projectID := c.Param("project_id") + owners, err := service.GetProjectOwners(projectID) + if err != nil { + c.JSON(utils.ErrorStatusCodes[utils.ErrServerError], presenter.CreateErrorResponse(utils.ErrServerError)) + return + } + c.JSON(http.StatusOK, gin.H{"data": owners}) + } +} + // getInvitation returns the Invitation status func getInvitation(service services.ApplicationService, member entities.MemberInput) (entities.Invitation, error) { project, err := service.GetProjectByProjectID(member.ProjectID) @@ -380,7 +403,7 @@ func SendInvitation(service services.ApplicationService) gin.HandlerFunc { return } // Validating member role - if member.Role == nil || (*member.Role != entities.RoleEditor && *member.Role != entities.RoleViewer) { + if member.Role == nil || (*member.Role != entities.RoleEditor && *member.Role != entities.RoleViewer && *member.Role != entities.RoleOwner) { c.JSON(utils.ErrorStatusCodes[utils.ErrInvalidRole], presenter.CreateErrorResponse(utils.ErrInvalidRole)) return } @@ -568,7 +591,21 @@ func LeaveProject(service services.ApplicationService) gin.HandlerFunc { c.JSON(utils.ErrorStatusCodes[utils.ErrInvalidRequest], presenter.CreateErrorResponse(utils.ErrInvalidRequest)) return } + + if member.Role != nil && *member.Role == entities.RoleOwner { + owners, err := service.GetProjectOwners(member.ProjectID) + if err != nil { + log.Error(err) + c.JSON(utils.ErrorStatusCodes[utils.ErrServerError], presenter.CreateErrorResponse(utils.ErrServerError)) + return + } + if len(owners) == 1 { + c.JSON(utils.ErrorStatusCodes[utils.ErrInvalidRequest], gin.H{"message": "Cannot leave project. There must be at least one owner."}) + return + } + } + err = validations.RbacValidator(c.MustGet("uid").(string), member.ProjectID, validations.MutationRbacRules["leaveProject"], string(entities.AcceptedInvitation), @@ -726,6 +763,68 @@ func UpdateProjectName(service services.ApplicationService) gin.HandlerFunc { } } +// UpdateMemberRole godoc +// +// @Summary Update member role. +// @Description Return updated member role. +// @Tags ProjectRouter +// @Accept json +// @Produce json +// @Failure 400 {object} response.ErrInvalidRequest +// @Failure 401 {object} response.ErrUnauthorized +// @Failure 500 {object} response.ErrServerError +// @Success 200 {object} response.Response{} +// @Router /update_member_role [post] +// +// UpdateMemberRole is used to update a member role in the project +func UpdateMemberRole(service services.ApplicationService) gin.HandlerFunc { + return func(c *gin.Context) { + var member entities.MemberInput + err := c.BindJSON(&member) + if err != nil { + log.Warn(err) + c.JSON(utils.ErrorStatusCodes[utils.ErrInvalidRequest], presenter.CreateErrorResponse(utils.ErrInvalidRequest)) + return + } + + + // Validating member role + if member.Role == nil || (*member.Role != entities.RoleEditor && *member.Role != entities.RoleViewer && *member.Role != entities.RoleOwner) { + c.JSON(utils.ErrorStatusCodes[utils.ErrInvalidRole], presenter.CreateErrorResponse(utils.ErrInvalidRole)) + return + } + + err = validations.RbacValidator(c.MustGet("uid").(string), + member.ProjectID, + validations.MutationRbacRules["updateMemberRole"], + string(entities.AcceptedInvitation), + service) + if err != nil { + log.Warn(err) + c.JSON(utils.ErrorStatusCodes[utils.ErrUnauthorized], + presenter.CreateErrorResponse(utils.ErrUnauthorized)) + return + } + + uid := c.MustGet("uid").(string) + if uid == member.UserID { + c.JSON(http.StatusBadRequest, gin.H{"message": "User cannot change their own role."}) + return + } + + err = service.UpdateMemberRole(member.ProjectID, member.UserID, member.Role) + if err != nil { + log.Error(err) + c.JSON(utils.ErrorStatusCodes[utils.ErrServerError], presenter.CreateErrorResponse(utils.ErrServerError)) + return + } + + c.JSON(http.StatusOK, gin.H{ + "message": "Successfully updated Role", + }) + } +} + // GetOwnerProjects godoc // // @Summary Get projects owner. @@ -796,3 +895,44 @@ func GetProjectRole(service services.ApplicationService) gin.HandlerFunc { } } + +// DeleteProject godoc +// +// @Description Delete a project. +// @Tags ProjectRouter +// @Accept json +// @Produce json +// @Failure 400 {object} response.ErrProjectNotFound +// @Failure 500 {object} response.ErrServerError +// @Success 200 {object} response.Response{} +// @Router /delete_project/{project_id} [post] +// +// DeleteProject is used to delete a project. +func DeleteProject (service services.ApplicationService) gin.HandlerFunc { + return func(c *gin.Context) { + projectID := c.Param("project_id") + + err := validations.RbacValidator(c.MustGet("uid").(string), + projectID, + validations.MutationRbacRules["deleteProject"], + string(entities.AcceptedInvitation), + service) + if err != nil { + log.Warn(err) + c.JSON(utils.ErrorStatusCodes[utils.ErrUnauthorized], + presenter.CreateErrorResponse(utils.ErrUnauthorized)) + return + } + + err = service.DeleteProject(projectID) + if err != nil { + log.Error(err) + c.JSON(utils.ErrorStatusCodes[utils.ErrServerError], presenter.CreateErrorResponse(utils.ErrServerError)) + return + } + + c.JSON(http.StatusOK, gin.H{ + "message": "Successfully deleted project.", + }) + } +} \ No newline at end of file diff --git a/chaoscenter/authentication/api/mocks/rest_mocks.go b/chaoscenter/authentication/api/mocks/rest_mocks.go index 80218864b50..83faca5af93 100644 --- a/chaoscenter/authentication/api/mocks/rest_mocks.go +++ b/chaoscenter/authentication/api/mocks/rest_mocks.go @@ -120,6 +120,11 @@ func (m *MockedApplicationService) UpdateProjectName(projectID, projectName stri return args.Error(0) } +func (m *MockedApplicationService) UpdateMemberRole(projectID, userID string, role *entities.MemberRole) error { + args := m.Called(projectID, userID, role) + return args.Error(0) +} + func (m *MockedApplicationService) GetAggregateProjects(pipeline mongo.Pipeline, opts *options.AggregateOptions) (*mongo.Cursor, error) { args := m.Called(pipeline, opts) return args.Get(0).(*mongo.Cursor), args.Error(1) @@ -145,6 +150,11 @@ func (m *MockedApplicationService) GetProjectMembers(projectID, state string) ([ return args.Get(0).([]*entities.Member), args.Error(1) } +func (m *MockedApplicationService) GetProjectOwners(projectID string) ([]*entities.Member, error) { + args := m.Called(projectID) + return args.Get(0).([]*entities.Member), args.Error(1) +} + func (m *MockedApplicationService) ListInvitations(userID string, invitationState entities.Invitation) ([]*entities.Project, error) { args := m.Called(userID, invitationState) return args.Get(0).([]*entities.Project), args.Error(1) @@ -199,3 +209,8 @@ func (m *MockedApplicationService) RbacValidator(userID, resourceID string, rule args := m.Called(userID, resourceID, rules, invitationStatus) return args.Error(0) } + +func (m *MockedApplicationService) DeleteProject(projectID string) error { + args := m.Called(projectID) + return args.Error(0) +} \ No newline at end of file diff --git a/chaoscenter/authentication/api/routes/project_router.go b/chaoscenter/authentication/api/routes/project_router.go index f1c82e0077a..1f28d5e27c0 100644 --- a/chaoscenter/authentication/api/routes/project_router.go +++ b/chaoscenter/authentication/api/routes/project_router.go @@ -13,6 +13,7 @@ func ProjectRouter(router *gin.Engine, service services.ApplicationService) { router.Use(middleware.JwtMiddleware(service)) router.GET("/get_project/:project_id", rest.GetProject(service)) router.GET("/get_project_members/:project_id/:state", rest.GetActiveProjectMembers(service)) + router.GET("/get_project_owners/:project_id", rest.GetActiveProjectOwners(service)) router.GET("/get_user_with_project/:username", rest.GetUserWithProject(service)) router.GET("/get_owner_projects", rest.GetOwnerProjects(service)) router.GET("/get_project_role/:project_id", rest.GetProjectRole(service)) @@ -26,4 +27,6 @@ func ProjectRouter(router *gin.Engine, service services.ApplicationService) { router.POST("/remove_invitation", rest.RemoveInvitation(service)) router.POST("/leave_project", rest.LeaveProject(service)) router.POST("/update_project_name", rest.UpdateProjectName(service)) + router.POST("/update_member_role", rest.UpdateMemberRole(service)) + router.POST("/delete_project/:project_id", rest.DeleteProject(service)) } diff --git a/chaoscenter/authentication/pkg/entities/project.go b/chaoscenter/authentication/pkg/entities/project.go index 3dda69124d5..c9cbc65426f 100644 --- a/chaoscenter/authentication/pkg/entities/project.go +++ b/chaoscenter/authentication/pkg/entities/project.go @@ -10,9 +10,13 @@ type Project struct { } type Owner struct { - UserID string `bson:"user_id" json:"userID"` - Username string `bson:"username" json:"username"` + UserID string `bson:"user_id" json:"userID"` + Username string `bson:"username" json:"username"` + Invitation Invitation `bson:"invitation" json:"invitation"` + JoinedAt int64 `bson:"joined_at" json:"joinedAt"` + DeactivatedAt *int64 `bson:"deactivated_at,omitempty" json:"deactivatedAt,omitempty"` } + type MemberStat struct { Owner *[]Owner `bson:"owner" json:"owner"` Total int `bson:"total" json:"total"` @@ -50,6 +54,10 @@ type CreateProjectInput struct { UserID string `bson:"user_id" json:"userID"` } +type DeleteProjectInput struct { + ProjectID string `json:"projectID"` +} + type MemberInput struct { ProjectID string `json:"projectID"` UserID string `json:"userID"` diff --git a/chaoscenter/authentication/pkg/project/repository.go b/chaoscenter/authentication/pkg/project/repository.go index 8037eb24374..82d5c61f4f6 100644 --- a/chaoscenter/authentication/pkg/project/repository.go +++ b/chaoscenter/authentication/pkg/project/repository.go @@ -25,11 +25,14 @@ type Repository interface { RemoveInvitation(projectID string, userID string, invitation entities.Invitation) error UpdateInvite(projectID string, userID string, invitation entities.Invitation, role *entities.MemberRole) error UpdateProjectName(projectID string, projectName string) error + UpdateMemberRole(projectID string, userID string, role *entities.MemberRole) error GetAggregateProjects(pipeline mongo.Pipeline, opts *options.AggregateOptions) (*mongo.Cursor, error) UpdateProjectState(ctx context.Context, userID string, deactivateTime int64, isDeactivate bool) error GetOwnerProjects(ctx context.Context, userID string) ([]*entities.Project, error) GetProjectRole(projectID string, userID string) (*entities.MemberRole, error) GetProjectMembers(projectID string, state string) ([]*entities.Member, error) + GetProjectOwners(projectID string) ([]*entities.Member, error) + DeleteProject(projectID string) error ListInvitations(userID string, invitationState entities.Invitation) ([]*entities.Project, error) } @@ -277,6 +280,24 @@ func (r repository) UpdateProjectName(projectID string, projectName string) erro return nil } +// UpdateMemberRole : Updates Role of the member in the project. +func (r repository) UpdateMemberRole(projectID string, userID string, role *entities.MemberRole) error { + opts := options.Update().SetArrayFilters(options.ArrayFilters{ + Filters: []interface{}{ + bson.D{{"elem.user_id", userID}}, + }, + }) + query := bson.D{{"_id", projectID}} + update := bson.D{{"$set", bson.M{"members.$[elem].role": role}}} + + _, err := r.Collection.UpdateOne(context.TODO(), query, update, opts) + if err != nil { + return err + } + + return nil +} + // GetAggregateProjects takes a mongo pipeline to retrieve the project details from the database func (r repository) GetAggregateProjects(pipeline mongo.Pipeline, opts *options.AggregateOptions) (*mongo.Cursor, error) { results, err := r.Collection.Aggregate(context.TODO(), pipeline, opts) @@ -381,6 +402,28 @@ func (r repository) GetOwnerProjects(ctx context.Context, userID string) ([]*ent return projects, nil } +// GetProjectOwners takes projectID and returns the owners +func (r repository) GetProjectOwners(projectID string) ([]*entities.Member, error) { + filter := bson.D{{"_id", projectID}} + + var project struct { + Members []*entities.Member `bson:"members"` + } + err := r.Collection.FindOne(context.TODO(), filter).Decode(&project) + if err != nil { + return nil, err + } + + // Filter the members to include only the owners + var owners []*entities.Member + for _, member := range project.Members { + if member.Role == entities.RoleOwner && member.Invitation == entities.AcceptedInvitation { + owners = append(owners, member) + } + } + return owners, nil +} + // GetProjectRole returns the role of a user in the project func (r repository) GetProjectRole(projectID string, userID string) (*entities.MemberRole, error) { filter := bson.D{ @@ -556,3 +599,19 @@ func NewRepo(collection *mongo.Collection) Repository { Collection: collection, } } + +// DeleteProject deletes the project with given projectID +func (r repository) DeleteProject(projectID string) error { + query := bson.D{{"_id", projectID}} + + result, err := r.Collection.DeleteOne(context.TODO(), query) + if err != nil { + return err + } + + if result.DeletedCount == 0 { + return errors.New("no project found with the given projectID") + } + + return nil +} diff --git a/chaoscenter/authentication/pkg/services/project_service.go b/chaoscenter/authentication/pkg/services/project_service.go index 664f4a20e81..3c5316b091f 100644 --- a/chaoscenter/authentication/pkg/services/project_service.go +++ b/chaoscenter/authentication/pkg/services/project_service.go @@ -20,11 +20,14 @@ type projectService interface { RemoveInvitation(projectID string, userID string, invitation entities.Invitation) error UpdateInvite(projectID string, userID string, invitation entities.Invitation, role *entities.MemberRole) error UpdateProjectName(projectID string, projectName string) error + UpdateMemberRole(projectID string, userID string, role *entities.MemberRole) error GetAggregateProjects(pipeline mongo.Pipeline, opts *options.AggregateOptions) (*mongo.Cursor, error) UpdateProjectState(ctx context.Context, userID string, deactivateTime int64, isDeactivate bool) error GetOwnerProjectIDs(ctx context.Context, userID string) ([]*entities.Project, error) GetProjectRole(projectID string, userID string) (*entities.MemberRole, error) GetProjectMembers(projectID string, state string) ([]*entities.Member, error) + GetProjectOwners(projectID string) ([]*entities.Member, error) + DeleteProject(projectID string) error ListInvitations(userID string, invitationState entities.Invitation) ([]*entities.Project, error) } @@ -64,6 +67,10 @@ func (a applicationService) UpdateProjectName(projectID string, projectName stri return a.projectRepository.UpdateProjectName(projectID, projectName) } +func (a applicationService) UpdateMemberRole(projectID string, userID string, role *entities.MemberRole) error { + return a.projectRepository.UpdateMemberRole(projectID, userID, role) +} + func (a applicationService) GetAggregateProjects(pipeline mongo.Pipeline, opts *options.AggregateOptions) (*mongo.Cursor, error) { return a.projectRepository.GetAggregateProjects(pipeline, opts) } @@ -82,6 +89,14 @@ func (a applicationService) GetProjectMembers(projectID string, state string) ([ return a.projectRepository.GetProjectMembers(projectID, state) } +func (a applicationService) GetProjectOwners(projectID string) ([]*entities.Member, error) { + return a.projectRepository.GetProjectOwners(projectID) +} + func (a applicationService) ListInvitations(userID string, invitationState entities.Invitation) ([]*entities.Project, error) { return a.projectRepository.ListInvitations(userID, invitationState) } + +func (a applicationService) DeleteProject(projectID string) error { + return a.projectRepository.DeleteProject(projectID) +} diff --git a/chaoscenter/authentication/pkg/validations/roles.go b/chaoscenter/authentication/pkg/validations/roles.go index eb5c8471586..7fbf999afec 100644 --- a/chaoscenter/authentication/pkg/validations/roles.go +++ b/chaoscenter/authentication/pkg/validations/roles.go @@ -4,11 +4,13 @@ import "github.com/litmuschaos/litmus/chaoscenter/authentication/pkg/entities" var MutationRbacRules = map[string][]string{ "sendInvitation": {string(entities.RoleOwner)}, - "acceptInvitation": {string(entities.RoleViewer), string(entities.RoleEditor)}, - "declineInvitation": {string(entities.RoleViewer), + "acceptInvitation": {string(entities.RoleOwner), string(entities.RoleViewer), string(entities.RoleEditor)}, + "declineInvitation": {string(entities.RoleOwner), string(entities.RoleViewer), string(entities.RoleEditor)}, "removeInvitation": {string(entities.RoleOwner)}, - "leaveProject": {string(entities.RoleViewer), string(entities.RoleEditor)}, + "leaveProject": {string(entities.RoleOwner), string(entities.RoleViewer), string(entities.RoleEditor)}, "updateProjectName": {string(entities.RoleOwner)}, + "updateMemberRole": {string(entities.RoleOwner)}, + "deleteProject": {string(entities.RoleOwner)}, "getProject": {string(entities.RoleOwner), string(entities.RoleViewer), string(entities.RoleEditor)}, }