From c244a0f1455270a014c0deeef9da8600575bd880 Mon Sep 17 00:00:00 2001 From: kolaente Date: Thu, 29 Dec 2022 16:40:06 +0100 Subject: [PATCH] feat(projects): remove namespaces --- pkg/cmd/user.go | 2 +- pkg/models/label_rights.go | 2 +- pkg/models/label_task.go | 2 +- pkg/models/namespace.go | 774 ---------------------- pkg/models/namespace_rights.go | 145 ---- pkg/models/namespace_team.go | 244 ------- pkg/models/namespace_team_rights.go | 40 -- pkg/models/namespace_team_rights_test.go | 107 --- pkg/models/namespace_team_test.go | 298 --------- pkg/models/namespace_test.go | 372 ----------- pkg/models/namespace_users.go | 251 ------- pkg/models/namespace_users_rights.go | 42 -- pkg/models/namespace_users_rights_test.go | 107 --- pkg/models/namespace_users_test.go | 436 ------------ pkg/models/prject.go | 480 +++++++------- pkg/models/project_rights.go | 111 ++-- pkg/models/saved_filters.go | 2 +- pkg/models/user_delete.go | 104 --- pkg/modules/auth/openid/openid.go | 2 +- pkg/routes/api/v1/project_by_namespace.go | 97 --- pkg/routes/api/v1/user_register.go | 2 +- pkg/routes/routes.go | 34 +- 22 files changed, 317 insertions(+), 3337 deletions(-) delete mode 100644 pkg/models/namespace.go delete mode 100644 pkg/models/namespace_rights.go delete mode 100644 pkg/models/namespace_team.go delete mode 100644 pkg/models/namespace_team_rights.go delete mode 100644 pkg/models/namespace_team_rights_test.go delete mode 100644 pkg/models/namespace_team_test.go delete mode 100644 pkg/models/namespace_test.go delete mode 100644 pkg/models/namespace_users.go delete mode 100644 pkg/models/namespace_users_rights.go delete mode 100644 pkg/models/namespace_users_rights_test.go delete mode 100644 pkg/models/namespace_users_test.go delete mode 100644 pkg/routes/api/v1/project_by_namespace.go diff --git a/pkg/cmd/user.go b/pkg/cmd/user.go index 6506ad623..e62b4b4f4 100644 --- a/pkg/cmd/user.go +++ b/pkg/cmd/user.go @@ -188,7 +188,7 @@ var userCreateCmd = &cobra.Command{ log.Fatalf("Error creating new user: %s", err) } - err = models.CreateNewNamespaceForUser(s, newUser) + err = models.CreateNewProjectForUser(s, newUser) if err != nil { _ = s.Rollback() log.Fatalf("Error creating new namespace for user: %s", err) diff --git a/pkg/models/label_rights.go b/pkg/models/label_rights.go index 196dd10c5..b737dc8bc 100644 --- a/pkg/models/label_rights.go +++ b/pkg/models/label_rights.go @@ -77,7 +77,7 @@ func (l *Label) hasAccessToLabel(s *xorm.Session, a web.Auth) (has bool, maxRigh builder. Select("id"). From("tasks"). - Where(builder.In("project_id", getUserProjectsStatement(u.ID).Select("l.id"))), + Where(builder.In("project_id", getUserProjectsStatement(u.ID, "", false).Select("l.id"))), ) ll := &LabelTask{} diff --git a/pkg/models/label_task.go b/pkg/models/label_task.go index 370b449b6..04894f402 100644 --- a/pkg/models/label_task.go +++ b/pkg/models/label_task.go @@ -180,7 +180,7 @@ func getLabelsByTaskIDs(s *xorm.Session, opts *LabelByTaskIDsOptions) (ls []*lab builder. Select("id"). From("tasks"). - Where(builder.In("project_id", getUserProjectsStatement(opts.GetForUser).Select("l.id"))), + Where(builder.In("project_id", getUserProjectsStatement(opts.GetForUser, "", false).Select("l.id"))), ), cond) } if opts.GetUnusedLabels { diff --git a/pkg/models/namespace.go b/pkg/models/namespace.go deleted file mode 100644 index c78a23096..000000000 --- a/pkg/models/namespace.go +++ /dev/null @@ -1,774 +0,0 @@ -// Vikunja is a to-do list application to facilitate your life. -// Copyright 2018-2021 Vikunja and contributors. All rights reserved. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public Licensee as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public Licensee for more details. -// -// You should have received a copy of the GNU Affero General Public Licensee -// along with this program. If not, see . - -package models - -import ( - "sort" - "strconv" - "strings" - "time" - - "code.vikunja.io/api/pkg/db" - - "code.vikunja.io/api/pkg/events" - "code.vikunja.io/api/pkg/log" - "code.vikunja.io/api/pkg/user" - - "code.vikunja.io/web" - "xorm.io/builder" - "xorm.io/xorm" -) - -// Namespace holds informations about a namespace -type Namespace struct { - // The unique, numeric id of this namespace. - ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"namespace"` - // The name of this namespace. - Title string `xorm:"varchar(250) not null" json:"title" valid:"required,runelength(1|250)" minLength:"1" maxLength:"250"` - // The description of the namespace - Description string `xorm:"longtext null" json:"description"` - OwnerID int64 `xorm:"bigint not null INDEX" json:"-"` - - // The hex color of this namespace - HexColor string `xorm:"varchar(6) null" json:"hex_color" valid:"runelength(0|6)" maxLength:"6"` - - // Whether or not a namespace is archived. - IsArchived bool `xorm:"not null default false" json:"is_archived" query:"is_archived"` - - // The user who owns this namespace - Owner *user.User `xorm:"-" json:"owner" valid:"-"` - - // The subscription status for the user reading this namespace. You can only read this property, use the subscription endpoints to modify it. - // Will only returned when retreiving one namespace. - Subscription *Subscription `xorm:"-" json:"subscription,omitempty"` - - // A timestamp when this namespace was created. You cannot change this value. - Created time.Time `xorm:"created not null" json:"created"` - // A timestamp when this namespace was last updated. You cannot change this value. - Updated time.Time `xorm:"updated not null" json:"updated"` - - // If set to true, will only return the namespaces, not their projects. - NamespacesOnly bool `xorm:"-" json:"-" query:"namespaces_only"` - - web.CRUDable `xorm:"-" json:"-"` - web.Rights `xorm:"-" json:"-"` -} - -// SharedProjectsPseudoNamespace is a pseudo namespace used to hold shared projects -var SharedProjectsPseudoNamespace = Namespace{ - ID: -1, - Title: "Shared Projects", - Description: "Projects of other users shared with you via teams or directly.", - Created: time.Now(), - Updated: time.Now(), -} - -// FavoritesPseudoNamespace is a pseudo namespace used to hold favorited projects and tasks -var FavoritesPseudoNamespace = Namespace{ - ID: -2, - Title: "Favorites", - Description: "Favorite projects and tasks.", - Created: time.Now(), - Updated: time.Now(), -} - -// SavedFiltersPseudoNamespace is a pseudo namespace used to hold saved filters -var SavedFiltersPseudoNamespace = Namespace{ - ID: -3, - Title: "Filters", - Description: "Saved filters.", - Created: time.Now(), - Updated: time.Now(), -} - -// TableName makes beautiful table names -func (Namespace) TableName() string { - return "namespaces" -} - -// GetSimpleByID gets a namespace without things like the owner, it more or less only checks if it exists. -func getNamespaceSimpleByID(s *xorm.Session, id int64) (namespace *Namespace, err error) { - if id == 0 { - return nil, ErrNamespaceDoesNotExist{ID: id} - } - - // Get the namesapce with shared projects - if id == -1 { - return &SharedProjectsPseudoNamespace, nil - } - - if id == FavoritesPseudoNamespace.ID { - return &FavoritesPseudoNamespace, nil - } - - if id == SavedFiltersPseudoNamespace.ID { - return &SavedFiltersPseudoNamespace, nil - } - - namespace = &Namespace{} - - exists, err := s.Where("id = ?", id).Get(namespace) - if err != nil { - return - } - if !exists { - return nil, ErrNamespaceDoesNotExist{ID: id} - } - - return -} - -// GetNamespaceByID returns a namespace object by its ID -func GetNamespaceByID(s *xorm.Session, id int64) (namespace *Namespace, err error) { - namespace, err = getNamespaceSimpleByID(s, id) - if err != nil { - return - } - - // Get the namespace Owner - namespace.Owner, err = user.GetUserByID(s, namespace.OwnerID) - return -} - -// CheckIsArchived returns an ErrNamespaceIsArchived if the namepace is archived. -func (n *Namespace) CheckIsArchived(s *xorm.Session) error { - exists, err := s. - Where("id = ? AND is_archived = true", n.ID). - Exist(&Namespace{}) - if err != nil { - return err - } - if exists { - return ErrNamespaceIsArchived{NamespaceID: n.ID} - } - return nil -} - -// ReadOne gets one namespace -// @Summary Gets one namespace -// @Description Returns a namespace by its ID. -// @tags namespace -// @Accept json -// @Produce json -// @Security JWTKeyAuth -// @Param id path int true "Namespace ID" -// @Success 200 {object} models.Namespace "The Namespace" -// @Failure 403 {object} web.HTTPError "The user does not have access to that namespace." -// @Failure 500 {object} models.Message "Internal error" -// @Router /namespaces/{id} [get] -func (n *Namespace) ReadOne(s *xorm.Session, a web.Auth) (err error) { - nn, err := GetNamespaceByID(s, n.ID) - if err != nil { - return err - } - *n = *nn - - n.Subscription, err = GetSubscription(s, SubscriptionEntityNamespace, n.ID, a) - return -} - -// NamespaceWithProjects represents a namespace with project meta informations -type NamespaceWithProjects struct { - Namespace `xorm:"extends"` - Projects []*Project `xorm:"-" json:"projects"` -} - -type NamespaceWithProjectsAndTasks struct { - Namespace - Projects []*ProjectWithTasksAndBuckets `xorm:"-" json:"projects"` -} - -func makeNamespaceSlice(namespaces map[int64]*NamespaceWithProjects, userMap map[int64]*user.User, subscriptions map[int64]*Subscription) []*NamespaceWithProjects { - all := make([]*NamespaceWithProjects, 0, len(namespaces)) - for _, n := range namespaces { - n.Owner = userMap[n.OwnerID] - n.Subscription = subscriptions[n.ID] - all = append(all, n) - for _, l := range n.Projects { - if n.Subscription != nil && l.Subscription == nil { - l.Subscription = n.Subscription - } - } - } - sort.Slice(all, func(i, j int) bool { - return all[i].ID < all[j].ID - }) - - return all -} - -func getNamespaceFilterCond(search string) (filterCond builder.Cond) { - filterCond = db.ILIKE("namespaces.title", search) - - if search == "" { - return - } - - vals := strings.Split(search, ",") - - if len(vals) == 0 { - return - } - - ids := []int64{} - for _, val := range vals { - v, err := strconv.ParseInt(val, 10, 64) - if err != nil { - log.Debugf("Namespace search string part '%s' is not a number: %s", val, err) - continue - } - ids = append(ids, v) - } - - if len(ids) > 0 { - filterCond = builder.In("namespaces.id", ids) - } - - return -} - -func getNamespaceArchivedCond(archived bool) builder.Cond { - // Adding a 1=1 condition by default here because xorm always needs a condition and cannot handle nil conditions - var isArchivedCond builder.Cond = builder.Eq{"1": 1} - if !archived { - isArchivedCond = builder.And( - builder.Eq{"namespaces.is_archived": false}, - ) - } - - return isArchivedCond -} - -func getNamespacesWithProjects(s *xorm.Session, namespaces *map[int64]*NamespaceWithProjects, search string, isArchived bool, page, perPage int, userID int64) (numberOfTotalItems int64, err error) { - isArchivedCond := getNamespaceArchivedCond(isArchived) - filterCond := getNamespaceFilterCond(search) - - limit, start := getLimitFromPageIndex(page, perPage) - query := s.Select("namespaces.*"). - Table("namespaces"). - Join("LEFT", "team_namespaces", "namespaces.id = team_namespaces.namespace_id"). - Join("LEFT", "team_members", "team_members.team_id = team_namespaces.team_id"). - Join("LEFT", "users_namespaces", "users_namespaces.namespace_id = namespaces.id"). - Where("team_members.user_id = ?", userID). - Or("namespaces.owner_id = ?", userID). - Or("users_namespaces.user_id = ?", userID). - GroupBy("namespaces.id"). - Where(filterCond). - Where(isArchivedCond) - if limit > 0 { - query = query.Limit(limit, start) - } - err = query.Find(namespaces) - if err != nil { - return 0, err - } - - numberOfTotalItems, err = s. - Table("namespaces"). - Join("LEFT", "team_namespaces", "namespaces.id = team_namespaces.namespace_id"). - Join("LEFT", "team_members", "team_members.team_id = team_namespaces.team_id"). - Join("LEFT", "users_namespaces", "users_namespaces.namespace_id = namespaces.id"). - Where("team_members.user_id = ?", userID). - Or("namespaces.owner_id = ?", userID). - Or("users_namespaces.user_id = ?", userID). - And("namespaces.is_archived = false"). - GroupBy("namespaces.id"). - Where(filterCond). - Where(isArchivedCond). - Count(&NamespaceWithProjects{}) - return numberOfTotalItems, err -} - -func getNamespaceOwnerIDs(namespaces map[int64]*NamespaceWithProjects) (namespaceIDs, ownerIDs []int64) { - for _, nsp := range namespaces { - namespaceIDs = append(namespaceIDs, nsp.ID) - ownerIDs = append(ownerIDs, nsp.OwnerID) - } - - return -} - -func getNamespaceSubscriptions(s *xorm.Session, namespaceIDs []int64, userID int64) (map[int64]*Subscription, error) { - subscriptionsMap := make(map[int64]*Subscription) - if len(namespaceIDs) == 0 { - return subscriptionsMap, nil - } - - subscriptions := []*Subscription{} - err := s. - Where("entity_type = ? AND user_id = ?", SubscriptionEntityNamespace, userID). - In("entity_id", namespaceIDs). - Find(&subscriptions) - if err != nil { - return nil, err - } - for _, sub := range subscriptions { - sub.Entity = sub.EntityType.String() - subscriptionsMap[sub.EntityID] = sub - } - - return subscriptionsMap, err -} - -func getProjectsForNamespaces(s *xorm.Session, namespaceIDs []int64, archived bool) ([]*Project, error) { - projects := []*Project{} - projectQuery := s. - OrderBy("position"). - In("namespace_id", namespaceIDs) - - if !archived { - projectQuery.And("is_archived = false") - } - err := projectQuery.Find(&projects) - return projects, err -} - -func getSharedProjectsInNamespace(s *xorm.Session, archived bool, doer *user.User) (sharedProjectsNamespace *NamespaceWithProjects, err error) { - // Create our pseudo namespace to hold the shared projects - sharedProjectsPseudonamespace := SharedProjectsPseudoNamespace - sharedProjectsPseudonamespace.OwnerID = doer.ID - sharedProjectsNamespace = &NamespaceWithProjects{ - sharedProjectsPseudonamespace, - []*Project{}, - } - - // Get all projects individually shared with our user (not via a namespace) - individualProjects := []*Project{} - iProjectQuery := s.Select("l.*"). - Table("projects"). - Alias("l"). - Join("LEFT", []string{"team_projects", "tl"}, "l.id = tl.project_id"). - Join("LEFT", []string{"team_members", "tm"}, "tm.team_id = tl.team_id"). - Join("LEFT", []string{"users_projects", "ul"}, "ul.project_id = l.id"). - Where(builder.And( - builder.Eq{"tm.user_id": doer.ID}, - builder.Neq{"l.owner_id": doer.ID}, - )). - Or(builder.And( - builder.Eq{"ul.user_id": doer.ID}, - builder.Neq{"l.owner_id": doer.ID}, - )). - GroupBy("l.id") - if !archived { - iProjectQuery.And("l.is_archived = false") - } - err = iProjectQuery.Find(&individualProjects) - if err != nil { - return - } - - // Make the namespace -1 so we now later which one it was - // + Append it to all projects we already have - for _, l := range individualProjects { - l.NamespaceID = sharedProjectsNamespace.ID - } - - sharedProjectsNamespace.Projects = individualProjects - - // Remove the sharedProjectsPseudonamespace if we don't have any shared projects - if len(individualProjects) == 0 { - sharedProjectsNamespace = nil - } - - return -} - -func getFavoriteProjects(s *xorm.Session, projects []*Project, namespaceIDs []int64, doer *user.User) (favoriteNamespace *NamespaceWithProjects, err error) { - // Create our pseudo namespace with favorite projects - pseudoFavoriteNamespace := FavoritesPseudoNamespace - pseudoFavoriteNamespace.OwnerID = doer.ID - favoriteNamespace = &NamespaceWithProjects{ - Namespace: pseudoFavoriteNamespace, - Projects: []*Project{{}}, - } - *favoriteNamespace.Projects[0] = FavoritesPseudoProject // Copying the project to be able to modify it later - favoriteNamespace.Projects[0].Owner = doer - - for _, project := range projects { - if !project.IsFavorite { - continue - } - favoriteNamespace.Projects = append(favoriteNamespace.Projects, project) - } - - // Check if we have any favorites or favorited projects and remove the favorites namespace from the project if not - cond := builder. - Select("tasks.id"). - From("tasks"). - Join("INNER", "projects", "tasks.project_id = projects.id"). - Join("INNER", "namespaces", "projects.namespace_id = namespaces.id"). - Where(builder.In("namespaces.id", namespaceIDs)) - - var favoriteCount int64 - favoriteCount, err = s. - Where(builder.And( - builder.Eq{"user_id": doer.ID}, - builder.Eq{"kind": FavoriteKindTask}, - builder.In("entity_id", cond), - )). - Count(&Favorite{}) - if err != nil { - return - } - - // If we don't have any favorites in the favorites pseudo project, remove that pseudo project from the namespace - if favoriteCount == 0 { - for in, l := range favoriteNamespace.Projects { - if l.ID == FavoritesPseudoProject.ID { - favoriteNamespace.Projects = append(favoriteNamespace.Projects[:in], favoriteNamespace.Projects[in+1:]...) - break - } - } - } - - // If we don't have any favorites in the namespace, remove it - if len(favoriteNamespace.Projects) == 0 { - return nil, nil - } - - return -} - -func getSavedFilters(s *xorm.Session, doer *user.User) (savedFiltersNamespace *NamespaceWithProjects, err error) { - savedFilters, err := getSavedFiltersForUser(s, doer) - if err != nil { - return - } - - if len(savedFilters) == 0 { - return nil, nil - } - - savedFiltersPseudoNamespace := SavedFiltersPseudoNamespace - savedFiltersPseudoNamespace.OwnerID = doer.ID - savedFiltersNamespace = &NamespaceWithProjects{ - Namespace: savedFiltersPseudoNamespace, - Projects: make([]*Project, 0, len(savedFilters)), - } - - for _, filter := range savedFilters { - filterProject := filter.toProject() - filterProject.NamespaceID = savedFiltersNamespace.ID - filterProject.Owner = doer - savedFiltersNamespace.Projects = append(savedFiltersNamespace.Projects, filterProject) - } - - return -} - -// ReadAll gets all namespaces a user has access to -// @Summary Get all namespaces a user has access to -// @Description Returns all namespaces a user has access to. -// @tags namespace -// @Accept json -// @Produce json -// @Param page query int false "The page number. Used for pagination. If not provided, the first page of results is returned." -// @Param per_page query int false "The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page." -// @Param s query string false "Search namespaces by name." -// @Param is_archived query bool false "If true, also returns all archived namespaces." -// @Param namespaces_only query bool false "If true, also returns only namespaces without their projects." -// @Security JWTKeyAuth -// @Success 200 {array} models.NamespaceWithProjects "The Namespaces." -// @Failure 500 {object} models.Message "Internal error" -// @Router /namespaces [get] -// -//nolint:gocyclo -func (n *Namespace) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) { - if _, is := a.(*LinkSharing); is { - return nil, 0, 0, ErrGenericForbidden{} - } - - // This map will hold all namespaces and their projects. The key is usually the id of the namespace. - // We're using a map here because it makes a few things like adding projects or removing pseudo namespaces easier. - namespaces := make(map[int64]*NamespaceWithProjects) - - ////////////////////////////// - // Projects with their namespaces - - doer, err := user.GetFromAuth(a) - if err != nil { - return nil, 0, 0, err - } - - numberOfTotalItems, err = getNamespacesWithProjects(s, &namespaces, search, n.IsArchived, page, perPage, doer.ID) - if err != nil { - return nil, 0, 0, err - } - - namespaceIDs, ownerIDs := getNamespaceOwnerIDs(namespaces) - - if len(namespaceIDs) == 0 { - return nil, 0, 0, nil - } - - subscriptionsMap, err := getNamespaceSubscriptions(s, namespaceIDs, doer.ID) - if err != nil { - return nil, 0, 0, err - } - - ownerMap, err := user.GetUsersByIDs(s, ownerIDs) - if err != nil { - return nil, 0, 0, err - } - ownerMap[doer.ID] = doer - - if n.NamespacesOnly { - all := makeNamespaceSlice(namespaces, ownerMap, subscriptionsMap) - return all, len(all), numberOfTotalItems, nil - } - - // Get all projects - projects, err := getProjectsForNamespaces(s, namespaceIDs, n.IsArchived) - if err != nil { - return nil, 0, 0, err - } - - /////////////// - // Shared Projects - - sharedProjectsNamespace, err := getSharedProjectsInNamespace(s, n.IsArchived, doer) - if err != nil { - return nil, 0, 0, err - } - - if sharedProjectsNamespace != nil { - namespaces[sharedProjectsNamespace.ID] = sharedProjectsNamespace - projects = append(projects, sharedProjectsNamespace.Projects...) - } - - ///////////////// - // Saved Filters - - savedFiltersNamespace, err := getSavedFilters(s, doer) - if err != nil { - return nil, 0, 0, err - } - - if savedFiltersNamespace != nil { - namespaces[savedFiltersNamespace.ID] = savedFiltersNamespace - projects = append(projects, savedFiltersNamespace.Projects...) - } - - ///////////////// - // Add project details (favorite state, among other things) - err = addProjectDetails(s, projects, a) - if err != nil { - return - } - - ///////////////// - // Favorite projects - - favoritesNamespace, err := getFavoriteProjects(s, projects, namespaceIDs, doer) - if err != nil { - return nil, 0, 0, err - } - - if favoritesNamespace != nil { - namespaces[favoritesNamespace.ID] = favoritesNamespace - } - - ////////////////////// - // Put it all together - - for _, project := range projects { - if project.NamespaceID == SharedProjectsPseudoNamespace.ID || project.NamespaceID == SavedFiltersPseudoNamespace.ID { - // Shared projects and filtered projects are already in the namespace - continue - } - namespaces[project.NamespaceID].Projects = append(namespaces[project.NamespaceID].Projects, project) - } - - all := makeNamespaceSlice(namespaces, ownerMap, subscriptionsMap) - return all, len(all), numberOfTotalItems, err -} - -// Create implements the creation method via the interface -// @Summary Creates a new namespace -// @Description Creates a new namespace. -// @tags namespace -// @Accept json -// @Produce json -// @Security JWTKeyAuth -// @Param namespace body models.Namespace true "The namespace you want to create." -// @Success 201 {object} models.Namespace "The created namespace." -// @Failure 400 {object} web.HTTPError "Invalid namespace object provided." -// @Failure 403 {object} web.HTTPError "The user does not have access to the namespace" -// @Failure 500 {object} models.Message "Internal error" -// @Router /namespaces [put] -func (n *Namespace) Create(s *xorm.Session, a web.Auth) (err error) { - // Check if we have at least a title - if n.Title == "" { - return ErrNamespaceNameCannotBeEmpty{NamespaceID: 0, UserID: a.GetID()} - } - - n.Owner, err = user.GetUserByID(s, a.GetID()) - if err != nil { - return - } - n.OwnerID = n.Owner.ID - - if _, err = s.Insert(n); err != nil { - return err - } - - err = events.Dispatch(&NamespaceCreatedEvent{ - Namespace: n, - Doer: a, - }) - if err != nil { - return err - } - - return -} - -// CreateNewNamespaceForUser creates a new namespace for a user. To prevent import cycles, we can't do that -// directly in the user.Create function. -func CreateNewNamespaceForUser(s *xorm.Session, user *user.User) (err error) { - newN := &Namespace{ - Title: user.Username, - Description: user.Username + "'s namespace.", - } - return newN.Create(s, user) -} - -// Delete deletes a namespace -// @Summary Deletes a namespace -// @Description Delets a namespace -// @tags namespace -// @Produce json -// @Security JWTKeyAuth -// @Param id path int true "Namespace ID" -// @Success 200 {object} models.Message "The namespace was successfully deleted." -// @Failure 400 {object} web.HTTPError "Invalid namespace object provided." -// @Failure 403 {object} web.HTTPError "The user does not have access to the namespace" -// @Failure 500 {object} models.Message "Internal error" -// @Router /namespaces/{id} [delete] -func (n *Namespace) Delete(s *xorm.Session, a web.Auth) (err error) { - return deleteNamespace(s, n, a, true) -} - -func deleteNamespace(s *xorm.Session, n *Namespace, a web.Auth, withProjects bool) (err error) { - // Check if the namespace exists - _, err = GetNamespaceByID(s, n.ID) - if err != nil { - return - } - - // Delete the namespace - _, err = s.ID(n.ID).Delete(&Namespace{}) - if err != nil { - return - } - - namespaceDeleted := &NamespaceDeletedEvent{ - Namespace: n, - Doer: a, - } - - if !withProjects { - return events.Dispatch(namespaceDeleted) - } - - // Delete all projects with their tasks - projects, err := GetProjectsByNamespaceID(s, n.ID, &user.User{}) - if err != nil { - return - } - - if len(projects) == 0 { - return events.Dispatch(namespaceDeleted) - } - - // Looping over all projects to let the project handle properly cleaning up the tasks and everything else associated with it. - for _, project := range projects { - err = project.Delete(s, a) - if err != nil { - return err - } - } - - return events.Dispatch(namespaceDeleted) -} - -// Update implements the update method via the interface -// @Summary Updates a namespace -// @Description Updates a namespace. -// @tags namespace -// @Accept json -// @Produce json -// @Security JWTKeyAuth -// @Param id path int true "Namespace ID" -// @Param namespace body models.Namespace true "The namespace with updated values you want to update." -// @Success 200 {object} models.Namespace "The updated namespace." -// @Failure 400 {object} web.HTTPError "Invalid namespace object provided." -// @Failure 403 {object} web.HTTPError "The user does not have access to the namespace" -// @Failure 500 {object} models.Message "Internal error" -// @Router /namespace/{id} [post] -func (n *Namespace) Update(s *xorm.Session, a web.Auth) (err error) { - // Check if we have at least a name - if n.Title == "" { - return ErrNamespaceNameCannotBeEmpty{NamespaceID: n.ID} - } - - // Check if the namespace exists - currentNamespace, err := GetNamespaceByID(s, n.ID) - if err != nil { - return - } - - // Check if the namespace is archived and the update is not un-archiving it - if currentNamespace.IsArchived && n.IsArchived { - return ErrNamespaceIsArchived{NamespaceID: n.ID} - } - - // Check if the (new) owner exists - if n.Owner != nil { - n.OwnerID = n.Owner.ID - if currentNamespace.OwnerID != n.OwnerID { - n.Owner, err = user.GetUserByID(s, n.OwnerID) - if err != nil { - return - } - } - } - - // We need to specify the cols we want to update here to be able to un-archive projects - colsToUpdate := []string{ - "title", - "is_archived", - "hex_color", - } - if n.Description != "" { - colsToUpdate = append(colsToUpdate, "description") - } - - // Do the actual update - _, err = s. - ID(currentNamespace.ID). - Cols(colsToUpdate...). - Update(n) - if err != nil { - return err - } - - return events.Dispatch(&NamespaceUpdatedEvent{ - Namespace: n, - Doer: a, - }) -} diff --git a/pkg/models/namespace_rights.go b/pkg/models/namespace_rights.go deleted file mode 100644 index 36d964897..000000000 --- a/pkg/models/namespace_rights.go +++ /dev/null @@ -1,145 +0,0 @@ -// Vikunja is a to-do list application to facilitate your life. -// Copyright 2018-2021 Vikunja and contributors. All rights reserved. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public Licensee as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public Licensee for more details. -// -// You should have received a copy of the GNU Affero General Public Licensee -// along with this program. If not, see . - -package models - -import ( - "code.vikunja.io/web" - "xorm.io/builder" - "xorm.io/xorm" -) - -// CanWrite checks if a user has write access to a namespace -func (n *Namespace) CanWrite(s *xorm.Session, a web.Auth) (bool, error) { - can, _, err := n.checkRight(s, a, RightWrite, RightAdmin) - return can, err -} - -// IsAdmin returns true or false if the user is admin on that namespace or not -func (n *Namespace) IsAdmin(s *xorm.Session, a web.Auth) (bool, error) { - is, _, err := n.checkRight(s, a, RightAdmin) - return is, err -} - -// CanRead checks if a user has read access to that namespace -func (n *Namespace) CanRead(s *xorm.Session, a web.Auth) (bool, int, error) { - return n.checkRight(s, a, RightRead, RightWrite, RightAdmin) -} - -// CanUpdate checks if the user can update the namespace -func (n *Namespace) CanUpdate(s *xorm.Session, a web.Auth) (bool, error) { - return n.IsAdmin(s, a) -} - -// CanDelete checks if the user can delete a namespace -func (n *Namespace) CanDelete(s *xorm.Session, a web.Auth) (bool, error) { - return n.IsAdmin(s, a) -} - -// CanCreate checks if the user can create a new namespace -func (n *Namespace) CanCreate(s *xorm.Session, a web.Auth) (bool, error) { - if _, is := a.(*LinkSharing); is { - return false, nil - } - - // This is currently a dummy function, later on we could imagine global limits etc. - return true, nil -} - -func (n *Namespace) checkRight(s *xorm.Session, a web.Auth, rights ...Right) (bool, int, error) { - - // If the auth is a link share, don't do anything - if _, is := a.(*LinkSharing); is { - return false, 0, nil - } - - // Get the namespace and check the right - nn, err := getNamespaceSimpleByID(s, n.ID) - if err != nil { - return false, 0, err - } - - if a.GetID() == nn.OwnerID || - nn.ID == SharedProjectsPseudoNamespace.ID || - nn.ID == FavoritesPseudoNamespace.ID || - nn.ID == SavedFiltersPseudoNamespace.ID { - return true, int(RightAdmin), nil - } - - /* - The following loop creates an sql condition like this one: - - namespaces.owner_id = 1 OR - (users_namespaces.user_id = 1 AND users_namespaces.right = 1) OR - (team_members.user_id = 1 AND team_namespaces.right = 1) OR - - - for each passed right. That way, we can check with a single sql query (instead if 8) - if the user has the right to see the project or not. - */ - - var conds []builder.Cond - conds = append(conds, builder.Eq{"namespaces.owner_id": a.GetID()}) - for _, r := range rights { - // User conditions - // If the namespace was shared directly with the user and the user has the right - conds = append(conds, builder.And( - builder.Eq{"users_namespaces.user_id": a.GetID()}, - builder.Eq{"users_namespaces.right": r}, - )) - - // Team rights - // If the namespace was shared directly with the team and the team has the right - conds = append(conds, builder.And( - builder.Eq{"team_members.user_id": a.GetID()}, - builder.Eq{"team_namespaces.right": r}, - )) - } - - type allRights struct { - UserNamespace NamespaceUser `xorm:"extends"` - TeamNamespace TeamNamespace `xorm:"extends"` - } - - var maxRights = 0 - r := &allRights{} - exists, err := s. - Select("*"). - Table("namespaces"). - // User stuff - Join("LEFT", "users_namespaces", "users_namespaces.namespace_id = namespaces.id"). - // Teams stuff - Join("LEFT", "team_namespaces", "namespaces.id = team_namespaces.namespace_id"). - Join("LEFT", "team_members", "team_members.team_id = team_namespaces.team_id"). - // The actual condition - Where(builder.And( - builder.Or( - conds..., - ), - builder.Eq{"namespaces.id": n.ID}, - )). - Exist(r) - - // Figure out the max right and return it - if int(r.UserNamespace.Right) > maxRights { - maxRights = int(r.UserNamespace.Right) - } - if int(r.TeamNamespace.Right) > maxRights { - maxRights = int(r.TeamNamespace.Right) - } - - return exists, maxRights, err -} diff --git a/pkg/models/namespace_team.go b/pkg/models/namespace_team.go deleted file mode 100644 index 392912778..000000000 --- a/pkg/models/namespace_team.go +++ /dev/null @@ -1,244 +0,0 @@ -// Vikunja is a to-do list application to facilitate your life. -// Copyright 2018-2021 Vikunja and contributors. All rights reserved. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public Licensee as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public Licensee for more details. -// -// You should have received a copy of the GNU Affero General Public Licensee -// along with this program. If not, see . - -package models - -import ( - "time" - - "code.vikunja.io/api/pkg/db" - - "code.vikunja.io/api/pkg/events" - "code.vikunja.io/web" - - "xorm.io/xorm" -) - -// TeamNamespace defines the relationship between a Team and a Namespace -type TeamNamespace struct { - // The unique, numeric id of this namespace <-> team relation. - ID int64 `xorm:"bigint autoincr not null unique pk" json:"id"` - // The team id. - TeamID int64 `xorm:"bigint not null INDEX" json:"team_id" param:"team"` - // The namespace id. - NamespaceID int64 `xorm:"bigint not null INDEX" json:"-" param:"namespace"` - // The right this team has. 0 = Read only, 1 = Read & Write, 2 = Admin. See the docs for more details. - Right Right `xorm:"bigint INDEX not null default 0" json:"right" valid:"length(0|2)" maximum:"2" default:"0"` - - // A timestamp when this relation was created. You cannot change this value. - Created time.Time `xorm:"created not null" json:"created"` - // A timestamp when this relation was last updated. You cannot change this value. - Updated time.Time `xorm:"updated not null" json:"updated"` - - web.CRUDable `xorm:"-" json:"-"` - web.Rights `xorm:"-" json:"-"` -} - -// TableName makes beautiful table names -func (TeamNamespace) TableName() string { - return "team_namespaces" -} - -// Create creates a new team <-> namespace relation -// @Summary Add a team to a namespace -// @Description Gives a team access to a namespace. -// @tags sharing -// @Accept json -// @Produce json -// @Security JWTKeyAuth -// @Param id path int true "Namespace ID" -// @Param namespace body models.TeamNamespace true "The team you want to add to the namespace." -// @Success 201 {object} models.TeamNamespace "The created team<->namespace relation." -// @Failure 400 {object} web.HTTPError "Invalid team namespace object provided." -// @Failure 404 {object} web.HTTPError "The team does not exist." -// @Failure 403 {object} web.HTTPError "The team does not have access to the namespace" -// @Failure 500 {object} models.Message "Internal error" -// @Router /namespaces/{id}/teams [put] -func (tn *TeamNamespace) Create(s *xorm.Session, a web.Auth) (err error) { - - // Check if the rights are valid - if err = tn.Right.isValid(); err != nil { - return - } - - // Check if the team exists - team, err := GetTeamByID(s, tn.TeamID) - if err != nil { - return err - } - - // Check if the namespace exists - namespace, err := GetNamespaceByID(s, tn.NamespaceID) - if err != nil { - return err - } - - // Check if the team already has access to the namespace - exists, err := s. - Where("team_id = ?", tn.TeamID). - And("namespace_id = ?", tn.NamespaceID). - Get(&TeamNamespace{}) - if err != nil { - return - } - if exists { - return ErrTeamAlreadyHasAccess{tn.TeamID, tn.NamespaceID} - } - - // Insert the new team - _, err = s.Insert(tn) - if err != nil { - return err - } - - return events.Dispatch(&NamespaceSharedWithTeamEvent{ - Namespace: namespace, - Team: team, - Doer: a, - }) -} - -// Delete deletes a team <-> namespace relation based on the namespace & team id -// @Summary Delete a team from a namespace -// @Description Delets a team from a namespace. The team won't have access to the namespace anymore. -// @tags sharing -// @Produce json -// @Security JWTKeyAuth -// @Param namespaceID path int true "Namespace ID" -// @Param teamID path int true "team ID" -// @Success 200 {object} models.Message "The team was successfully deleted." -// @Failure 403 {object} web.HTTPError "The team does not have access to the namespace" -// @Failure 404 {object} web.HTTPError "team or namespace does not exist." -// @Failure 500 {object} models.Message "Internal error" -// @Router /namespaces/{namespaceID}/teams/{teamID} [delete] -func (tn *TeamNamespace) Delete(s *xorm.Session, a web.Auth) (err error) { - - // Check if the team exists - _, err = GetTeamByID(s, tn.TeamID) - if err != nil { - return - } - - // Check if the team has access to the namespace - has, err := s. - Where("team_id = ? AND namespace_id = ?", tn.TeamID, tn.NamespaceID). - Get(&TeamNamespace{}) - if err != nil { - return - } - if !has { - return ErrTeamDoesNotHaveAccessToNamespace{TeamID: tn.TeamID, NamespaceID: tn.NamespaceID} - } - - // Delete the relation - _, err = s. - Where("team_id = ?", tn.TeamID). - And("namespace_id = ?", tn.NamespaceID). - Delete(TeamNamespace{}) - - return -} - -// ReadAll implements the method to read all teams of a namespace -// @Summary Get teams on a namespace -// @Description Returns a namespace with all teams which have access on a given namespace. -// @tags sharing -// @Accept json -// @Produce json -// @Param id path int true "Namespace ID" -// @Param page query int false "The page number. Used for pagination. If not provided, the first page of results is returned." -// @Param per_page query int false "The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page." -// @Param s query string false "Search teams by its name." -// @Security JWTKeyAuth -// @Success 200 {array} models.TeamWithRight "The teams with the right they have." -// @Failure 403 {object} web.HTTPError "No right to see the namespace." -// @Failure 500 {object} models.Message "Internal error" -// @Router /namespaces/{id}/teams [get] -func (tn *TeamNamespace) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) { - // Check if the user can read the namespace - n := Namespace{ID: tn.NamespaceID} - canRead, _, err := n.CanRead(s, a) - if err != nil { - return nil, 0, 0, err - } - if !canRead { - return nil, 0, 0, ErrNeedToHaveNamespaceReadAccess{NamespaceID: tn.NamespaceID, UserID: a.GetID()} - } - - // Get the teams - all := []*TeamWithRight{} - limit, start := getLimitFromPageIndex(page, perPage) - query := s. - Table("teams"). - Join("INNER", "team_namespaces", "team_id = teams.id"). - Where("team_namespaces.namespace_id = ?", tn.NamespaceID). - Where(db.ILIKE("teams.name", search)) - if limit > 0 { - query = query.Limit(limit, start) - } - err = query.Find(&all) - if err != nil { - return nil, 0, 0, err - } - - teams := []*Team{} - for _, t := range all { - teams = append(teams, &t.Team) - } - - err = addMoreInfoToTeams(s, teams) - if err != nil { - return - } - - numberOfTotalItems, err = s. - Table("teams"). - Join("INNER", "team_namespaces", "team_id = teams.id"). - Where("team_namespaces.namespace_id = ?", tn.NamespaceID). - Where("teams.name LIKE ?", "%"+search+"%"). - Count(&TeamWithRight{}) - - return all, len(all), numberOfTotalItems, err -} - -// Update updates a team <-> namespace relation -// @Summary Update a team <-> namespace relation -// @Description Update a team <-> namespace relation. Mostly used to update the right that team has. -// @tags sharing -// @Accept json -// @Produce json -// @Param namespaceID path int true "Namespace ID" -// @Param teamID path int true "Team ID" -// @Param namespace body models.TeamNamespace true "The team you want to update." -// @Security JWTKeyAuth -// @Success 200 {object} models.TeamNamespace "The updated team <-> namespace relation." -// @Failure 403 {object} web.HTTPError "The team does not have admin-access to the namespace" -// @Failure 404 {object} web.HTTPError "Team or namespace does not exist." -// @Failure 500 {object} models.Message "Internal error" -// @Router /namespaces/{namespaceID}/teams/{teamID} [post] -func (tn *TeamNamespace) Update(s *xorm.Session, a web.Auth) (err error) { - - // Check if the right is valid - if err := tn.Right.isValid(); err != nil { - return err - } - - _, err = s. - Where("namespace_id = ? AND team_id = ?", tn.NamespaceID, tn.TeamID). - Cols("right"). - Update(tn) - return -} diff --git a/pkg/models/namespace_team_rights.go b/pkg/models/namespace_team_rights.go deleted file mode 100644 index edde7125a..000000000 --- a/pkg/models/namespace_team_rights.go +++ /dev/null @@ -1,40 +0,0 @@ -// Vikunja is a to-do list application to facilitate your life. -// Copyright 2018-2021 Vikunja and contributors. All rights reserved. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public Licensee as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public Licensee for more details. -// -// You should have received a copy of the GNU Affero General Public Licensee -// along with this program. If not, see . - -package models - -import ( - "code.vikunja.io/web" - "xorm.io/xorm" -) - -// CanCreate checks if one can create a new team <-> namespace relation -func (tn *TeamNamespace) CanCreate(s *xorm.Session, a web.Auth) (bool, error) { - n := &Namespace{ID: tn.NamespaceID} - return n.IsAdmin(s, a) -} - -// CanDelete checks if a user can remove a team from a namespace. Only namespace admins can do that. -func (tn *TeamNamespace) CanDelete(s *xorm.Session, a web.Auth) (bool, error) { - n := &Namespace{ID: tn.NamespaceID} - return n.IsAdmin(s, a) -} - -// CanUpdate checks if a user can update a team from a Only namespace admins can do that. -func (tn *TeamNamespace) CanUpdate(s *xorm.Session, a web.Auth) (bool, error) { - n := &Namespace{ID: tn.NamespaceID} - return n.IsAdmin(s, a) -} diff --git a/pkg/models/namespace_team_rights_test.go b/pkg/models/namespace_team_rights_test.go deleted file mode 100644 index e9dc37c67..000000000 --- a/pkg/models/namespace_team_rights_test.go +++ /dev/null @@ -1,107 +0,0 @@ -// Vikunja is a to-do list application to facilitate your life. -// Copyright 2018-2021 Vikunja and contributors. All rights reserved. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public Licensee as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public Licensee for more details. -// -// You should have received a copy of the GNU Affero General Public Licensee -// along with this program. If not, see . - -package models - -import ( - "testing" - "time" - - "code.vikunja.io/api/pkg/db" - "code.vikunja.io/api/pkg/user" - - "code.vikunja.io/web" -) - -func TestTeamNamespace_CanDoSomething(t *testing.T) { - type fields struct { - ID int64 - TeamID int64 - NamespaceID int64 - Right Right - Created time.Time - Updated time.Time - CRUDable web.CRUDable - Rights web.Rights - } - type args struct { - a web.Auth - } - tests := []struct { - name string - fields fields - args args - want map[string]bool - }{ - { - name: "CanDoSomething Normally", - fields: fields{ - NamespaceID: 3, - }, - args: args{ - a: &user.User{ID: 3}, - }, - want: map[string]bool{"CanCreate": true, "CanDelete": true, "CanUpdate": true}, - }, - { - name: "CanDoSomething for a nonexistant namespace", - fields: fields{ - NamespaceID: 300, - }, - args: args{ - a: &user.User{ID: 3}, - }, - want: map[string]bool{"CanCreate": false, "CanDelete": false, "CanUpdate": false}, - }, - { - name: "CanDoSomething where the user does not have the rights", - fields: fields{ - NamespaceID: 3, - }, - args: args{ - a: &user.User{ID: 4}, - }, - want: map[string]bool{"CanCreate": false, "CanDelete": false, "CanUpdate": false}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - db.LoadAndAssertFixtures(t) - s := db.NewSession() - - tn := &TeamNamespace{ - ID: tt.fields.ID, - TeamID: tt.fields.TeamID, - NamespaceID: tt.fields.NamespaceID, - Right: tt.fields.Right, - Created: tt.fields.Created, - Updated: tt.fields.Updated, - CRUDable: tt.fields.CRUDable, - Rights: tt.fields.Rights, - } - if got, _ := tn.CanCreate(s, tt.args.a); got != tt.want["CanCreate"] { - t.Errorf("TeamNamespace.CanCreate() = %v, want %v", got, tt.want["CanCreate"]) - } - if got, _ := tn.CanDelete(s, tt.args.a); got != tt.want["CanDelete"] { - t.Errorf("TeamNamespace.CanDelete() = %v, want %v", got, tt.want["CanDelete"]) - } - if got, _ := tn.CanUpdate(s, tt.args.a); got != tt.want["CanUpdate"] { - t.Errorf("TeamNamespace.CanUpdate() = %v, want %v", got, tt.want["CanUpdate"]) - } - _ = s.Close() - }) - } -} diff --git a/pkg/models/namespace_team_test.go b/pkg/models/namespace_team_test.go deleted file mode 100644 index 9417df459..000000000 --- a/pkg/models/namespace_team_test.go +++ /dev/null @@ -1,298 +0,0 @@ -// Vikunja is a to-do list application to facilitate your life. -// Copyright 2018-2021 Vikunja and contributors. All rights reserved. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public Licensee as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public Licensee for more details. -// -// You should have received a copy of the GNU Affero General Public Licensee -// along with this program. If not, see . - -package models - -import ( - "reflect" - "runtime" - "testing" - "time" - - "code.vikunja.io/api/pkg/db" - "code.vikunja.io/api/pkg/user" - "code.vikunja.io/web" - "github.com/stretchr/testify/assert" -) - -func TestTeamNamespace_ReadAll(t *testing.T) { - u := &user.User{ID: 1} - - t.Run("normal", func(t *testing.T) { - tn := TeamNamespace{ - NamespaceID: 3, - } - db.LoadAndAssertFixtures(t) - s := db.NewSession() - teams, _, _, err := tn.ReadAll(s, u, "", 1, 50) - assert.NoError(t, err) - assert.Equal(t, reflect.TypeOf(teams).Kind(), reflect.Slice) - ts := reflect.ValueOf(teams) - assert.Equal(t, ts.Len(), 2) - _ = s.Close() - }) - t.Run("nonexistant namespace", func(t *testing.T) { - tn := TeamNamespace{ - NamespaceID: 9999, - } - db.LoadAndAssertFixtures(t) - s := db.NewSession() - _, _, _, err := tn.ReadAll(s, u, "", 1, 50) - assert.Error(t, err) - assert.True(t, IsErrNamespaceDoesNotExist(err)) - _ = s.Close() - }) - t.Run("no right for namespace", func(t *testing.T) { - tn := TeamNamespace{ - NamespaceID: 17, - } - db.LoadAndAssertFixtures(t) - s := db.NewSession() - _, _, _, err := tn.ReadAll(s, u, "", 1, 50) - assert.Error(t, err) - assert.True(t, IsErrNeedToHaveNamespaceReadAccess(err)) - _ = s.Close() - }) - t.Run("search", func(t *testing.T) { - tn := TeamNamespace{ - NamespaceID: 3, - } - db.LoadAndAssertFixtures(t) - s := db.NewSession() - teams, _, _, err := tn.ReadAll(s, u, "READ_only_on_project6", 1, 50) - assert.NoError(t, err) - assert.Equal(t, reflect.TypeOf(teams).Kind(), reflect.Slice) - ts := teams.([]*TeamWithRight) - assert.Len(t, ts, 1) - assert.Equal(t, int64(2), ts[0].ID) - - _ = s.Close() - }) -} - -func TestTeamNamespace_Create(t *testing.T) { - u := &user.User{ID: 1} - - t.Run("normal", func(t *testing.T) { - tn := TeamNamespace{ - TeamID: 1, - NamespaceID: 1, - Right: RightAdmin, - } - db.LoadAndAssertFixtures(t) - s := db.NewSession() - allowed, _ := tn.CanCreate(s, u) - assert.True(t, allowed) - err := tn.Create(s, u) - assert.NoError(t, err) - - err = s.Commit() - assert.NoError(t, err) - - db.AssertExists(t, "team_namespaces", map[string]interface{}{ - "team_id": 1, - "namespace_id": 1, - "right": RightAdmin, - }, false) - }) - t.Run("team already has access", func(t *testing.T) { - tn := TeamNamespace{ - TeamID: 1, - NamespaceID: 3, - Right: RightRead, - } - db.LoadAndAssertFixtures(t) - s := db.NewSession() - err := tn.Create(s, u) - assert.Error(t, err) - assert.True(t, IsErrTeamAlreadyHasAccess(err)) - _ = s.Close() - }) - t.Run("invalid team right", func(t *testing.T) { - tn := TeamNamespace{ - TeamID: 1, - NamespaceID: 3, - Right: RightUnknown, - } - db.LoadAndAssertFixtures(t) - s := db.NewSession() - err := tn.Create(s, u) - assert.Error(t, err) - assert.True(t, IsErrInvalidRight(err)) - _ = s.Close() - }) - t.Run("nonexistant team", func(t *testing.T) { - tn := TeamNamespace{ - TeamID: 9999, - NamespaceID: 1, - } - db.LoadAndAssertFixtures(t) - s := db.NewSession() - err := tn.Create(s, u) - assert.Error(t, err) - assert.True(t, IsErrTeamDoesNotExist(err)) - _ = s.Close() - }) - t.Run("nonexistant namespace", func(t *testing.T) { - tn := TeamNamespace{ - TeamID: 1, - NamespaceID: 9999, - } - db.LoadAndAssertFixtures(t) - s := db.NewSession() - err := tn.Create(s, u) - assert.Error(t, err) - assert.True(t, IsErrNamespaceDoesNotExist(err)) - _ = s.Close() - }) -} - -func TestTeamNamespace_Delete(t *testing.T) { - u := &user.User{ID: 1} - - t.Run("normal", func(t *testing.T) { - tn := TeamNamespace{ - TeamID: 7, - NamespaceID: 9, - } - db.LoadAndAssertFixtures(t) - s := db.NewSession() - allowed, _ := tn.CanDelete(s, u) - assert.True(t, allowed) - err := tn.Delete(s, u) - assert.NoError(t, err) - err = s.Commit() - assert.NoError(t, err) - - db.AssertMissing(t, "team_namespaces", map[string]interface{}{ - "team_id": 7, - "namespace_id": 9, - }) - }) - t.Run("nonexistant team", func(t *testing.T) { - tn := TeamNamespace{ - TeamID: 9999, - NamespaceID: 3, - } - db.LoadAndAssertFixtures(t) - s := db.NewSession() - err := tn.Delete(s, u) - assert.Error(t, err) - assert.True(t, IsErrTeamDoesNotExist(err)) - _ = s.Close() - }) - t.Run("nonexistant namespace", func(t *testing.T) { - tn := TeamNamespace{ - TeamID: 1, - NamespaceID: 9999, - } - db.LoadAndAssertFixtures(t) - s := db.NewSession() - err := tn.Delete(s, u) - assert.Error(t, err) - assert.True(t, IsErrTeamDoesNotHaveAccessToNamespace(err)) - _ = s.Close() - }) -} - -func TestTeamNamespace_Update(t *testing.T) { - type fields struct { - ID int64 - TeamID int64 - NamespaceID int64 - Right Right - Created time.Time - Updated time.Time - CRUDable web.CRUDable - Rights web.Rights - } - tests := []struct { - name string - fields fields - wantErr bool - errType func(err error) bool - }{ - { - name: "Test Update Normally", - fields: fields{ - NamespaceID: 3, - TeamID: 1, - Right: RightAdmin, - }, - }, - { - name: "Test Update to write", - fields: fields{ - NamespaceID: 3, - TeamID: 1, - Right: RightWrite, - }, - }, - { - name: "Test Update to Read", - fields: fields{ - NamespaceID: 3, - TeamID: 1, - Right: RightRead, - }, - }, - { - name: "Test Update with invalid right", - fields: fields{ - NamespaceID: 3, - TeamID: 1, - Right: 500, - }, - wantErr: true, - errType: IsErrInvalidRight, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - db.LoadAndAssertFixtures(t) - s := db.NewSession() - - tl := &TeamNamespace{ - ID: tt.fields.ID, - TeamID: tt.fields.TeamID, - NamespaceID: tt.fields.NamespaceID, - Right: tt.fields.Right, - Created: tt.fields.Created, - Updated: tt.fields.Updated, - CRUDable: tt.fields.CRUDable, - Rights: tt.fields.Rights, - } - err := tl.Update(s, &user.User{ID: 1}) - if (err != nil) != tt.wantErr { - t.Errorf("TeamNamespace.Update() error = %v, wantErr %v", err, tt.wantErr) - } - if (err != nil) && tt.wantErr && !tt.errType(err) { - t.Errorf("TeamNamespace.Update() Wrong error type! Error = %v, want = %v", err, runtime.FuncForPC(reflect.ValueOf(tt.errType).Pointer()).Name()) - } - - err = s.Commit() - assert.NoError(t, err) - - if !tt.wantErr { - db.AssertExists(t, "team_namespaces", map[string]interface{}{ - "team_id": tt.fields.TeamID, - "namespace_id": tt.fields.NamespaceID, - "right": tt.fields.Right, - }, false) - } - }) - } -} diff --git a/pkg/models/namespace_test.go b/pkg/models/namespace_test.go deleted file mode 100644 index 3b6e54f51..000000000 --- a/pkg/models/namespace_test.go +++ /dev/null @@ -1,372 +0,0 @@ -// Vikunja is a to-do list application to facilitate your life. -// Copyright 2018-2021 Vikunja and contributors. All rights reserved. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public Licensee as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public Licensee for more details. -// -// You should have received a copy of the GNU Affero General Public Licensee -// along with this program. If not, see . - -package models - -import ( - "testing" - - "code.vikunja.io/api/pkg/db" - "code.vikunja.io/api/pkg/user" - "github.com/stretchr/testify/assert" -) - -func TestNamespace_Create(t *testing.T) { - - // Dummy namespace - dummynamespace := Namespace{ - Title: "Test", - Description: "Lorem Ipsum", - } - - user1 := &user.User{ID: 1} - - t.Run("normal", func(t *testing.T) { - db.LoadAndAssertFixtures(t) - s := db.NewSession() - err := dummynamespace.Create(s, user1) - assert.NoError(t, err) - err = s.Commit() - assert.NoError(t, err) - - db.AssertExists(t, "namespaces", map[string]interface{}{ - "title": "Test", - "description": "Lorem Ipsum", - }, false) - }) - t.Run("no title", func(t *testing.T) { - db.LoadAndAssertFixtures(t) - s := db.NewSession() - n2 := Namespace{} - err := n2.Create(s, user1) - assert.Error(t, err) - assert.True(t, IsErrNamespaceNameCannotBeEmpty(err)) - _ = s.Close() - }) - t.Run("nonexistant user", func(t *testing.T) { - db.LoadAndAssertFixtures(t) - s := db.NewSession() - nUser := &user.User{ID: 9482385} - dnsp2 := dummynamespace - err := dnsp2.Create(s, nUser) - assert.Error(t, err) - assert.True(t, user.IsErrUserDoesNotExist(err)) - _ = s.Close() - }) -} - -func TestNamespace_ReadOne(t *testing.T) { - u := &user.User{ID: 1} - - t.Run("normal", func(t *testing.T) { - n := &Namespace{ID: 1} - db.LoadAndAssertFixtures(t) - s := db.NewSession() - defer s.Close() - - err := n.ReadOne(s, u) - assert.NoError(t, err) - assert.Equal(t, n.Title, "testnamespace") - }) - t.Run("nonexistant", func(t *testing.T) { - n := &Namespace{ID: 99999} - db.LoadAndAssertFixtures(t) - s := db.NewSession() - defer s.Close() - - err := n.ReadOne(s, u) - assert.Error(t, err) - assert.True(t, IsErrNamespaceDoesNotExist(err)) - }) - t.Run("with subscription", func(t *testing.T) { - n := &Namespace{ID: 8} - db.LoadAndAssertFixtures(t) - s := db.NewSession() - defer s.Close() - - err := n.ReadOne(s, &user.User{ID: 6}) - assert.NoError(t, err) - assert.NotNil(t, n.Subscription) - }) -} - -func TestNamespace_Update(t *testing.T) { - u := &user.User{ID: 1} - - t.Run("normal", func(t *testing.T) { - db.LoadAndAssertFixtures(t) - s := db.NewSession() - n := &Namespace{ - ID: 1, - Title: "Lorem Ipsum", - } - err := n.Update(s, u) - assert.NoError(t, err) - err = s.Commit() - assert.NoError(t, err) - - db.AssertExists(t, "namespaces", map[string]interface{}{ - "id": 1, - "title": "Lorem Ipsum", - }, false) - }) - t.Run("nonexisting", func(t *testing.T) { - db.LoadAndAssertFixtures(t) - s := db.NewSession() - n := &Namespace{ - ID: 99999, - Title: "Lorem Ipsum", - } - err := n.Update(s, u) - assert.Error(t, err) - assert.True(t, IsErrNamespaceDoesNotExist(err)) - _ = s.Close() - }) - t.Run("nonexisting owner", func(t *testing.T) { - db.LoadAndAssertFixtures(t) - s := db.NewSession() - n := &Namespace{ - ID: 1, - Title: "Lorem Ipsum", - Owner: &user.User{ID: 99999}, - } - err := n.Update(s, u) - assert.Error(t, err) - assert.True(t, user.IsErrUserDoesNotExist(err)) - _ = s.Close() - }) - t.Run("no title", func(t *testing.T) { - db.LoadAndAssertFixtures(t) - s := db.NewSession() - n := &Namespace{ - ID: 1, - } - err := n.Update(s, u) - assert.Error(t, err) - assert.True(t, IsErrNamespaceNameCannotBeEmpty(err)) - _ = s.Close() - }) -} - -func TestNamespace_Delete(t *testing.T) { - u := &user.User{ID: 1} - - t.Run("normal", func(t *testing.T) { - db.LoadAndAssertFixtures(t) - s := db.NewSession() - n := &Namespace{ - ID: 1, - } - err := n.Delete(s, u) - assert.NoError(t, err) - err = s.Commit() - assert.NoError(t, err) - - db.AssertMissing(t, "namespaces", map[string]interface{}{ - "id": 1, - }) - }) - t.Run("nonexisting", func(t *testing.T) { - db.LoadAndAssertFixtures(t) - s := db.NewSession() - n := &Namespace{ - ID: 9999, - } - err := n.Delete(s, u) - assert.Error(t, err) - assert.True(t, IsErrNamespaceDoesNotExist(err)) - _ = s.Close() - }) -} - -func TestNamespace_ReadAll(t *testing.T) { - user1 := &user.User{ID: 1} - user6 := &user.User{ID: 6} - user7 := &user.User{ID: 7} - user11 := &user.User{ID: 11} - user12 := &user.User{ID: 12} - - t.Run("normal", func(t *testing.T) { - db.LoadAndAssertFixtures(t) - s := db.NewSession() - defer s.Close() - - n := &Namespace{} - nn, _, _, err := n.ReadAll(s, user1, "", 1, -1) - assert.NoError(t, err) - namespaces := nn.([]*NamespaceWithProjects) - assert.NotNil(t, namespaces) - assert.Len(t, namespaces, 11) // Total of 11 including shared, favorites and saved filters - assert.Equal(t, int64(-3), namespaces[0].ID) // The first one should be the one with saved filters - assert.Equal(t, int64(-2), namespaces[1].ID) // The second one should be the one with favorites - assert.Equal(t, int64(-1), namespaces[2].ID) // The third one should be the one with the shared namespaces - // Ensure every project and namespace are not archived - for _, namespace := range namespaces { - assert.False(t, namespace.IsArchived) - for _, project := range namespace.Projects { - assert.False(t, project.IsArchived) - } - } - }) - t.Run("no own shared projects", func(t *testing.T) { - db.LoadAndAssertFixtures(t) - s := db.NewSession() - defer s.Close() - - n := &Namespace{} - nn, _, _, err := n.ReadAll(s, user6, "", 1, -1) - assert.NoError(t, err) - namespaces := nn.([]*NamespaceWithProjects) - assert.NotNil(t, namespaces) - assert.Equal(t, int64(-1), namespaces[1].ID) // The third one should be the one with the shared namespaces - - sharedProjectOccurences := make(map[int64]int64) - for _, project := range namespaces[1].Projects { - assert.NotEqual(t, user1.ID, project.OwnerID) - sharedProjectOccurences[project.ID]++ - } - - for projectID, occ := range sharedProjectOccurences { - assert.Equal(t, int64(1), occ, "shared project %d is present %d times, should be 1", projectID, occ) - } - }) - t.Run("namespaces only", func(t *testing.T) { - db.LoadAndAssertFixtures(t) - s := db.NewSession() - defer s.Close() - - n := &Namespace{ - NamespacesOnly: true, - } - nn, _, _, err := n.ReadAll(s, user1, "", 1, -1) - assert.NoError(t, err) - namespaces := nn.([]*NamespaceWithProjects) - assert.NotNil(t, namespaces) - assert.Len(t, namespaces, 8) // Total of 8 - excluding shared, favorites and saved filters (normally 11) - // Ensure every namespace does not contain projects - for _, namespace := range namespaces { - assert.Nil(t, namespace.Projects) - } - }) - t.Run("ids only", func(t *testing.T) { - db.LoadAndAssertFixtures(t) - s := db.NewSession() - defer s.Close() - - n := &Namespace{ - NamespacesOnly: true, - } - nn, _, _, err := n.ReadAll(s, user7, "13,14", 1, -1) - assert.NoError(t, err) - namespaces := nn.([]*NamespaceWithProjects) - assert.NotNil(t, namespaces) - assert.Len(t, namespaces, 2) - assert.Equal(t, int64(13), namespaces[0].ID) - assert.Equal(t, int64(14), namespaces[1].ID) - }) - t.Run("ids only but ids with other people's namespace", func(t *testing.T) { - db.LoadAndAssertFixtures(t) - s := db.NewSession() - defer s.Close() - - n := &Namespace{ - NamespacesOnly: true, - } - nn, _, _, err := n.ReadAll(s, user1, "1,w", 1, -1) - assert.NoError(t, err) - namespaces := nn.([]*NamespaceWithProjects) - assert.NotNil(t, namespaces) - assert.Len(t, namespaces, 1) - assert.Equal(t, int64(1), namespaces[0].ID) - }) - t.Run("archived", func(t *testing.T) { - db.LoadAndAssertFixtures(t) - s := db.NewSession() - defer s.Close() - - n := &Namespace{ - IsArchived: true, - } - nn, _, _, err := n.ReadAll(s, user1, "", 1, -1) - namespaces := nn.([]*NamespaceWithProjects) - assert.NoError(t, err) - assert.NotNil(t, namespaces) - assert.Len(t, namespaces, 12) // Total of 12 including shared & favorites, one is archived - assert.Equal(t, int64(-3), namespaces[0].ID) // The first one should be the one with shared filters - assert.Equal(t, int64(-2), namespaces[1].ID) // The second one should be the one with favorites - assert.Equal(t, int64(-1), namespaces[2].ID) // The third one should be the one with the shared namespaces - }) - t.Run("no favorites", func(t *testing.T) { - db.LoadAndAssertFixtures(t) - s := db.NewSession() - defer s.Close() - - n := &Namespace{} - nn, _, _, err := n.ReadAll(s, user11, "", 1, -1) - namespaces := nn.([]*NamespaceWithProjects) - assert.NoError(t, err) - // Assert the first namespace is not the favorites namespace - assert.NotEqual(t, FavoritesPseudoNamespace.ID, namespaces[0].ID) - }) - t.Run("no favorite tasks but namespace", func(t *testing.T) { - db.LoadAndAssertFixtures(t) - s := db.NewSession() - defer s.Close() - - n := &Namespace{} - nn, _, _, err := n.ReadAll(s, user12, "", 1, -1) - namespaces := nn.([]*NamespaceWithProjects) - assert.NoError(t, err) - // Assert the first namespace is the favorites namespace and contains projects - assert.Equal(t, FavoritesPseudoNamespace.ID, namespaces[0].ID) - assert.NotEqual(t, 0, namespaces[0].Projects) - }) - t.Run("no saved filters", func(t *testing.T) { - db.LoadAndAssertFixtures(t) - s := db.NewSession() - defer s.Close() - - n := &Namespace{} - nn, _, _, err := n.ReadAll(s, user11, "", 1, -1) - namespaces := nn.([]*NamespaceWithProjects) - assert.NoError(t, err) - // Assert the first namespace is not the favorites namespace - assert.NotEqual(t, SavedFiltersPseudoNamespace.ID, namespaces[0].ID) - }) - t.Run("no results", func(t *testing.T) { - db.LoadAndAssertFixtures(t) - s := db.NewSession() - defer s.Close() - - n := &Namespace{} - nn, _, _, err := n.ReadAll(s, user1, "some search string which will never return results", 1, -1) - assert.NoError(t, err) - assert.Nil(t, nn) - }) - t.Run("search", func(t *testing.T) { - db.LoadAndAssertFixtures(t) - s := db.NewSession() - defer s.Close() - - n := &Namespace{} - nn, _, _, err := n.ReadAll(s, user6, "NamespACE7", 1, -1) - assert.NoError(t, err) - namespaces := nn.([]*NamespaceWithProjects) - assert.NotNil(t, namespaces) - assert.Len(t, namespaces, 2) - assert.Equal(t, int64(7), namespaces[1].ID) - }) -} diff --git a/pkg/models/namespace_users.go b/pkg/models/namespace_users.go deleted file mode 100644 index eda9bfc3e..000000000 --- a/pkg/models/namespace_users.go +++ /dev/null @@ -1,251 +0,0 @@ -// Vikunja is a to-do list application to facilitate your life. -// Copyright 2018-2021 Vikunja and contributors. All rights reserved. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public Licensee as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public Licensee for more details. -// -// You should have received a copy of the GNU Affero General Public Licensee -// along with this program. If not, see . - -package models - -import ( - "time" - - "code.vikunja.io/api/pkg/db" - - "code.vikunja.io/api/pkg/events" - user2 "code.vikunja.io/api/pkg/user" - "code.vikunja.io/web" - - "xorm.io/xorm" -) - -// NamespaceUser represents a namespace <-> user relation -type NamespaceUser struct { - // The unique, numeric id of this namespace <-> user relation. - ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"namespace"` - // The username. - Username string `xorm:"-" json:"user_id" param:"user"` - UserID int64 `xorm:"bigint not null INDEX" json:"-"` - // The namespace id - NamespaceID int64 `xorm:"bigint not null INDEX" json:"-" param:"namespace"` - // The right this user has. 0 = Read only, 1 = Read & Write, 2 = Admin. See the docs for more details. - Right Right `xorm:"bigint INDEX not null default 0" json:"right" valid:"length(0|2)" maximum:"2" default:"0"` - - // A timestamp when this relation was created. You cannot change this value. - Created time.Time `xorm:"created not null" json:"created"` - // A timestamp when this relation was last updated. You cannot change this value. - Updated time.Time `xorm:"updated not null" json:"updated"` - - web.CRUDable `xorm:"-" json:"-"` - web.Rights `xorm:"-" json:"-"` -} - -// TableName is the table name for NamespaceUser -func (NamespaceUser) TableName() string { - return "users_namespaces" -} - -// Create creates a new namespace <-> user relation -// @Summary Add a user to a namespace -// @Description Gives a user access to a namespace. -// @tags sharing -// @Accept json -// @Produce json -// @Security JWTKeyAuth -// @Param id path int true "Namespace ID" -// @Param namespace body models.NamespaceUser true "The user you want to add to the namespace." -// @Success 201 {object} models.NamespaceUser "The created user<->namespace relation." -// @Failure 400 {object} web.HTTPError "Invalid user namespace object provided." -// @Failure 404 {object} web.HTTPError "The user does not exist." -// @Failure 403 {object} web.HTTPError "The user does not have access to the namespace" -// @Failure 500 {object} models.Message "Internal error" -// @Router /namespaces/{id}/users [put] -func (nu *NamespaceUser) Create(s *xorm.Session, a web.Auth) (err error) { - // Reset the id - nu.ID = 0 - - // Check if the right is valid - if err := nu.Right.isValid(); err != nil { - return err - } - - // Check if the namespace exists - n, err := GetNamespaceByID(s, nu.NamespaceID) - if err != nil { - return - } - - // Check if the user exists - user, err := user2.GetUserByUsername(s, nu.Username) - if err != nil { - return err - } - nu.UserID = user.ID - - // Check if the user already has access or is owner of that namespace - // We explicitly DO NOT check for teams here - if n.OwnerID == nu.UserID { - return ErrUserAlreadyHasNamespaceAccess{UserID: nu.UserID, NamespaceID: nu.NamespaceID} - } - - exist, err := s. - Where("namespace_id = ? AND user_id = ?", nu.NamespaceID, nu.UserID). - Get(&NamespaceUser{}) - if err != nil { - return - } - if exist { - return ErrUserAlreadyHasNamespaceAccess{UserID: nu.UserID, NamespaceID: nu.NamespaceID} - } - - // Insert user <-> namespace relation - _, err = s.Insert(nu) - if err != nil { - return err - } - - return events.Dispatch(&NamespaceSharedWithUserEvent{ - Namespace: n, - User: user, - Doer: a, - }) -} - -// Delete deletes a namespace <-> user relation -// @Summary Delete a user from a namespace -// @Description Delets a user from a namespace. The user won't have access to the namespace anymore. -// @tags sharing -// @Produce json -// @Security JWTKeyAuth -// @Param namespaceID path int true "Namespace ID" -// @Param userID path int true "user ID" -// @Success 200 {object} models.Message "The user was successfully deleted." -// @Failure 403 {object} web.HTTPError "The user does not have access to the namespace" -// @Failure 404 {object} web.HTTPError "user or namespace does not exist." -// @Failure 500 {object} models.Message "Internal error" -// @Router /namespaces/{namespaceID}/users/{userID} [delete] -func (nu *NamespaceUser) Delete(s *xorm.Session, a web.Auth) (err error) { - - // Check if the user exists - user, err := user2.GetUserByUsername(s, nu.Username) - if err != nil { - return - } - nu.UserID = user.ID - - // Check if the user has access to the namespace - has, err := s. - Where("user_id = ? AND namespace_id = ?", nu.UserID, nu.NamespaceID). - Get(&NamespaceUser{}) - if err != nil { - return - } - if !has { - return ErrUserDoesNotHaveAccessToNamespace{NamespaceID: nu.NamespaceID, UserID: nu.UserID} - } - - _, err = s. - Where("user_id = ? AND namespace_id = ?", nu.UserID, nu.NamespaceID). - Delete(&NamespaceUser{}) - return -} - -// ReadAll gets all users who have access to a namespace -// @Summary Get users on a namespace -// @Description Returns a namespace with all users which have access on a given namespace. -// @tags sharing -// @Accept json -// @Produce json -// @Param id path int true "Namespace ID" -// @Param page query int false "The page number. Used for pagination. If not provided, the first page of results is returned." -// @Param per_page query int false "The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page." -// @Param s query string false "Search users by its name." -// @Security JWTKeyAuth -// @Success 200 {array} models.UserWithRight "The users with the right they have." -// @Failure 403 {object} web.HTTPError "No right to see the namespace." -// @Failure 500 {object} models.Message "Internal error" -// @Router /namespaces/{id}/users [get] -func (nu *NamespaceUser) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) { - // Check if the user has access to the namespace - l := Namespace{ID: nu.NamespaceID} - canRead, _, err := l.CanRead(s, a) - if err != nil { - return nil, 0, 0, err - } - if !canRead { - return nil, 0, 0, ErrNeedToHaveNamespaceReadAccess{} - } - - // Get all users - all := []*UserWithRight{} - limit, start := getLimitFromPageIndex(page, perPage) - query := s. - Join("INNER", "users_namespaces", "user_id = users.id"). - Where("users_namespaces.namespace_id = ?", nu.NamespaceID). - Where(db.ILIKE("users.username", search)) - if limit > 0 { - query = query.Limit(limit, start) - } - err = query.Find(&all) - if err != nil { - return nil, 0, 0, err - } - - // Obfuscate all user emails - for _, u := range all { - u.Email = "" - } - - numberOfTotalItems, err = s. - Join("INNER", "users_namespaces", "user_id = users.id"). - Where("users_namespaces.namespace_id = ?", nu.NamespaceID). - Where("users.username LIKE ?", "%"+search+"%"). - Count(&UserWithRight{}) - - return all, len(all), numberOfTotalItems, err -} - -// Update updates a user <-> namespace relation -// @Summary Update a user <-> namespace relation -// @Description Update a user <-> namespace relation. Mostly used to update the right that user has. -// @tags sharing -// @Accept json -// @Produce json -// @Param namespaceID path int true "Namespace ID" -// @Param userID path int true "User ID" -// @Param namespace body models.NamespaceUser true "The user you want to update." -// @Security JWTKeyAuth -// @Success 200 {object} models.NamespaceUser "The updated user <-> namespace relation." -// @Failure 403 {object} web.HTTPError "The user does not have admin-access to the namespace" -// @Failure 404 {object} web.HTTPError "User or namespace does not exist." -// @Failure 500 {object} models.Message "Internal error" -// @Router /namespaces/{namespaceID}/users/{userID} [post] -func (nu *NamespaceUser) Update(s *xorm.Session, a web.Auth) (err error) { - - // Check if the right is valid - if err := nu.Right.isValid(); err != nil { - return err - } - - // Check if the user exists - user, err := user2.GetUserByUsername(s, nu.Username) - if err != nil { - return err - } - nu.UserID = user.ID - - _, err = s. - Where("namespace_id = ? AND user_id = ?", nu.NamespaceID, nu.UserID). - Cols("right"). - Update(nu) - return -} diff --git a/pkg/models/namespace_users_rights.go b/pkg/models/namespace_users_rights.go deleted file mode 100644 index 00f7e77ca..000000000 --- a/pkg/models/namespace_users_rights.go +++ /dev/null @@ -1,42 +0,0 @@ -// Vikunja is a to-do list application to facilitate your life. -// Copyright 2018-2021 Vikunja and contributors. All rights reserved. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public Licensee as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public Licensee for more details. -// -// You should have received a copy of the GNU Affero General Public Licensee -// along with this program. If not, see . - -package models - -import ( - "code.vikunja.io/web" - "xorm.io/xorm" -) - -// CanCreate checks if the user can create a new user <-> namespace relation -func (nu *NamespaceUser) CanCreate(s *xorm.Session, a web.Auth) (bool, error) { - return nu.canDoNamespaceUser(s, a) -} - -// CanDelete checks if the user can delete a user <-> namespace relation -func (nu *NamespaceUser) CanDelete(s *xorm.Session, a web.Auth) (bool, error) { - return nu.canDoNamespaceUser(s, a) -} - -// CanUpdate checks if the user can update a user <-> namespace relation -func (nu *NamespaceUser) CanUpdate(s *xorm.Session, a web.Auth) (bool, error) { - return nu.canDoNamespaceUser(s, a) -} - -func (nu *NamespaceUser) canDoNamespaceUser(s *xorm.Session, a web.Auth) (bool, error) { - n := &Namespace{ID: nu.NamespaceID} - return n.IsAdmin(s, a) -} diff --git a/pkg/models/namespace_users_rights_test.go b/pkg/models/namespace_users_rights_test.go deleted file mode 100644 index f8a8f7304..000000000 --- a/pkg/models/namespace_users_rights_test.go +++ /dev/null @@ -1,107 +0,0 @@ -// Vikunja is a to-do list application to facilitate your life. -// Copyright 2018-2021 Vikunja and contributors. All rights reserved. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public Licensee as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public Licensee for more details. -// -// You should have received a copy of the GNU Affero General Public Licensee -// along with this program. If not, see . - -package models - -import ( - "testing" - "time" - - "code.vikunja.io/api/pkg/db" - "code.vikunja.io/api/pkg/user" - - "code.vikunja.io/web" -) - -func TestNamespaceUser_CanDoSomething(t *testing.T) { - type fields struct { - ID int64 - UserID int64 - NamespaceID int64 - Right Right - Created time.Time - Updated time.Time - CRUDable web.CRUDable - Rights web.Rights - } - type args struct { - a web.Auth - } - tests := []struct { - name string - fields fields - args args - want map[string]bool - }{ - { - name: "CanDoSomething Normally", - fields: fields{ - NamespaceID: 3, - }, - args: args{ - a: &user.User{ID: 3}, - }, - want: map[string]bool{"CanCreate": true, "CanDelete": true, "CanUpdate": true}, - }, - { - name: "CanDoSomething for a nonexistant namespace", - fields: fields{ - NamespaceID: 300, - }, - args: args{ - a: &user.User{ID: 3}, - }, - want: map[string]bool{"CanCreate": false, "CanDelete": false, "CanUpdate": false}, - }, - { - name: "CanDoSomething where the user does not have the rights", - fields: fields{ - NamespaceID: 3, - }, - args: args{ - a: &user.User{ID: 4}, - }, - want: map[string]bool{"CanCreate": false, "CanDelete": false, "CanUpdate": false}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - db.LoadAndAssertFixtures(t) - s := db.NewSession() - defer s.Close() - - nu := &NamespaceUser{ - ID: tt.fields.ID, - UserID: tt.fields.UserID, - NamespaceID: tt.fields.NamespaceID, - Right: tt.fields.Right, - Created: tt.fields.Created, - Updated: tt.fields.Updated, - CRUDable: tt.fields.CRUDable, - Rights: tt.fields.Rights, - } - if got, _ := nu.CanCreate(s, tt.args.a); got != tt.want["CanCreate"] { - t.Errorf("NamespaceUser.CanCreate() = %v, want %v", got, tt.want["CanCreate"]) - } - if got, _ := nu.CanDelete(s, tt.args.a); got != tt.want["CanDelete"] { - t.Errorf("NamespaceUser.CanDelete() = %v, want %v", got, tt.want["CanDelete"]) - } - if got, _ := nu.CanUpdate(s, tt.args.a); got != tt.want["CanUpdate"] { - t.Errorf("NamespaceUser.CanUpdate() = %v, want %v", got, tt.want["CanUpdate"]) - } - }) - } -} diff --git a/pkg/models/namespace_users_test.go b/pkg/models/namespace_users_test.go deleted file mode 100644 index 634e796b8..000000000 --- a/pkg/models/namespace_users_test.go +++ /dev/null @@ -1,436 +0,0 @@ -// Vikunja is a to-do list application to facilitate your life. -// Copyright 2018-2021 Vikunja and contributors. All rights reserved. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public Licensee as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public Licensee for more details. -// -// You should have received a copy of the GNU Affero General Public Licensee -// along with this program. If not, see . - -package models - -import ( - "reflect" - "runtime" - "testing" - "time" - - "code.vikunja.io/api/pkg/db" - "code.vikunja.io/api/pkg/user" - "code.vikunja.io/web" - "github.com/stretchr/testify/assert" - "gopkg.in/d4l3k/messagediff.v1" -) - -func TestNamespaceUser_Create(t *testing.T) { - type fields struct { - ID int64 - Username string - UserID int64 - NamespaceID int64 - Right Right - Created time.Time - Updated time.Time - CRUDable web.CRUDable - Rights web.Rights - } - type args struct { - a web.Auth - } - tests := []struct { - name string - fields fields - args args - wantErr bool - errType func(err error) bool - }{ - { - name: "NamespaceUsers Create normally", - fields: fields{ - Username: "user1", - UserID: 1, - NamespaceID: 2, - }, - }, - { - name: "NamespaceUsers Create for duplicate", - fields: fields{ - Username: "user1", - NamespaceID: 3, - }, - wantErr: true, - errType: IsErrUserAlreadyHasNamespaceAccess, - }, - { - name: "NamespaceUsers Create with invalid right", - fields: fields{ - Username: "user1", - NamespaceID: 2, - Right: 500, - }, - wantErr: true, - errType: IsErrInvalidRight, - }, - { - name: "NamespaceUsers Create with inexisting project", - fields: fields{ - Username: "user1", - NamespaceID: 2000, - }, - wantErr: true, - errType: IsErrNamespaceDoesNotExist, - }, - { - name: "NamespaceUsers Create with inexisting user", - fields: fields{ - Username: "user500", - NamespaceID: 2, - }, - wantErr: true, - errType: user.IsErrUserDoesNotExist, - }, - { - name: "NamespaceUsers Create with the owner as shared user", - fields: fields{ - Username: "user1", - NamespaceID: 1, - }, - wantErr: true, - errType: IsErrUserAlreadyHasNamespaceAccess, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - db.LoadAndAssertFixtures(t) - s := db.NewSession() - - un := &NamespaceUser{ - ID: tt.fields.ID, - Username: tt.fields.Username, - NamespaceID: tt.fields.NamespaceID, - Right: tt.fields.Right, - Created: tt.fields.Created, - Updated: tt.fields.Updated, - CRUDable: tt.fields.CRUDable, - Rights: tt.fields.Rights, - } - err := un.Create(s, tt.args.a) - if (err != nil) != tt.wantErr { - t.Errorf("NamespaceUser.Create() error = %v, wantErr %v", err, tt.wantErr) - } - if (err != nil) && tt.wantErr && !tt.errType(err) { - t.Errorf("NamespaceUser.Create() Wrong error type! Error = %v, want = %v", err, runtime.FuncForPC(reflect.ValueOf(tt.errType).Pointer()).Name()) - } - err = s.Commit() - assert.NoError(t, err) - - if !tt.wantErr { - db.AssertExists(t, "users_namespaces", map[string]interface{}{ - "user_id": tt.fields.UserID, - "namespace_id": tt.fields.NamespaceID, - }, false) - } - }) - } -} - -func TestNamespaceUser_ReadAll(t *testing.T) { - user1 := &UserWithRight{ - User: user.User{ - ID: 1, - Username: "user1", - Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", - Issuer: "local", - EmailRemindersEnabled: true, - OverdueTasksRemindersEnabled: true, - OverdueTasksRemindersTime: "09:00", - Created: testCreatedTime, - Updated: testUpdatedTime, - }, - Right: RightRead, - } - user2 := &UserWithRight{ - User: user.User{ - ID: 2, - Username: "user2", - Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", - Issuer: "local", - EmailRemindersEnabled: true, - OverdueTasksRemindersEnabled: true, - OverdueTasksRemindersTime: "09:00", - Created: testCreatedTime, - Updated: testUpdatedTime, - }, - Right: RightRead, - } - - type fields struct { - ID int64 - UserID int64 - NamespaceID int64 - Right Right - Created time.Time - Updated time.Time - CRUDable web.CRUDable - Rights web.Rights - } - type args struct { - search string - a web.Auth - page int - } - tests := []struct { - name string - fields fields - args args - want interface{} - wantErr bool - errType func(err error) bool - }{ - { - name: "Test readall normal", - fields: fields{ - NamespaceID: 3, - }, - args: args{ - a: &user.User{ID: 3}, - }, - want: []*UserWithRight{ - user1, - user2, - }, - }, - { - name: "Test ReadAll by a user who does not have access to the project", - fields: fields{ - NamespaceID: 3, - }, - args: args{ - a: &user.User{ID: 4}, - }, - wantErr: true, - errType: IsErrNeedToHaveNamespaceReadAccess, - }, - { - name: "Search", - fields: fields{ - NamespaceID: 3, - }, - args: args{ - a: &user.User{ID: 3}, - search: "usER2", - }, - want: []*UserWithRight{ - user2, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - db.LoadAndAssertFixtures(t) - s := db.NewSession() - defer s.Close() - - un := &NamespaceUser{ - ID: tt.fields.ID, - UserID: tt.fields.UserID, - NamespaceID: tt.fields.NamespaceID, - Right: tt.fields.Right, - Created: tt.fields.Created, - Updated: tt.fields.Updated, - CRUDable: tt.fields.CRUDable, - Rights: tt.fields.Rights, - } - got, _, _, err := un.ReadAll(s, tt.args.a, tt.args.search, tt.args.page, 50) - if (err != nil) != tt.wantErr { - t.Errorf("NamespaceUser.ReadAll() error = %v, wantErr %v", err, tt.wantErr) - return - } - if (err != nil) && tt.wantErr && !tt.errType(err) { - t.Errorf("NamespaceUser.ReadAll() Wrong error type! Error = %v, want = %v", err, runtime.FuncForPC(reflect.ValueOf(tt.errType).Pointer()).Name()) - } - if diff, equal := messagediff.PrettyDiff(got, tt.want); !equal { - t.Errorf("NamespaceUser.ReadAll() = %v, want %v, diff: %v", got, tt.want, diff) - } - }) - } -} - -func TestNamespaceUser_Update(t *testing.T) { - type fields struct { - ID int64 - Username string - UserID int64 - NamespaceID int64 - Right Right - Created time.Time - Updated time.Time - CRUDable web.CRUDable - Rights web.Rights - } - tests := []struct { - name string - fields fields - wantErr bool - errType func(err error) bool - }{ - { - name: "Test Update Normally", - fields: fields{ - NamespaceID: 3, - Username: "user1", - UserID: 1, - Right: RightAdmin, - }, - }, - { - name: "Test Update to write", - fields: fields{ - NamespaceID: 3, - Username: "user1", - UserID: 1, - Right: RightWrite, - }, - }, - { - name: "Test Update to Read", - fields: fields{ - NamespaceID: 3, - Username: "user1", - UserID: 1, - Right: RightRead, - }, - }, - { - name: "Test Update with invalid right", - fields: fields{ - NamespaceID: 3, - Username: "user1", - Right: 500, - }, - wantErr: true, - errType: IsErrInvalidRight, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - db.LoadAndAssertFixtures(t) - s := db.NewSession() - - nu := &NamespaceUser{ - ID: tt.fields.ID, - Username: tt.fields.Username, - NamespaceID: tt.fields.NamespaceID, - Right: tt.fields.Right, - Created: tt.fields.Created, - Updated: tt.fields.Updated, - CRUDable: tt.fields.CRUDable, - Rights: tt.fields.Rights, - } - err := nu.Update(s, &user.User{ID: 1}) - if (err != nil) != tt.wantErr { - t.Errorf("NamespaceUser.Update() error = %v, wantErr %v", err, tt.wantErr) - } - if (err != nil) && tt.wantErr && !tt.errType(err) { - t.Errorf("NamespaceUser.Update() Wrong error type! Error = %v, want = %v", err, runtime.FuncForPC(reflect.ValueOf(tt.errType).Pointer()).Name()) - } - err = s.Commit() - assert.NoError(t, err) - - if !tt.wantErr { - db.AssertExists(t, "users_namespaces", map[string]interface{}{ - "user_id": tt.fields.UserID, - "namespace_id": tt.fields.NamespaceID, - "right": tt.fields.Right, - }, false) - } - }) - } -} - -func TestNamespaceUser_Delete(t *testing.T) { - type fields struct { - ID int64 - Username string - UserID int64 - NamespaceID int64 - Right Right - Created time.Time - Updated time.Time - CRUDable web.CRUDable - Rights web.Rights - } - tests := []struct { - name string - fields fields - wantErr bool - errType func(err error) bool - }{ - { - name: "Try deleting some unexistant user", - fields: fields{ - Username: "user1000", - NamespaceID: 2, - }, - wantErr: true, - errType: user.IsErrUserDoesNotExist, - }, - { - name: "Try deleting a user which does not has access but exists", - fields: fields{ - Username: "user1", - NamespaceID: 4, - }, - wantErr: true, - errType: IsErrUserDoesNotHaveAccessToNamespace, - }, - { - name: "Try deleting normally", - fields: fields{ - Username: "user1", - UserID: 1, - NamespaceID: 3, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - db.LoadAndAssertFixtures(t) - s := db.NewSession() - - nu := &NamespaceUser{ - ID: tt.fields.ID, - Username: tt.fields.Username, - NamespaceID: tt.fields.NamespaceID, - Right: tt.fields.Right, - Created: tt.fields.Created, - Updated: tt.fields.Updated, - CRUDable: tt.fields.CRUDable, - Rights: tt.fields.Rights, - } - err := nu.Delete(s, &user.User{ID: 1}) - if (err != nil) != tt.wantErr { - t.Errorf("NamespaceUser.Delete() error = %v, wantErr %v", err, tt.wantErr) - } - if (err != nil) && tt.wantErr && !tt.errType(err) { - t.Errorf("NamespaceUser.Delete() Wrong error type! Error = %v, want = %v", err, runtime.FuncForPC(reflect.ValueOf(tt.errType).Pointer()).Name()) - } - err = s.Commit() - assert.NoError(t, err) - - if !tt.wantErr { - db.AssertMissing(t, "users_namespaces", map[string]interface{}{ - "user_id": tt.fields.UserID, - "namespace_id": tt.fields.NamespaceID, - }) - } - }) - } -} diff --git a/pkg/models/prject.go b/pkg/models/prject.go index 06d666c54..6ada13fcf 100644 --- a/pkg/models/prject.go +++ b/pkg/models/prject.go @@ -47,13 +47,14 @@ type Project struct { HexColor string `xorm:"varchar(6) null" json:"hex_color" valid:"runelength(0|6)" maxLength:"6"` OwnerID int64 `xorm:"bigint INDEX not null" json:"-"` - NamespaceID int64 `xorm:"bigint INDEX not null" json:"namespace_id" param:"namespace"` ParentProjectID int64 `xorm:"bigint INDEX null" json:"parent_project_id"` + ChildProjects []*Project `xorm:"-" json:"child_projects"` + // The user who created this project. Owner *user.User `xorm:"-" json:"owner" valid:"-"` - // Whether or not a project is archived. + // Whether a project is archived. IsArchived bool `xorm:"not null default false" json:"is_archived" query:"is_archived"` // The id of the file this project has set as background @@ -92,7 +93,7 @@ type ProjectWithTasksAndBuckets struct { } // TableName returns a better name for the projects table -func (l *Project) TableName() string { +func (p *Project) TableName() string { return "projects" } @@ -104,70 +105,42 @@ type ProjectBackgroundType struct { // ProjectBackgroundUpload represents the project upload background type const ProjectBackgroundUpload string = "upload" -// FavoritesPseudoProject holds all tasks marked as favorites -var FavoritesPseudoProject = Project{ +// SharedProjectsPseudoProject is a pseudo project used to hold shared projects +var SharedProjectsPseudoProject = &Project{ ID: -1, - Title: "Favorites", - Description: "This project has all tasks marked as favorites.", - NamespaceID: FavoritesPseudoNamespace.ID, - IsFavorite: true, + Title: "Shared Projects", + Description: "Projects of other users shared with you via teams or directly.", Created: time.Now(), Updated: time.Now(), } -// GetProjectsByNamespaceID gets all projects in a namespace -func GetProjectsByNamespaceID(s *xorm.Session, nID int64, doer *user.User) (projects []*Project, err error) { - switch nID { - case SharedProjectsPseudoNamespace.ID: - nnn, err := getSharedProjectsInNamespace(s, false, doer) - if err != nil { - return nil, err - } - if nnn != nil && nnn.Projects != nil { - projects = nnn.Projects - } - case FavoritesPseudoNamespace.ID: - namespaces := make(map[int64]*NamespaceWithProjects) - _, err := getNamespacesWithProjects(s, &namespaces, "", false, 0, -1, doer.ID) - if err != nil { - return nil, err - } - namespaceIDs, _ := getNamespaceOwnerIDs(namespaces) - ls, err := getProjectsForNamespaces(s, namespaceIDs, false) - if err != nil { - return nil, err - } - nnn, err := getFavoriteProjects(s, ls, namespaceIDs, doer) - if err != nil { - return nil, err - } - if nnn != nil && nnn.Projects != nil { - projects = nnn.Projects - } - case SavedFiltersPseudoNamespace.ID: - nnn, err := getSavedFilters(s, doer) - if err != nil { - return nil, err - } - if nnn != nil && nnn.Projects != nil { - projects = nnn.Projects - } - default: - err = s.Select("l.*"). - Alias("l"). - Join("LEFT", []string{"namespaces", "n"}, "l.namespace_id = n.id"). - Where("l.is_archived = false"). - Where("n.is_archived = false OR n.is_archived IS NULL"). - Where("namespace_id = ?", nID). - Find(&projects) - } - if err != nil { - return nil, err - } +// FavoriteProjectsPseudoProject is a pseudo namespace used to hold favorite projects and tasks +var FavoriteProjectsPseudoProject = &Project{ + ID: -2, + Title: "Favorites", + Description: "Favorite projects and tasks.", + Created: time.Now(), + Updated: time.Now(), +} - // get more project details - err = addProjectDetails(s, projects, doer) - return projects, err +// SavedFiltersPseudoProject is a pseudo namespace used to hold saved filters +var SavedFiltersPseudoProject = &Project{ + ID: -3, + Title: "Filters", + Description: "Saved filters.", + Created: time.Now(), + Updated: time.Now(), +} + +// FavoritesPseudoProject holds all tasks marked as favorites +var FavoritesPseudoProject = Project{ + ID: -1, + Title: "Favorites", + Description: "This project has all tasks marked as favorites.", + ParentProjectID: FavoriteProjectsPseudoProject.ID, + IsFavorite: true, + Created: time.Now(), + Updated: time.Now(), } // ReadAll gets all projects a user has access to @@ -185,7 +158,7 @@ func GetProjectsByNamespaceID(s *xorm.Session, nID int64, doer *user.User) (proj // @Failure 403 {object} web.HTTPError "The user does not have access to the project" // @Failure 500 {object} models.Message "Internal error" // @Router /projects [get] -func (l *Project) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, totalItems int64, err error) { +func (p *Project) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, totalItems int64, err error) { // Check if we're dealing with a share auth shareAuth, ok := a.(*LinkSharing) if ok { @@ -193,26 +166,67 @@ func (l *Project) ReadAll(s *xorm.Session, a web.Auth, search string, page int, if err != nil { return nil, 0, 0, err } - projects := []*Project{project} + projects := map[int64]*Project{} + projects[project.ID] = project err = addProjectDetails(s, projects, a) return projects, 0, 0, err } - projects, resultCount, totalItems, err := getRawProjectsForUser( + doer, err := user.GetFromAuth(a) + if err != nil { + return nil, 0, 0, err + } + + allProjects, resultCount, totalItems, err := getRawProjectsForUser( s, &projectOptions{ - search: search, - user: &user.User{ID: a.GetID()}, - page: page, - perPage: perPage, - isArchived: l.IsArchived, + search: search, + user: doer, + page: page, + perPage: perPage, + getArchived: p.IsArchived, }) if err != nil { return nil, 0, 0, err } - // Add more project details - err = addProjectDetails(s, projects, a) + ///////////////// + // Saved Filters + + savedFiltersProject, err := getSavedFilterProjects(s, doer) + if err != nil { + return nil, 0, 0, err + } + + if savedFiltersProject != nil { + allProjects[savedFiltersProject.ID] = savedFiltersProject + } + + ///////////////// + // Add project details (favorite state, among other things) + err = addProjectDetails(s, allProjects, a) + if err != nil { + return + } + + ////////////////////////// + // Putting it all together + + var projects []*Project + for _, p := range allProjects { + if p.ParentProjectID != 0 { + if allProjects[p.ParentProjectID].ChildProjects == nil { + allProjects[p.ParentProjectID].ChildProjects = []*Project{} + } + allProjects[p.ParentProjectID].ChildProjects = append(allProjects[p.ParentProjectID].ChildProjects, p) + continue + } + + // The projects variable will contain all projects which have no parents + // And because we're using the same pointers for everything, those will contain child projects + projects = append(projects, p) + } + return projects, resultCount, totalItems, err } @@ -228,61 +242,61 @@ func (l *Project) ReadAll(s *xorm.Session, a web.Auth, search string, page int, // @Failure 403 {object} web.HTTPError "The user does not have access to the project" // @Failure 500 {object} models.Message "Internal error" // @Router /projects/{id} [get] -func (l *Project) ReadOne(s *xorm.Session, a web.Auth) (err error) { +func (p *Project) ReadOne(s *xorm.Session, a web.Auth) (err error) { - if l.ID == FavoritesPseudoProject.ID { + if p.ID == FavoritesPseudoProject.ID { // Already "built" the project in CanRead return nil } // Check for saved filters - if getSavedFilterIDFromProjectID(l.ID) > 0 { - sf, err := getSavedFilterSimpleByID(s, getSavedFilterIDFromProjectID(l.ID)) + if getSavedFilterIDFromProjectID(p.ID) > 0 { + sf, err := getSavedFilterSimpleByID(s, getSavedFilterIDFromProjectID(p.ID)) if err != nil { return err } - l.Title = sf.Title - l.Description = sf.Description - l.Created = sf.Created - l.Updated = sf.Updated - l.OwnerID = sf.OwnerID + p.Title = sf.Title + p.Description = sf.Description + p.Created = sf.Created + p.Updated = sf.Updated + p.OwnerID = sf.OwnerID } // Get project owner - l.Owner, err = user.GetUserByID(s, l.OwnerID) + p.Owner, err = user.GetUserByID(s, p.OwnerID) if err != nil { return err } // Check if the namespace is archived and set the namespace to archived if it is not already archived individually. - if !l.IsArchived { - err = l.CheckIsArchived(s) + if !p.IsArchived { + err = p.CheckIsArchived(s) if err != nil { if !IsErrNamespaceIsArchived(err) && !IsErrProjectIsArchived(err) { return } - l.IsArchived = true + p.IsArchived = true } } // Get any background information if there is one set - if l.BackgroundFileID != 0 { + if p.BackgroundFileID != 0 { // Unsplash image - l.BackgroundInformation, err = GetUnsplashPhotoByFileID(s, l.BackgroundFileID) + p.BackgroundInformation, err = GetUnsplashPhotoByFileID(s, p.BackgroundFileID) if err != nil && !files.IsErrFileIsNotUnsplashFile(err) { return } if err != nil && files.IsErrFileIsNotUnsplashFile(err) { - l.BackgroundInformation = &ProjectBackgroundType{Type: ProjectBackgroundUpload} + p.BackgroundInformation = &ProjectBackgroundType{Type: ProjectBackgroundUpload} } } - l.IsFavorite, err = isFavorite(s, l.ID, a, FavoriteKindProject) + p.IsFavorite, err = isFavorite(s, p.ID, a, FavoriteKindProject) if err != nil { return } - l.Subscription, err = GetSubscription(s, SubscriptionEntityProject, l.ID, a) + p.Subscription, err = GetSubscription(s, SubscriptionEntityProject, p.ID, a) return } @@ -345,62 +359,32 @@ func GetProjectsByIDs(s *xorm.Session, projectIDs []int64) (projects map[int64]* } type projectOptions struct { - search string - user *user.User - page int - perPage int - isArchived bool + search string + user *user.User + page int + perPage int + getArchived bool } -func getUserProjectsStatement(userID int64) *builder.Builder { +func getUserProjectsStatement(userID int64, search string, getArchived bool) *builder.Builder { dialect := config.DatabaseType.GetString() if dialect == "sqlite" { dialect = builder.SQLITE } - return builder.Dialect(dialect). - Select("l.*"). - From("projects", "l"). - Join("INNER", "namespaces n", "l.namespace_id = n.id"). - Join("LEFT", "team_namespaces tn", "tn.namespace_id = n.id"). - Join("LEFT", "team_members tm", "tm.team_id = tn.team_id"). - Join("LEFT", "team_projects tl", "l.id = tl.project_id"). - Join("LEFT", "team_members tm2", "tm2.team_id = tl.team_id"). - Join("LEFT", "users_projects ul", "ul.project_id = l.id"). - Join("LEFT", "users_namespaces un", "un.namespace_id = l.namespace_id"). - Where(builder.Or( - builder.Eq{"tm.user_id": userID}, - builder.Eq{"tm2.user_id": userID}, - builder.Eq{"ul.user_id": userID}, - builder.Eq{"un.user_id": userID}, - builder.Eq{"l.owner_id": userID}, - )). - OrderBy("position"). - GroupBy("l.id") -} - -// Gets the projects only, without any tasks or so -func getRawProjectsForUser(s *xorm.Session, opts *projectOptions) (projects []*Project, resultCount int, totalItems int64, err error) { - fullUser, err := user.GetUserByID(s, opts.user.ID) - if err != nil { - return nil, 0, 0, err - } - // Adding a 1=1 condition by default here because xorm always needs a condition and cannot handle nil conditions - var isArchivedCond builder.Cond = builder.Eq{"1": 1} - if !opts.isArchived { - isArchivedCond = builder.And( + var getArchivedCond builder.Cond = builder.Eq{"1": 1} + if !getArchived { + getArchivedCond = builder.And( builder.Eq{"l.is_archived": false}, builder.Eq{"n.is_archived": false}, ) } - limit, start := getLimitFromPageIndex(opts.page, opts.perPage) - var filterCond builder.Cond ids := []int64{} - if opts.search != "" { - vals := strings.Split(opts.search, ",") + if search != "" { + vals := strings.Split(search, ",") for _, val := range vals { v, err := strconv.ParseInt(val, 10, 64) if err != nil { @@ -411,65 +395,114 @@ func getRawProjectsForUser(s *xorm.Session, opts *projectOptions) (projects []*P } } - filterCond = db.ILIKE("l.title", opts.search) + filterCond = db.ILIKE("l.title", search) if len(ids) > 0 { filterCond = builder.In("l.id", ids) } - // Gets all Projects where the user is either owner or in a team which has access to the project - // Or in a team which has namespace read access + return builder.Dialect(dialect). + Select("l.*"). + From("projects", "l"). + // TODO: remove namespaces + Join("INNER", "namespaces n", "l.namespace_id = n.id"). + Join("LEFT", "team_namespaces tn", "tn.namespace_id = n.id"). + Join("LEFT", "team_members tm", "tm.team_id = tn.team_id"). + Join("LEFT", "team_projects tl", "l.id = tl.project_id"). + Join("LEFT", "team_members tm2", "tm2.team_id = tl.team_id"). + Join("LEFT", "users_projects ul", "ul.project_id = l.id"). + Join("LEFT", "users_namespaces un", "un.namespace_id = l.namespace_id"). + Where(builder.And( + builder.Or( + builder.Eq{"tm.user_id": userID}, + builder.Eq{"tm2.user_id": userID}, + builder.Eq{"ul.user_id": userID}, + builder.Eq{"un.user_id": userID}, + builder.Eq{"l.owner_id": userID}, + ), + filterCond, + getArchivedCond, + )). + OrderBy("position"). + GroupBy("l.id") +} - query := getUserProjectsStatement(fullUser.ID). - Where(filterCond). - Where(isArchivedCond) - if limit > 0 { - query = query.Limit(limit, start) - } - err = s.SQL(query).Find(&projects) +// Gets the projects with their children without any tasks +func getRawProjectsForUser(s *xorm.Session, opts *projectOptions) (projects map[int64]*Project, resultCount int, totalItems int64, err error) { + fullUser, err := user.GetUserByID(s, opts.user.ID) if err != nil { return nil, 0, 0, err } - query = getUserProjectsStatement(fullUser.ID). - Where(filterCond). - Where(isArchivedCond) + limit, start := getLimitFromPageIndex(opts.page, opts.perPage) + + // Gets all projects where the user is either owner or it was shared to them + + allProjects := make(map[int64]*Project) + + query := getUserProjectsStatement(fullUser.ID, opts.search, opts.getArchived) + if limit > 0 { + query = query.Limit(limit, start) + } + err = s.SQL(query).Find(&allProjects) + if err != nil { + return nil, 0, 0, err + } + + query = getUserProjectsStatement(fullUser.ID, opts.search, opts.getArchived) totalItems, err = s. SQL(query.Select("count(*)")). Count(&Project{}) - return projects, len(projects), totalItems, err + + if len(allProjects) == 0 { + return nil, 0, totalItems, nil + } + + return projects, len(allProjects), totalItems, err +} + +func getSavedFilterProjects(s *xorm.Session, doer *user.User) (savedFiltersNamespace *Project, err error) { + savedFilters, err := getSavedFiltersForUser(s, doer) + if err != nil { + return + } + + if len(savedFilters) == 0 { + return nil, nil + } + + savedFiltersPseudoNamespace := SavedFiltersPseudoProject + savedFiltersPseudoNamespace.OwnerID = doer.ID + *savedFiltersNamespace = *savedFiltersPseudoNamespace + savedFiltersNamespace.ChildProjects = make([]*Project, 0, len(savedFilters)) + + for _, filter := range savedFilters { + filterProject := filter.toProject() + filterProject.ParentProjectID = savedFiltersNamespace.ID + filterProject.Owner = doer + savedFiltersNamespace.ChildProjects = append(savedFiltersNamespace.ChildProjects, filterProject) + } + + return } // addProjectDetails adds owner user objects and project tasks to all projects in the slice -func addProjectDetails(s *xorm.Session, projects []*Project, a web.Auth) (err error) { +func addProjectDetails(s *xorm.Session, projects map[int64]*Project, a web.Auth) (err error) { if len(projects) == 0 { return } var ownerIDs []int64 - for _, l := range projects { - ownerIDs = append(ownerIDs, l.OwnerID) - } - - // Get all project owners - owners := map[int64]*user.User{} - if len(ownerIDs) > 0 { - err = s.In("id", ownerIDs).Find(&owners) - if err != nil { - return - } - } - - var fileIDs []int64 var projectIDs []int64 - for _, l := range projects { - projectIDs = append(projectIDs, l.ID) - if o, exists := owners[l.OwnerID]; exists { - l.Owner = o - } - if l.BackgroundFileID != 0 { - l.BackgroundInformation = &ProjectBackgroundType{Type: ProjectBackgroundUpload} - } - fileIDs = append(fileIDs, l.BackgroundFileID) + var fileIDs []int64 + for _, p := range projects { + ownerIDs = append(ownerIDs, p.OwnerID) + projectIDs = append(projectIDs, p.ID) + fileIDs = append(fileIDs, p.BackgroundFileID) + } + + owners, err := user.GetUsersByIDs(s, ownerIDs) + if err != nil { + return err } favs, err := getFavorites(s, projectIDs, a, FavoriteKindProject) @@ -479,19 +512,26 @@ func addProjectDetails(s *xorm.Session, projects []*Project, a web.Auth) (err er subscriptions, err := GetSubscriptions(s, SubscriptionEntityProject, projectIDs, a) if err != nil { - log.Errorf("An error occurred while getting project subscriptions for a namespace item: %s", err.Error()) + log.Errorf("An error occurred while getting project subscriptions for a project: %s", err.Error()) subscriptions = make(map[int64]*Subscription) } - for _, project := range projects { + for _, p := range projects { + if o, exists := owners[p.OwnerID]; exists { + p.Owner = o + } + if p.BackgroundFileID != 0 { + p.BackgroundInformation = &ProjectBackgroundType{Type: ProjectBackgroundUpload} + } + // Don't override the favorite state if it was already set from before (favorite saved filters do this) - if project.IsFavorite { + if p.IsFavorite { continue } - project.IsFavorite = favs[project.ID] + p.IsFavorite = favs[p.ID] - if subscription, exists := subscriptions[project.ID]; exists { - project.Subscription = subscription + if subscription, exists := subscriptions[p.ID]; exists { + p.Subscription = subscription } } @@ -521,46 +561,36 @@ func addProjectDetails(s *xorm.Session, projects []*Project, a web.Auth) (err er return } -// NamespaceProject is a meta type to be able to join a project with its namespace -type NamespaceProject struct { - Project Project `xorm:"extends"` - Namespace Namespace `xorm:"extends"` -} - -// CheckIsArchived returns an ErrProjectIsArchived or ErrNamespaceIsArchived if the project or its namespace is archived. -func (l *Project) CheckIsArchived(s *xorm.Session) (err error) { - // When creating a new project, we check if the namespace is archived - if l.ID == 0 { - n := &Namespace{ID: l.NamespaceID} - return n.CheckIsArchived(s) +// CheckIsArchived returns an ErrProjectIsArchived or ErrNamespaceIsArchived if the project or any of its parent projects is archived. +func (p *Project) CheckIsArchived(s *xorm.Session) (err error) { + // When creating a new project, we check if the parent is archived + if p.ID == 0 { + p := &Project{ID: p.ParentProjectID} + return p.CheckIsArchived(s) } - nl := &NamespaceProject{} - exists, err := s. - Table("projects"). - Join("LEFT", "namespaces", "projects.namespace_id = namespaces.id"). - Where("projects.id = ? AND (projects.is_archived = true OR namespaces.is_archived = true)", l.ID). - Get(nl) + p, err := GetProjectSimpleByID(s, p.ID) if err != nil { - return + return err } - if exists && nl.Project.ID != 0 && nl.Project.IsArchived { - return ErrProjectIsArchived{ProjectID: l.ID} - } - if exists && nl.Namespace.ID != 0 && nl.Namespace.IsArchived { - return ErrNamespaceIsArchived{NamespaceID: nl.Namespace.ID} + + // TODO: parent project + + if p.IsArchived { + return ErrProjectIsArchived{ProjectID: p.ID} } + return nil } func checkProjectBeforeUpdateOrDelete(s *xorm.Session, project *Project) error { - if project.NamespaceID < 0 { - return &ErrProjectCannotBelongToAPseudoNamespace{ProjectID: project.ID, NamespaceID: project.NamespaceID} + if project.ParentProjectID < 0 { + return &ErrProjectCannotBelongToAPseudoNamespace{ProjectID: project.ID, NamespaceID: project.ParentProjectID} } - // Check if the namespace exists - if project.NamespaceID > 0 { - _, err := GetNamespaceByID(s, project.NamespaceID) + // Check if the parent project exists + if project.ParentProjectID > 0 { + _, err := GetProjectSimpleByID(s, project.ParentProjectID) if err != nil { return err } @@ -635,19 +665,21 @@ func CreateProject(s *xorm.Session, project *Project, auth web.Auth) (err error) }) } +// CreateNewProjectForUser creates a new inbox project for a user. To prevent import cycles, we can't do that +// directly in the user.Create function. +func CreateNewProjectForUser(s *xorm.Session, user *user.User) (err error) { + p := &Project{ + Title: "Inbox", + } + return p.Create(s, user) +} + func UpdateProject(s *xorm.Session, project *Project, auth web.Auth, updateProjectBackground bool) (err error) { err = checkProjectBeforeUpdateOrDelete(s, project) if err != nil { return } - if project.NamespaceID == 0 { - return &ErrProjectMustBelongToANamespace{ - ProjectID: project.ID, - NamespaceID: project.NamespaceID, - } - } - // We need to specify the cols we want to update here to be able to un-archive projects colsToUpdate := []string{ "title", @@ -721,27 +753,27 @@ func UpdateProject(s *xorm.Session, project *Project, auth web.Auth, updateProje // @Failure 403 {object} web.HTTPError "The user does not have access to the project" // @Failure 500 {object} models.Message "Internal error" // @Router /projects/{id} [post] -func (l *Project) Update(s *xorm.Session, a web.Auth) (err error) { - fid := getSavedFilterIDFromProjectID(l.ID) +func (p *Project) Update(s *xorm.Session, a web.Auth) (err error) { + fid := getSavedFilterIDFromProjectID(p.ID) if fid > 0 { f, err := getSavedFilterSimpleByID(s, fid) if err != nil { return err } - f.Title = l.Title - f.Description = l.Description - f.IsFavorite = l.IsFavorite + f.Title = p.Title + f.Description = p.Description + f.IsFavorite = p.IsFavorite err = f.Update(s, a) if err != nil { return err } - *l = *f.toProject() + *p = *f.toProject() return nil } - return UpdateProject(s, l, a, false) + return UpdateProject(s, p, a, false) } func updateProjectLastUpdated(s *xorm.Session, project *Project) error { @@ -773,13 +805,13 @@ func updateProjectByTaskID(s *xorm.Session, taskID int64) (err error) { // @Failure 403 {object} web.HTTPError "The user does not have access to the project" // @Failure 500 {object} models.Message "Internal error" // @Router /namespaces/{namespaceID}/projects [put] -func (l *Project) Create(s *xorm.Session, a web.Auth) (err error) { - err = CreateProject(s, l, a) +func (p *Project) Create(s *xorm.Session, a web.Auth) (err error) { + err = CreateProject(s, p, a) if err != nil { return } - return l.ReadOne(s, a) + return p.ReadOne(s, a) } // Delete implements the delete method of CRUDable @@ -794,17 +826,17 @@ func (l *Project) Create(s *xorm.Session, a web.Auth) (err error) { // @Failure 403 {object} web.HTTPError "The user does not have access to the project" // @Failure 500 {object} models.Message "Internal error" // @Router /projects/{id} [delete] -func (l *Project) Delete(s *xorm.Session, a web.Auth) (err error) { +func (p *Project) Delete(s *xorm.Session, a web.Auth) (err error) { // Delete the project - _, err = s.ID(l.ID).Delete(&Project{}) + _, err = s.ID(p.ID).Delete(&Project{}) if err != nil { return } // Delete all tasks on that project // Using the loop to make sure all related entities to all tasks are properly deleted as well. - tasks, _, _, err := getRawTasksForProjects(s, []*Project{l}, a, &taskOptions{}) + tasks, _, _, err := getRawTasksForProjects(s, []*Project{p}, a, &taskOptions{}) if err != nil { return } @@ -817,7 +849,7 @@ func (l *Project) Delete(s *xorm.Session, a web.Auth) (err error) { } return events.Dispatch(&ProjectDeletedEvent{ - Project: l, + Project: p, Doer: a, }) } diff --git a/pkg/models/project_rights.go b/pkg/models/project_rights.go index da8285fb2..cf41ce780 100644 --- a/pkg/models/project_rights.go +++ b/pkg/models/project_rights.go @@ -24,15 +24,15 @@ import ( ) // CanWrite return whether the user can write on that project or not -func (l *Project) CanWrite(s *xorm.Session, a web.Auth) (bool, error) { +func (p *Project) CanWrite(s *xorm.Session, a web.Auth) (bool, error) { // The favorite project can't be edited - if l.ID == FavoritesPseudoProject.ID { + if p.ID == FavoritesPseudoProject.ID { return false, nil } // Get the project and check the right - originalProject, err := GetProjectSimpleByID(s, l.ID) + originalProject, err := GetProjectSimpleByID(s, p.ID) if err != nil { return false, err } @@ -67,66 +67,66 @@ func (l *Project) CanWrite(s *xorm.Session, a web.Auth) (bool, error) { } // CanRead checks if a user has read access to a project -func (l *Project) CanRead(s *xorm.Session, a web.Auth) (bool, int, error) { +func (p *Project) CanRead(s *xorm.Session, a web.Auth) (bool, int, error) { // The favorite project needs a special treatment - if l.ID == FavoritesPseudoProject.ID { + if p.ID == FavoritesPseudoProject.ID { owner, err := user.GetFromAuth(a) if err != nil { return false, 0, err } - *l = FavoritesPseudoProject - l.Owner = owner + *p = FavoritesPseudoProject + p.Owner = owner return true, int(RightRead), nil } // Saved Filter Projects need a special case - if getSavedFilterIDFromProjectID(l.ID) > 0 { - sf := &SavedFilter{ID: getSavedFilterIDFromProjectID(l.ID)} + if getSavedFilterIDFromProjectID(p.ID) > 0 { + sf := &SavedFilter{ID: getSavedFilterIDFromProjectID(p.ID)} return sf.CanRead(s, a) } // Check if the user is either owner or can read var err error - originalProject, err := GetProjectSimpleByID(s, l.ID) + originalProject, err := GetProjectSimpleByID(s, p.ID) if err != nil { return false, 0, err } - *l = *originalProject + *p = *originalProject // Check if we're dealing with a share auth shareAuth, ok := a.(*LinkSharing) if ok { - return l.ID == shareAuth.ProjectID && + return p.ID == shareAuth.ProjectID && (shareAuth.Right == RightRead || shareAuth.Right == RightWrite || shareAuth.Right == RightAdmin), int(shareAuth.Right), nil } - if l.isOwner(&user.User{ID: a.GetID()}) { + if p.isOwner(&user.User{ID: a.GetID()}) { return true, int(RightAdmin), nil } - return l.checkRight(s, a, RightRead, RightWrite, RightAdmin) + return p.checkRight(s, a, RightRead, RightWrite, RightAdmin) } // CanUpdate checks if the user can update a project -func (l *Project) CanUpdate(s *xorm.Session, a web.Auth) (canUpdate bool, err error) { +func (p *Project) CanUpdate(s *xorm.Session, a web.Auth) (canUpdate bool, err error) { // The favorite project can't be edited - if l.ID == FavoritesPseudoProject.ID { + if p.ID == FavoritesPseudoProject.ID { return false, nil } // Get the project - ol, err := GetProjectSimpleByID(s, l.ID) + ol, err := GetProjectSimpleByID(s, p.ID) if err != nil { return false, err } - // Check if we're moving the project into a different namespace. + // Check if we're moving the project to a different parent project. // If that is the case, we need to verify permissions to do so. - if l.NamespaceID != 0 && l.NamespaceID != ol.NamespaceID { - newNamespace := &Namespace{ID: l.NamespaceID} - can, err := newNamespace.CanWrite(s, a) + if p.ParentProjectID != 0 && p.ParentProjectID != ol.ParentProjectID { + newProject := &Project{ID: p.ParentProjectID} + can, err := newProject.CanWrite(s, a) if err != nil { return false, err } @@ -135,7 +135,7 @@ func (l *Project) CanUpdate(s *xorm.Session, a web.Auth) (canUpdate bool, err er } } - fid := getSavedFilterIDFromProjectID(l.ID) + fid := getSavedFilterIDFromProjectID(p.ID) if fid > 0 { sf, err := getSavedFilterSimpleByID(s, fid) if err != nil { @@ -145,34 +145,40 @@ func (l *Project) CanUpdate(s *xorm.Session, a web.Auth) (canUpdate bool, err er return sf.CanUpdate(s, a) } - canUpdate, err = l.CanWrite(s, a) + canUpdate, err = p.CanWrite(s, a) // If the project is archived and the user tries to un-archive it, let the request through - if IsErrProjectIsArchived(err) && !l.IsArchived { + if IsErrProjectIsArchived(err) && !p.IsArchived { err = nil } return canUpdate, err } // CanDelete checks if the user can delete a project -func (l *Project) CanDelete(s *xorm.Session, a web.Auth) (bool, error) { - return l.IsAdmin(s, a) +func (p *Project) CanDelete(s *xorm.Session, a web.Auth) (bool, error) { + return p.IsAdmin(s, a) } // CanCreate checks if the user can create a project -func (l *Project) CanCreate(s *xorm.Session, a web.Auth) (bool, error) { - // A user can create a project if they have write access to the namespace - n := &Namespace{ID: l.NamespaceID} - return n.CanWrite(s, a) +func (p *Project) CanCreate(s *xorm.Session, a web.Auth) (bool, error) { + // If the new namespace has a parent, check that + if p.ParentProjectID != 0 { + // TODO: check the parent's parent (and so on) + parent := &Project{ID: p.ParentProjectID} + return parent.CanCreate(s, a) + } + + // Otherwise just allow + return true, nil } // IsAdmin returns whether the user has admin rights on the project or not -func (l *Project) IsAdmin(s *xorm.Session, a web.Auth) (bool, error) { +func (p *Project) IsAdmin(s *xorm.Session, a web.Auth) (bool, error) { // The favorite project can't be edited - if l.ID == FavoritesPseudoProject.ID { + if p.ID == FavoritesPseudoProject.ID { return false, nil } - originalProject, err := GetProjectSimpleByID(s, l.ID) + originalProject, err := GetProjectSimpleByID(s, p.ID) if err != nil { return false, err } @@ -194,12 +200,12 @@ func (l *Project) IsAdmin(s *xorm.Session, a web.Auth) (bool, error) { } // Little helper function to check if a user is project owner -func (l *Project) isOwner(u *user.User) bool { - return l.OwnerID == u.ID +func (p *Project) isOwner(u *user.User) bool { + return p.OwnerID == u.ID } // Checks n different rights for any given user -func (l *Project) checkRight(s *xorm.Session, a web.Auth, rights ...Right) (bool, int, error) { +func (p *Project) checkRight(s *xorm.Session, a web.Auth, rights ...Right) (bool, int, error) { /* The following loop creates a sql condition like this one: @@ -242,53 +248,44 @@ func (l *Project) checkRight(s *xorm.Session, a web.Auth, rights ...Right) (bool conds = append(conds, builder.Eq{"n.owner_id": a.GetID()}) type allProjectRights struct { - UserNamespace *NamespaceUser `xorm:"extends"` - UserProject *ProjectUser `xorm:"extends"` + UserProject *ProjectUser `xorm:"extends"` + TeamProject *TeamProject `xorm:"extends"` - TeamNamespace *TeamNamespace `xorm:"extends"` - TeamProject *TeamProject `xorm:"extends"` - - NamespaceOwnerID int64 `xorm:"namespaces_owner_id"` + OwnerID int64 `xorm:"namespaces_owner_id"` } r := &allProjectRights{} var maxRight = 0 exists, err := s. - Select("l.*, un.right, ul.right, tn.right, tl.right, n.owner_id as namespaces_owner_id"). + Select("p.*, un.right, ul.right, tn.right, tl.right, n.owner_id as namespaces_owner_id"). Table("projects"). - Alias("l"). + Alias("p"). // User stuff - Join("LEFT", []string{"users_namespaces", "un"}, "un.namespace_id = l.namespace_id"). - Join("LEFT", []string{"users_projects", "ul"}, "ul.project_id = l.id"). - Join("LEFT", []string{"namespaces", "n"}, "n.id = l.namespace_id"). + Join("LEFT", []string{"users_namespaces", "un"}, "un.namespace_id = p.namespace_id"). + Join("LEFT", []string{"users_projects", "ul"}, "ul.project_id = p.id"). + Join("LEFT", []string{"namespaces", "n"}, "n.id = p.namespace_id"). // Team stuff - Join("LEFT", []string{"team_namespaces", "tn"}, " l.namespace_id = tn.namespace_id"). + Join("LEFT", []string{"team_namespaces", "tn"}, " p.namespace_id = tn.namespace_id"). Join("LEFT", []string{"team_members", "tm"}, "tm.team_id = tn.team_id"). - Join("LEFT", []string{"team_projects", "tl"}, "l.id = tl.project_id"). + Join("LEFT", []string{"team_projects", "tl"}, "p.id = tl.project_id"). Join("LEFT", []string{"team_members", "tm2"}, "tm2.team_id = tl.team_id"). // The actual condition Where(builder.And( builder.Or( conds..., ), - builder.Eq{"l.id": l.ID}, + builder.Eq{"p.id": p.ID}, )). Get(r) // Figure out the max right and return it - if int(r.UserNamespace.Right) > maxRight { - maxRight = int(r.UserNamespace.Right) - } if int(r.UserProject.Right) > maxRight { maxRight = int(r.UserProject.Right) } - if int(r.TeamNamespace.Right) > maxRight { - maxRight = int(r.TeamNamespace.Right) - } if int(r.TeamProject.Right) > maxRight { maxRight = int(r.TeamProject.Right) } - if r.NamespaceOwnerID == a.GetID() { + if r.OwnerID == a.GetID() { maxRight = int(RightAdmin) } diff --git a/pkg/models/saved_filters.go b/pkg/models/saved_filters.go index 9c12f62f0..91db28214 100644 --- a/pkg/models/saved_filters.go +++ b/pkg/models/saved_filters.go @@ -102,7 +102,7 @@ func (sf *SavedFilter) toProject() *Project { Created: sf.Created, Updated: sf.Updated, Owner: sf.Owner, - NamespaceID: SavedFiltersPseudoNamespace.ID, + NamespaceID: SavedFiltersPseudoProject.ID, } } diff --git a/pkg/models/user_delete.go b/pkg/models/user_delete.go index 55858c149..d0009042c 100644 --- a/pkg/models/user_delete.go +++ b/pkg/models/user_delete.go @@ -87,45 +87,6 @@ func deleteUsers() { } } -func getNamespacesToDelete(s *xorm.Session, u *user.User) (namespacesToDelete []*Namespace, err error) { - namespacesToDelete = []*Namespace{} - nm := &Namespace{IsArchived: true} - res, _, _, err := nm.ReadAll(s, u, "", 1, -1) - if err != nil { - return nil, err - } - - if res == nil { - return nil, nil - } - - namespaces := res.([]*NamespaceWithProjects) - for _, n := range namespaces { - if n.ID < 0 { - continue - } - - hadUsers, err := ensureNamespaceAdminUser(s, &n.Namespace) - if err != nil { - return nil, err - } - if hadUsers { - continue - } - hadTeams, err := ensureNamespaceAdminTeam(s, &n.Namespace) - if err != nil { - return nil, err - } - if hadTeams { - continue - } - - namespacesToDelete = append(namespacesToDelete, &n.Namespace) - } - - return -} - func getProjectsToDelete(s *xorm.Session, u *user.User) (projectsToDelete []*Project, err error) { projectsToDelete = []*Project{} lm := &Project{IsArchived: true} @@ -170,24 +131,11 @@ func getProjectsToDelete(s *xorm.Session, u *user.User) (projectsToDelete []*Pro // This action is irrevocable. // Public to allow deletion from the CLI. func DeleteUser(s *xorm.Session, u *user.User) (err error) { - namespacesToDelete, err := getNamespacesToDelete(s, u) - if err != nil { - return err - } - projectsToDelete, err := getProjectsToDelete(s, u) if err != nil { return err } - // Delete everything not shared with anybody else - for _, n := range namespacesToDelete { - err = deleteNamespace(s, n, u, false) - if err != nil { - return err - } - } - for _, l := range projectsToDelete { err = l.Delete(s, u) if err != nil { @@ -205,58 +153,6 @@ func DeleteUser(s *xorm.Session, u *user.User) (err error) { }) } -func ensureNamespaceAdminUser(s *xorm.Session, n *Namespace) (hadUsers bool, err error) { - namespaceUsers := []*NamespaceUser{} - err = s.Where("namespace_id = ?", n.ID).Find(&namespaceUsers) - if err != nil { - return - } - - if len(namespaceUsers) == 0 { - return false, nil - } - - for _, lu := range namespaceUsers { - if lu.Right == RightAdmin { - // Project already has more than one admin, no need to do anything - return true, nil - } - } - - firstUser := namespaceUsers[0] - firstUser.Right = RightAdmin - _, err = s.Where("id = ?", firstUser.ID). - Cols("right"). - Update(firstUser) - return true, err -} - -func ensureNamespaceAdminTeam(s *xorm.Session, n *Namespace) (hadTeams bool, err error) { - namespaceTeams := []*TeamNamespace{} - err = s.Where("namespace_id = ?", n.ID).Find(&namespaceTeams) - if err != nil { - return - } - - if len(namespaceTeams) == 0 { - return false, nil - } - - for _, lu := range namespaceTeams { - if lu.Right == RightAdmin { - // Project already has more than one admin, no need to do anything - return true, nil - } - } - - firstTeam := namespaceTeams[0] - firstTeam.Right = RightAdmin - _, err = s.Where("id = ?", firstTeam.ID). - Cols("right"). - Update(firstTeam) - return true, err -} - func ensureProjectAdminUser(s *xorm.Session, l *Project) (hadUsers bool, err error) { projectUsers := []*ProjectUser{} err = s.Where("project_id = ?", l.ID).Find(&projectUsers) diff --git a/pkg/modules/auth/openid/openid.go b/pkg/modules/auth/openid/openid.go index e8202195e..aa80890ce 100644 --- a/pkg/modules/auth/openid/openid.go +++ b/pkg/modules/auth/openid/openid.go @@ -245,7 +245,7 @@ func getOrCreateUser(s *xorm.Session, cl *claims, issuer, subject string) (u *us } // And create its namespace - err = models.CreateNewNamespaceForUser(s, u) + err = models.CreateNewProjectForUser(s, u) if err != nil { return nil, err } diff --git a/pkg/routes/api/v1/project_by_namespace.go b/pkg/routes/api/v1/project_by_namespace.go deleted file mode 100644 index 0e947e892..000000000 --- a/pkg/routes/api/v1/project_by_namespace.go +++ /dev/null @@ -1,97 +0,0 @@ -// Vikunja is a to-do list application to facilitate your life. -// Copyright 2018-2021 Vikunja and contributors. All rights reserved. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public Licensee as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public Licensee for more details. -// -// You should have received a copy of the GNU Affero General Public Licensee -// along with this program. If not, see . - -package v1 - -import ( - "net/http" - "strconv" - - "code.vikunja.io/api/pkg/db" - "xorm.io/xorm" - - "code.vikunja.io/api/pkg/models" - "code.vikunja.io/api/pkg/user" - "code.vikunja.io/web/handler" - "github.com/labstack/echo/v4" -) - -// GetProjectsByNamespaceID is the web handler to get all projects belonging to a namespace -// TODO: deprecate this in favour of namespace.ReadOne() <-- should also return the projects -// @Summary Get all projects in a namespace -// @Description Returns all projects inside of a namespace. -// @tags namespace -// @Accept json -// @Produce json -// @Param id path int true "Namespace ID" -// @Security JWTKeyAuth -// @Success 200 {array} models.Project "The projects." -// @Failure 403 {object} models.Message "No access to that namespace." -// @Failure 404 {object} models.Message "The namespace does not exist." -// @Failure 500 {object} models.Message "Internal error" -// @Router /namespaces/{id}/projects [get] -func GetProjectsByNamespaceID(c echo.Context) error { - s := db.NewSession() - defer s.Close() - - // Get our namespace - namespace, err := getNamespace(s, c) - if err != nil { - return handler.HandleHTTPError(err, c) - } - - // Get the projects - doer, err := user.GetCurrentUser(c) - if err != nil { - return handler.HandleHTTPError(err, c) - } - - projects, err := models.GetProjectsByNamespaceID(s, namespace.ID, doer) - if err != nil { - return handler.HandleHTTPError(err, c) - } - return c.JSON(http.StatusOK, projects) -} - -func getNamespace(s *xorm.Session, c echo.Context) (namespace *models.Namespace, err error) { - // Check if we have our ID - id := c.Param("namespace") - // Make int - namespaceID, err := strconv.ParseInt(id, 10, 64) - if err != nil { - return - } - - if namespaceID == -1 { - namespace = &models.SharedProjectsPseudoNamespace - return - } - - // Check if the user has acces to that namespace - u, err := user.GetCurrentUser(c) - if err != nil { - return - } - namespace = &models.Namespace{ID: namespaceID} - canRead, _, err := namespace.CanRead(s, u) - if err != nil { - return namespace, err - } - if !canRead { - return nil, echo.ErrForbidden - } - return -} diff --git a/pkg/routes/api/v1/user_register.go b/pkg/routes/api/v1/user_register.go index 40f7d233a..b6e6d40ea 100644 --- a/pkg/routes/api/v1/user_register.go +++ b/pkg/routes/api/v1/user_register.go @@ -63,7 +63,7 @@ func RegisterUser(c echo.Context) error { } // Add its namespace - err = models.CreateNewNamespaceForUser(s, newUser) + err = models.CreateNewProjectForUser(s, newUser) if err != nil { _ = s.Rollback() return handler.HandleHTTPError(err, c) diff --git a/pkg/routes/routes.go b/pkg/routes/routes.go index 9ef395d6c..e8ee1eca3 100644 --- a/pkg/routes/routes.go +++ b/pkg/routes/routes.go @@ -22,7 +22,7 @@ // @description * `x-pagination-total-pages`: The total number of available pages for this request // @description * `x-pagination-result-count`: The number of items returned for this request. // @description # Rights -// @description All endpoints which return a single item (project, task, namespace, etc.) - no array - will also return a `x-max-right` header with the max right the user has on this item as an int where `0` is `Read Only`, `1` is `Read & Write` and `2` is `Admin`. +// @description All endpoints which return a single item (project, task, etc.) - no array - will also return a `x-max-right` header with the max right the user has on this item as an int where `0` is `Read Only`, `1` is `Read & Write` and `2` is `Admin`. // @description This can be used to show or hide ui elements based on the rights the user has. // @description # Authorization // @description **JWT-Auth:** Main authorization method, used for most of the requests. Needs `Authorization: Bearer `-header to authenticate successfully. @@ -489,38 +489,6 @@ func registerAPIRoutes(a *echo.Group) { a.DELETE("/filters/:filter", savedFiltersHandler.DeleteWeb) a.POST("/filters/:filter", savedFiltersHandler.UpdateWeb) - namespaceHandler := &handler.WebHandler{ - EmptyStruct: func() handler.CObject { - return &models.Namespace{} - }, - } - a.GET("/namespaces", namespaceHandler.ReadAllWeb) - a.PUT("/namespaces", namespaceHandler.CreateWeb) - a.GET("/namespaces/:namespace", namespaceHandler.ReadOneWeb) - a.POST("/namespaces/:namespace", namespaceHandler.UpdateWeb) - a.DELETE("/namespaces/:namespace", namespaceHandler.DeleteWeb) - a.GET("/namespaces/:namespace/projects", apiv1.GetProjectsByNamespaceID) - - namespaceTeamHandler := &handler.WebHandler{ - EmptyStruct: func() handler.CObject { - return &models.TeamNamespace{} - }, - } - a.GET("/namespaces/:namespace/teams", namespaceTeamHandler.ReadAllWeb) - a.PUT("/namespaces/:namespace/teams", namespaceTeamHandler.CreateWeb) - a.DELETE("/namespaces/:namespace/teams/:team", namespaceTeamHandler.DeleteWeb) - a.POST("/namespaces/:namespace/teams/:team", namespaceTeamHandler.UpdateWeb) - - namespaceUserHandler := &handler.WebHandler{ - EmptyStruct: func() handler.CObject { - return &models.NamespaceUser{} - }, - } - a.GET("/namespaces/:namespace/users", namespaceUserHandler.ReadAllWeb) - a.PUT("/namespaces/:namespace/users", namespaceUserHandler.CreateWeb) - a.DELETE("/namespaces/:namespace/users/:user", namespaceUserHandler.DeleteWeb) - a.POST("/namespaces/:namespace/users/:user", namespaceUserHandler.UpdateWeb) - teamHandler := &handler.WebHandler{ EmptyStruct: func() handler.CObject { return &models.Team{}