Skip to content

Commit

Permalink
Multiple project owner backend (#4774)
Browse files Browse the repository at this point in the history
* Modified db schema of Owner.

Signed-off-by: aryan <[email protected]>

* Added new API GetProjectOwners.

Signed-off-by: aryan <[email protected]>

* fix: return type error.

Signed-off-by: aryan <[email protected]>

* 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] <[email protected]>
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] <[email protected]>
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] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Raj Das <[email protected]>

* Modified SendInvitation API.

This modification unables to send invite with the role as owner.

Signed-off-by: aryan <[email protected]>

* 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 <[email protected]>

* RBAC modification `LeaveProject`.

Allows Owner to be able to leave the project.

Signed-off-by: aryan <[email protected]>

* Added `UpdateMemberRole` API.

This API is used for updating role of the member in the project.

Signed-off-by: aryan <[email protected]>

* Fixed some syntax errors.

Signed-off-by: aryan <[email protected]>

* Updated roles for owner.

Signed-off-by: aryan <[email protected]>

* Added new API `DeleteProject`.
Owner can delete project with help of this API.

Signed-off-by: aryan <[email protected]>

* Added mocks.

Signed-off-by: aryan <[email protected]>

* modified go.sum

Signed-off-by: aryan <[email protected]>

* 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 <[email protected]>

* made suggested changes.

Signed-off-by: aryan <[email protected]>

* Changed DeleteProject endpoint to have url parameter.

Signed-off-by: aryan <[email protected]>

* Minor fixes.

Signed-off-by: aryan <[email protected]>

* fixed import orders

Signed-off-by: aryan <[email protected]>

* fixing RoleEditor to RoleExecuter

Signed-off-by: aryan <[email protected]>

---------

Signed-off-by: aryan <[email protected]>
Signed-off-by: dependabot[bot] <[email protected]>
Signed-off-by: Aryan Bhokare <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Raj Das <[email protected]>
Co-authored-by: Saranya Jena <[email protected]>
  • Loading branch information
4 people authored Jul 22, 2024
1 parent 08c111a commit 164e280
Show file tree
Hide file tree
Showing 7 changed files with 246 additions and 5 deletions.
139 changes: 139 additions & 0 deletions chaoscenter/authentication/api/handlers/rest/project_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,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)
Expand Down Expand Up @@ -618,6 +641,20 @@ func LeaveProject(service services.ApplicationService) gin.HandlerFunc {
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
}
}

// admin/user shouldn't be able to perform any task if it's default pwd is not changes(initial login is true)
initialLogin, err := CheckInitialLogin(service, c.MustGet("uid").(string))
if err != nil {
Expand Down Expand Up @@ -799,6 +836,67 @@ 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.RoleExecutor && *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.
Expand Down Expand Up @@ -869,3 +967,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.",
})
}
}
15 changes: 15 additions & 0 deletions chaoscenter/authentication/api/mocks/rest_mocks.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,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)
Expand All @@ -152,6 +157,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)
Expand Down Expand Up @@ -207,6 +217,11 @@ func (m *MockedApplicationService) RbacValidator(userID, resourceID string, rule
return args.Error(0)
}

func (m *MockedApplicationService) DeleteProject(projectID string) error {
args := m.Called(projectID)
return args.Error(0)
}

func (m *MockedApplicationService) CreateConfig(config authConfig.AuthConfig) error {
args := m.Called(config)
return args.Error(0)
Expand Down
3 changes: 3 additions & 0 deletions chaoscenter/authentication/api/routes/project_router.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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))
}
12 changes: 10 additions & 2 deletions chaoscenter/authentication/pkg/entities/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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"`
Expand Down
59 changes: 59 additions & 0 deletions chaoscenter/authentication/pkg/project/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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
}
15 changes: 15 additions & 0 deletions chaoscenter/authentication/pkg/services/project_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down Expand Up @@ -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)
}
Expand All @@ -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)
}
8 changes: 5 additions & 3 deletions chaoscenter/authentication/pkg/validations/roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.RoleExecutor)},
"declineInvitation": {string(entities.RoleViewer),
"acceptInvitation": {string(entities.RoleOwner), string(entities.RoleViewer), string(entities.RoleExecutor)},
"declineInvitation": {string(entities.RoleOwner), string(entities.RoleViewer),
string(entities.RoleExecutor)},
"removeInvitation": {string(entities.RoleOwner)},
"leaveProject": {string(entities.RoleViewer), string(entities.RoleExecutor)},
"leaveProject": {string(entities.RoleOwner), string(entities.RoleViewer), string(entities.RoleExecutor)},
"updateProjectName": {string(entities.RoleOwner)},
"updateMemberRole": {string(entities.RoleOwner)},
"deleteProject": {string(entities.RoleOwner)},
"getProject": {string(entities.RoleOwner), string(entities.RoleViewer), string(entities.RoleExecutor)},
}

0 comments on commit 164e280

Please sign in to comment.