Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prevent deleting teams that are still referenced #5108

Merged
merged 4 commits into from
Feb 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
- Users are no longer allowed to deactivate their own accounts. [#5070](https://github.com/scalableminds/webknossos/pull/5070)

### Fixed
-
- Fixed a bug where the user could delete teams that were still referenced in annotations, projects or task types, thus creating invalid state. [#5108](https://github.com/scalableminds/webknossos/pull/5108/files)

### Removed
- The isosurface setting was removed. Instead, isosurfaces can be generated via the "Meshes" tab. Also note that the Shift+Click binding for generating an isosurface was removed (for now). Please refer to the "Meshes" tab, too. [#4917](https://github.com/scalableminds/webknossos/pull/4917)
29 changes: 15 additions & 14 deletions app/controllers/TeamController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,50 +8,51 @@ import oxalis.security.WkEnv
import play.api.i18n.Messages
import play.api.libs.json._
import utils.ObjectId

import javax.inject.Inject
import models.binary.DataSetAllowedTeamsDAO
import play.api.mvc.{Action, AnyContent}

import scala.concurrent.ExecutionContext

class TeamController @Inject()(teamDAO: TeamDAO,
userTeamRolesDAO: UserTeamRolesDAO,
datasetAllowedTeamsDAO: DataSetAllowedTeamsDAO,
teamService: TeamService,
sil: Silhouette[WkEnv])(implicit ec: ExecutionContext)
extends Controller {

private def teamNameReads: Reads[String] =
(__ \ "name").read[String]

def list(isEditable: Option[Boolean]) = sil.SecuredAction.async { implicit request =>
def list(isEditable: Option[Boolean]): Action[AnyContent] = sil.SecuredAction.async { implicit request =>
val onlyEditableTeams = isEditable.getOrElse(false)
for {
allTeams <- if (onlyEditableTeams) teamDAO.findAllEditable else teamDAO.findAll
js <- Fox.serialCombined(allTeams)(t => teamService.publicWrites(t))
} yield {
Ok(Json.toJson(js))
}
} yield Ok(Json.toJson(js))
}

def delete(id: String) = sil.SecuredAction.async { implicit request =>
def delete(id: String): Action[AnyContent] = sil.SecuredAction.async { implicit request =>
for {
teamIdValidated <- ObjectId.parse(id)
_ <- teamDAO.findOne(teamIdValidated) ?~> "team.notFound" ~> NOT_FOUND
_ <- bool2Fox(request.identity.isAdmin) ?~> "user.noAdmin" ~> FORBIDDEN
team <- teamDAO.findOne(teamIdValidated) ?~> "team.notFound" ~> NOT_FOUND
_ <- bool2Fox(!team.isOrganizationTeam) ?~> "team.delete.organizationTeam" ~> FORBIDDEN
_ <- teamDAO.deleteOne(teamIdValidated)
_ <- teamService.assertNoReferences(teamIdValidated) ?~> "team.delete.inUse" ~> FORBIDDEN
_ <- userTeamRolesDAO.removeTeamFromAllUsers(teamIdValidated)
} yield {
JsonOk(Messages("team.deleted"))
}
_ <- datasetAllowedTeamsDAO.removeTeamFromAllDatasets(teamIdValidated)
} yield JsonOk(Messages("team.deleted"))
}

def create = sil.SecuredAction.async(parse.json) { implicit request =>
def create: Action[JsValue] = sil.SecuredAction.async(parse.json) { implicit request =>
withJsonBodyUsing(teamNameReads) { teamName =>
for {
_ <- bool2Fox(request.identity.isAdmin) ?~> "user.noAdmin" ~> FORBIDDEN
team = Team(ObjectId.generate, request.identity._organization, teamName)
_ <- teamDAO.insertOne(team)
js <- teamService.publicWrites(team)
} yield {
JsonOk(js, Messages("team.created"))
}
} yield JsonOk(js, Messages("team.created"))
}
}
}
6 changes: 6 additions & 0 deletions app/models/annotation/Annotation.scala
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,12 @@ class AnnotationDAO @Inject()(sqlClient: SQLClient)(implicit ec: ExecutionContex
} yield parsed
}

def countForTeam(teamId: ObjectId): Fox[Int] =
for {
countList <- run(sql"select count(_id) from #$existingCollectionName where _team = $teamId".as[Int])
count <- countList.headOption
} yield count

// Does not use access query (because they dont support prefixes). Use only after separate access check!
def findAllFinishedForProject(projectId: ObjectId): Fox[List[Annotation]] =
for {
Expand Down
6 changes: 6 additions & 0 deletions app/models/binary/DataSet.scala
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,12 @@ class DataSetAllowedTeamsDAO @Inject()(sqlClient: SQLClient)(implicit ec: Execut
retryIfErrorContains = List(transactionSerializationError))
} yield ()
}

def removeTeamFromAllDatasets(teamId: ObjectId): Fox[Unit] =
for {
_ <- run(sqlu"delete from webknossos.dataSet_allowedTeams where _team = $teamId")
} yield ()

}

class DataSetLastUsedTimesDAO @Inject()(sqlClient: SQLClient)(implicit ec: ExecutionContext)
Expand Down
29 changes: 16 additions & 13 deletions app/models/project/Project.scala
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ case class Project(
isDeleted: Boolean = false
) extends FoxImplicits {

def isDeletableBy(user: User) = user._id == _owner || user.isAdmin
def isDeletableBy(user: User): Boolean = user._id == _owner || user.isAdmin

}

Expand Down Expand Up @@ -86,17 +86,15 @@ class ProjectDAO @Inject()(sqlClient: SQLClient)(implicit ec: ExecutionContext)
for {
accessQuery <- readAccessQuery
rList <- run(
sql"select #${columns} from #${existingCollectionName} where _id = ${id.id} and #${accessQuery}"
.as[ProjectsRow])
sql"select #$columns from #$existingCollectionName where _id = $id and #$accessQuery".as[ProjectsRow])
r <- rList.headOption.toFox ?~> ("Could not find object " + id + " in " + collectionName)
parsed <- parse(r) ?~> ("SQLDAO Error: Could not parse database row for object " + id + " in " + collectionName)
} yield parsed

override def findAll(implicit ctx: DBAccessContext): Fox[List[Project]] =
for {
accessQuery <- readAccessQuery
r <- run(
sql"select #${columns} from #${existingCollectionName} where #${accessQuery} order by created".as[ProjectsRow])
r <- run(sql"select #$columns from #$existingCollectionName where #$accessQuery order by created".as[ProjectsRow])
parsed <- Fox.combined(r.toList.map(parse))
} yield parsed

Expand All @@ -108,7 +106,7 @@ class ProjectDAO @Inject()(sqlClient: SQLClient)(implicit ec: ExecutionContext)
from webknossos.projects_ p
join webknossos.tasks_ t on t._project = p._id
join webknossos.taskTypes_ tt on t._taskType = tt._id
where tt._id = ${taskTypeId}
where tt._id = $taskTypeId
""".as[ProjectsRow]
)
parsed <- Fox.combined(r.toList.map(parse))
Expand All @@ -118,22 +116,21 @@ class ProjectDAO @Inject()(sqlClient: SQLClient)(implicit ec: ExecutionContext)
for {
accessQuery <- readAccessQuery
rList <- run(
sql"select #${columns} from #${existingCollectionName} where name = '#${sanitize(name)}' and #${accessQuery}"
sql"select #$columns from #$existingCollectionName where name = '#${sanitize(name)}' and #$accessQuery"
.as[ProjectsRow])
r <- rList.headOption.toFox
parsed <- parse(r)
} yield parsed

def findUsersWithActiveTasks(name: String)(implicit ctx: DBAccessContext): Fox[List[(String, String, String, Int)]] =
def findUsersWithActiveTasks(name: String): Fox[List[(String, String, String, Int)]] =
for {
accessQuery <- readAccessQuery
rSeq <- run(sql"""select u.email, u.firstName, u.lastName, count(a._id)
from
webknossos.annotations_ a
join webknossos.tasks_ t on a._task = t._id
join webknossos.projects_ p on t._project = p._id
join webknossos.users_ u on a._user = u._id
where p.name = ${name}
where p.name = $name
and a.state = '#${AnnotationState.Active.toString}'
and a.typ = '#${AnnotationType.Task}'
group by u.email, u.firstName, u.lastName
Expand All @@ -146,7 +143,7 @@ class ProjectDAO @Inject()(sqlClient: SQLClient)(implicit ec: ExecutionContext)
for {
_ <- run(
sqlu"""insert into webknossos.projects(_id, _team, _owner, name, priority, paused, expectedTime, isblacklistedfromreport, created, isDeleted)
values(${p._id.id}, ${p._team.id}, ${p._owner.id}, ${p.name}, ${p.priority}, ${p.paused}, ${p.expectedTime}, ${p.isBlacklistedFromReport}, ${new java.sql.Timestamp(
values(${p._id}, ${p._team}, ${p._owner}, ${p.name}, ${p.priority}, ${p.paused}, ${p.expectedTime}, ${p.isBlacklistedFromReport}, ${new java.sql.Timestamp(
p.created)}, ${p.isDeleted})""")
} yield ()

Expand All @@ -163,12 +160,18 @@ class ProjectDAO @Inject()(sqlClient: SQLClient)(implicit ec: ExecutionContext)
expectedTime = ${p.expectedTime},
isblacklistedfromreport = ${p.isBlacklistedFromReport},
isDeleted = ${p.isDeleted}
where _id = ${p._id.id}""")
where _id = ${p._id}""")
} yield ()

def updatePaused(id: ObjectId, isPaused: Boolean)(implicit ctx: DBAccessContext) =
def updatePaused(id: ObjectId, isPaused: Boolean)(implicit ctx: DBAccessContext): Fox[Unit] =
updateBooleanCol(id, _.paused, isPaused)

def countForTeam(teamId: ObjectId): Fox[Int] =
youri-k marked this conversation as resolved.
Show resolved Hide resolved
for {
countList <- run(sql"select count(_id) from #$existingCollectionName where _team = $teamId".as[Int])
count <- countList.headOption
} yield count

}

class ProjectService @Inject()(projectDAO: ProjectDAO, teamDAO: TeamDAO, userService: UserService, taskDAO: TaskDAO)(
Expand Down
5 changes: 5 additions & 0 deletions app/models/task/TaskType.scala
Original file line number Diff line number Diff line change
Expand Up @@ -158,4 +158,9 @@ class TaskTypeDAO @Inject()(sqlClient: SQLClient)(implicit ec: ExecutionContext)
where _id = ${t._id.id}""")
} yield ()

def countForTeam(teamId: ObjectId): Fox[Int] =
for {
countList <- run(sql"select count(_id) from #$existingCollectionName where _team = $teamId".as[Int])
count <- countList.headOption
} yield count
}
30 changes: 20 additions & 10 deletions app/models/team/Team.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import com.scalableminds.util.accesscontext.{DBAccessContext, GlobalAccessContex
import com.scalableminds.util.tools.{Fox, FoxImplicits}
import com.scalableminds.webknossos.schema.Tables._
import javax.inject.Inject
import models.annotation.AnnotationDAO
import models.project.ProjectDAO
import models.task.TaskTypeDAO
import models.user.User
import play.api.i18n.{Messages, MessagesProvider}
import play.api.libs.json._
import slick.jdbc.PostgresProfile.api._
import slick.lifted.Rep
Expand All @@ -26,7 +30,11 @@ case class Team(

}

class TeamService @Inject()(organizationDAO: OrganizationDAO)(implicit ec: ExecutionContext) {
class TeamService @Inject()(organizationDAO: OrganizationDAO,
annotationDAO: AnnotationDAO,
projectDAO: ProjectDAO,
taskTypeDAO: TaskTypeDAO)(implicit ec: ExecutionContext)
extends FoxImplicits {

def publicWrites(team: Team, organizationOpt: Option[Organization] = None): Fox[JsObject] =
for {
Expand All @@ -38,6 +46,17 @@ class TeamService @Inject()(organizationDAO: OrganizationDAO)(implicit ec: Execu
"organization" -> organization.name
)
}

def assertNoReferences(teamId: ObjectId)(implicit mp: MessagesProvider): Fox[Unit] =
for {
projectCount <- projectDAO.countForTeam(teamId)
_ <- bool2Fox(projectCount == 0) ?~> Messages("team.inUse.projects", projectCount)
taskTypeCount <- taskTypeDAO.countForTeam(teamId)
_ <- bool2Fox(projectCount == 0) ?~> Messages("team.inUse.taskTypes", taskTypeCount)
annotationCount <- annotationDAO.countForTeam(teamId)
_ <- bool2Fox(projectCount == 0) ?~> Messages("team.inUse.annotations", annotationCount)
} yield ()

}

class TeamDAO @Inject()(sqlClient: SQLClient)(implicit ec: ExecutionContext)
Expand Down Expand Up @@ -75,15 +94,6 @@ class TeamDAO @Inject()(sqlClient: SQLClient)(implicit ec: ExecutionContext)
parsed <- parse(r) ?~> ("SQLDAO Error: Could not parse database row for object " + id + " in " + collectionName)
} yield parsed

def findOneByName(name: String)(implicit ctx: DBAccessContext): Fox[Team] =
for {
accessQuery <- readAccessQuery
rList <- run(
sql"select #$columns from #$existingCollectionName where name = $name and #$accessQuery".as[TeamsRow])
r <- rList.headOption.toFox
parsed <- parse(r)
} yield parsed

override def findAll(implicit ctx: DBAccessContext): Fox[List[Team]] =
for {
accessQuery <- readAccessQuery
Expand Down
2 changes: 1 addition & 1 deletion app/models/user/User.scala
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ class UserTeamRolesDAO @Inject()(userDAO: UserDAO, sqlClient: SQLClient)(implici

def removeTeamFromAllUsers(teamId: ObjectId): Fox[Unit] =
for {
r <- run(sqlu"delete from webknossos.user_team_roles where _team = ${teamId}")
_ <- run(sqlu"delete from webknossos.user_team_roles where _team = $teamId")
} yield ()

def findMemberDifference(potentialSubteam: ObjectId, superteams: List[ObjectId]): Fox[List[User]] =
Expand Down
6 changes: 6 additions & 0 deletions conf/messages
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ team.notAllowed=You are not part of this team. Project can’t be created.
team.admin.notPossibleBy=User {1} can’t be assigned administrative rights in team {0} because he doesn't belong to the teams parent team.
team.admin.notAllowed=You are not authorized to administrate this team.
team.notFound=Team could not be found.
team.nameInUse=Team name is or was already in use. Please chose a different name.
team.delete.organizationTeam=Team cannot be deleted as it is the organization base team
team.delete.inUse=Team cannot be deleted as it is referenced in an at least one annotation, project or task type
team.inUse.annotations=Team is referenced by {0} annotations
team.inUse.projects=Team is referenced by {0} projects
team.inUse.tasktypes=Team is referenced by {0} task types

organization.create.forbidden=You are not allowed to create a new organization
organization.create.failed=Failed to create a new organization
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ test("teams_delete_user_D", async t => {
t.plan(1);

await api.deleteTeam("69882b370d889b84020efd4f").catch(err => {
t.is(err.messages[0].error, "Access denied.");
t.is(err.messages[0].error, "Access denied. Only admin users can execute this operation.");
});
});

Expand Down