diff --git a/REST-Tests/lists.http b/REST-Tests/lists.http index df0c4ca7..174bfbd9 100644 --- a/REST-Tests/lists.http +++ b/REST-Tests/lists.http @@ -25,3 +25,17 @@ Authorization: Bearer {{auth_token}} ### +# Get all users who have access to that list +GET http://localhost:8080/api/v1/lists/10/users +Authorization: Bearer {{auth_token}} + +### + +# Give a user access to that list +PUT http://localhost:8080/api/v1/lists/1/users +Authorization: Bearer {{auth_token}} +Content-Type: application/json + +{"user_id":2, "right": 5} + +### diff --git a/models/error.go b/models/error.go index c2ae2249..5418237b 100644 --- a/models/error.go +++ b/models/error.go @@ -495,3 +495,38 @@ func IsErrCannotDeleteLastTeamMember(err error) bool { func (err ErrCannotDeleteLastTeamMember) Error() string { return fmt.Sprintf("This user is already a member of that team. [Team ID: %d, User ID: %d]", err.TeamID, err.UserID) } + +// ==================== +// User <-> List errors +// ==================== + +// ErrInvalidUserRight represents an error where a user right is invalid +type ErrInvalidUserRight struct { + Right UserRight +} + +// IsErrInvalidUserRight checks if an error is ErrInvalidUserRight. +func IsErrInvalidUserRight(err error) bool { + _, ok := err.(ErrInvalidUserRight) + return ok +} + +func (err ErrInvalidUserRight) Error() string { + return fmt.Sprintf("The right is invalid [Right: %d]", err.Right) +} + +// ErrUserAlreadyHasAccess represents an error where a user already has access to a list/namespace +type ErrUserAlreadyHasAccess struct { + UserID int64 + ListID int64 +} + +// IsErrUserAlreadyHasAccess checks if an error is ErrUserAlreadyHasAccess. +func IsErrUserAlreadyHasAccess(err error) bool { + _, ok := err.(ErrUserAlreadyHasAccess) + return ok +} + +func (err ErrUserAlreadyHasAccess) Error() string { + return fmt.Sprintf("This user already has access to that list. [User ID: %d, List ID: %d]", err.UserID, err.ListID) +} diff --git a/models/list_users.go b/models/list_users.go new file mode 100644 index 00000000..0b8971d5 --- /dev/null +++ b/models/list_users.go @@ -0,0 +1,20 @@ +package models + +// ListUser represents a list <-> user relation +type ListUser struct { + ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id" param:"namespace"` + UserID int64 `xorm:"int(11) not null" json:"user_id" param:"user"` + ListID int64 `xorm:"int(11) not null" json:"list_id" param:"list"` + Right TeamRight `xorm:"int(11)" json:"right"` + + Created int64 `xorm:"created" json:"created"` + Updated int64 `xorm:"updated" json:"updated"` + + CRUDable `xorm:"-" json:"-"` + Rights `xorm:"-" json:"-"` +} + +// TableName is the table name for ListUser +func (ListUser) TableName() string { + return "users_list" +} diff --git a/models/list_users_create.go b/models/list_users_create.go new file mode 100644 index 00000000..fe7c6d50 --- /dev/null +++ b/models/list_users_create.go @@ -0,0 +1,40 @@ +package models + +// Create creates a new list <-> user relation +func (ul *ListUser) Create(user *User) (err error) { + + // Check if the right is valid + if err := ul.Right.isValid(); err != nil { + return err + } + + // Check if the list exists + l, err := GetListByID(ul.ListID) + if err != nil { + return + } + + // Check if the user exists + if _, _, err = GetUserByID(ul.UserID); err != nil { + return err + } + + // Check if the user already has access or is owner of that list + // We explicitly DONT check for teams here + if l.OwnerID == ul.UserID { + return ErrUserAlreadyHasAccess{UserID: ul.UserID, ListID: ul.ListID} + } + + exist, err := x.Where("list_id = ? AND user_id = ?", ul.ListID, ul.UserID).Get(&ListUser{}) + if err != nil { + return + } + if exist { + return ErrUserAlreadyHasAccess{UserID: ul.UserID, ListID: ul.ListID} + } + + // Insert user <-> list relation + _, err = x.Insert(ul) + + return +} diff --git a/models/list_users_readall.go b/models/list_users_readall.go new file mode 100644 index 00000000..c8982c54 --- /dev/null +++ b/models/list_users_readall.go @@ -0,0 +1,23 @@ +package models + +// ReadAll gets all users who have access to a list +func (ul *ListUser) ReadAll(user *User) (interface{}, error) { + // Check if the user has access to the list + l, err := GetListByID(ul.ListID) + if err != nil { + return nil, err + } + if !l.CanRead(user) { + return nil, ErrNeedToHaveListReadAccess{} + } + + // Get all users + all := []*User{} + err = x. + Select("users.*"). + Join("INNER", "users_list", "user_id = users.id"). + Where("users_list.list_id = ?", ul.ListID). + Find(&all) + + return all, err +} diff --git a/models/list_users_rights.go b/models/list_users_rights.go new file mode 100644 index 00000000..166b1ac9 --- /dev/null +++ b/models/list_users_rights.go @@ -0,0 +1,34 @@ +package models + +// UserRight defines the rights users can have for lists/namespaces +type UserRight int + +// define unknown user right +const ( + UserRightUnknown = -1 +) + +// Enumerate all the user rights +const ( + // Can read lists in a User + UserRightRead UserRight = iota + // Can write tasks in a User like lists and todo tasks. Cannot create new lists. + UserRightWrite + // Can manage a list/namespace, can do everything + UserRightAdmin +) + +func (r UserRight) isValid() error { + if r != UserRightAdmin && r != UserRightRead && r != UserRightWrite { + return ErrInvalidUserRight{r} + } + + return nil +} + +// CanCreate checks if the user can create a new user <-> list relation +func (lu *ListUser) CanCreate(doer *User) bool { + // Get the list and check if the user has write access on it + l, _ := GetListByID(lu.ListID) + return l.CanWrite(doer) +} diff --git a/models/models.go b/models/models.go index c2d69d60..a2fb97cf 100644 --- a/models/models.go +++ b/models/models.go @@ -40,6 +40,7 @@ func init() { new(TeamList), new(TeamNamespace), new(Namespace), + new(ListUser), ) } diff --git a/routes/crud/create.go b/routes/crud/create.go index c97aab7d..e56dd753 100644 --- a/routes/crud/create.go +++ b/routes/crud/create.go @@ -63,6 +63,13 @@ func (c *WebHandler) CreateWeb(ctx echo.Context) error { return echo.NewHTTPError(http.StatusBadRequest, "This user is already a member of that team.") } + if models.IsErrUserAlreadyHasAccess(err) { + return echo.NewHTTPError(http.StatusBadRequest, "This user already has access to this list.") + } + if models.IsErrInvalidUserRight(err) { + return echo.NewHTTPError(http.StatusBadRequest, "The right is invalid.") + } + return echo.NewHTTPError(http.StatusInternalServerError) } diff --git a/routes/crud/read_all.go b/routes/crud/read_all.go index f23e17fe..a1be9f78 100644 --- a/routes/crud/read_all.go +++ b/routes/crud/read_all.go @@ -20,6 +20,10 @@ func (c *WebHandler) ReadAllWeb(ctx echo.Context) error { lists, err := c.CObject.ReadAll(¤tUser) if err != nil { + if models.IsErrNeedToHaveListReadAccess(err) { + return echo.NewHTTPError(http.StatusForbidden, "You need to have read access to this list.") + } + return echo.NewHTTPError(http.StatusInternalServerError, "An error occured.") } diff --git a/routes/routes.go b/routes/routes.go index c0048945..557d1087 100644 --- a/routes/routes.go +++ b/routes/routes.go @@ -110,6 +110,13 @@ func RegisterRoutes(e *echo.Echo) { a.PUT("/lists/:list/teams", listTeamHandler.CreateWeb) a.DELETE("/lists/:list/teams/:team", listTeamHandler.DeleteWeb) + listUserHandler := &crud.WebHandler{ + CObject: &models.ListUser{}, + } + a.GET("/lists/:list/users", listUserHandler.ReadAllWeb) + a.PUT("/lists/:list/users", listUserHandler.CreateWeb) + a.DELETE("/lists/:list/users/:user", listUserHandler.DeleteWeb) + namespaceHandler := &crud.WebHandler{ CObject: &models.Namespace{}, }