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

add readonly mode #406

Merged
merged 11 commits into from
Sep 11, 2023
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
40 changes: 22 additions & 18 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: github branch
run: |
if [ "${{ github.event.release.target_commitish }}" != "" ]; then
Expand All @@ -46,9 +46,10 @@ jobs:
else
echo "CLOWDER_VERSION=testing" >> $GITHUB_ENV
fi
- uses: actions/setup-java@v1
- uses: actions/setup-java@v3
with:
java-version: 1.8
distribution: 'zulu'
java-version: 8
- name: Cache SBT ivy cache
uses: actions/cache@v1
with:
Expand Down Expand Up @@ -84,7 +85,7 @@ jobs:
ports:
- 27017:27017
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: github branch
run: |
if [ "${{ github.event.release.target_commitish }}" != "" ]; then
Expand All @@ -100,16 +101,17 @@ jobs:
else
echo "CLOWDER_VERSION=testing" >> $GITHUB_ENV
fi
- uses: actions/setup-java@v1
- uses: actions/setup-java@v3
with:
java-version: 1.8
distribution: 'zulu'
java-version: 8
- name: Cache SBT ivy cache
uses: actions/cache@v1
uses: actions/cache@v3
with:
path: ~/.ivy2/cache
key: ${{ runner.os }}-sbt-ivy-cache-${{ hashFiles('project/Build.scala') }}
- name: Cache SBT
uses: actions/cache@v1
uses: actions/cache@v3
with:
path: ~/.sbt
key: ${{ runner.os }}-sbt-${{ hashFiles('project/Build.scala') }}
Expand All @@ -128,7 +130,7 @@ jobs:
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: github branch
run: |
if [ "${{ github.event.release.target_commitish }}" != "" ]; then
Expand All @@ -144,16 +146,17 @@ jobs:
else
echo "CLOWDER_VERSION=testing" >> $GITHUB_ENV
fi
- uses: actions/setup-java@v1
- uses: actions/setup-java@v3
with:
java-version: 1.8
distribution: 'zulu'
java-version: 8
- name: Cache SBT ivy cache
uses: actions/cache@v1
uses: actions/cache@v3
with:
path: ~/.ivy2/cache
key: ${{ runner.os }}-sbt-ivy-cache-${{ hashFiles('project/Build.scala') }}
- name: Cache SBT
uses: actions/cache@v1
uses: actions/cache@v3
with:
path: ~/.sbt
key: ${{ runner.os }}-sbt-${{ hashFiles('project/Build.scala') }}
Expand Down Expand Up @@ -204,7 +207,7 @@ jobs:
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: github branch
run: |
if [ "${{ github.event.release.target_commitish }}" != "" ]; then
Expand All @@ -220,16 +223,17 @@ jobs:
else
echo "CLOWDER_VERSION=testing" >> $GITHUB_ENV
fi
- uses: actions/setup-java@v1
- uses: actions/setup-java@v3
with:
java-version: 1.8
distribution: 'zulu'
java-version: 8
- name: Cache SBT ivy cache
uses: actions/cache@v1
uses: actions/cache@v3
with:
path: ~/.ivy2/cache
key: ${{ runner.os }}-sbt-ivy-cache-${{ hashFiles('project/Build.scala') }}
- name: Cache SBT
uses: actions/cache@v1
uses: actions/cache@v3
with:
path: ~/.sbt
key: ${{ runner.os }}-sbt-${{ hashFiles('project/Build.scala') }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/swagger.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3

- name: openapi-lint
uses: mbowman100/swagger-validator-action@master
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
## Unreleased

### Added
- Users can be marked as ReadOnly [#405](https://github.com/clowder-framework/clowder/issues/405)
- Added Trash button to delete section [#347](https://github.com/clowder-framework/clowder/issues/347)
- Add "when" parameter in a few GET API endpoints to enable pagination [#266](https://github.com/clowder-framework/clowder/issues/266)
- Extractors can now specify an extractor_key and an owner (email address) when sending a
Expand Down
41 changes: 21 additions & 20 deletions app/api/Admin.scala
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,10 @@ class Admin @Inject() (userService: UserService,
list.foreach(id =>
userService.findById(UUID(id)) match {
case Some(u: ClowderUser) => {
if (u.status == UserStatus.Inactive) {
if (u.status != UserStatus.Active) {
userService.update(u.copy(status = UserStatus.Active))
val subject = s"[${AppConfiguration.getDisplayName}] account activated"
val body = views.html.emails.userActivated(u, active = true)(request)
val subject = s"[${AppConfiguration.getDisplayName}] account is now active"
val body = views.html.emails.userChanged(u, "activated")(request)
util.Mail.sendEmail(subject, request.user, u, body)
}
}
Expand All @@ -138,10 +138,10 @@ class Admin @Inject() (userService: UserService,
list.foreach(id =>
userService.findById(UUID(id)) match {
case Some(u: ClowderUser) => {
if (!(u.status == UserStatus.Inactive)) {
if (u.status != UserStatus.Inactive) {
userService.update(u.copy(status = UserStatus.Inactive))
val subject = s"[${AppConfiguration.getDisplayName}] account deactivated"
val body = views.html.emails.userActivated(u, active = false)(request)
val subject = s"[${AppConfiguration.getDisplayName}] account is deactivated"
val body = views.html.emails.userChanged(u, "deactivated")(request)
util.Mail.sendEmail(subject, request.user, u, body)
}
}
Expand All @@ -150,26 +150,27 @@ class Admin @Inject() (userService: UserService,
(request.body \ "admin").asOpt[List[String]].foreach(list =>
list.foreach(id =>
userService.findById(UUID(id)) match {
case Some(u: ClowderUser) if (u.status == UserStatus.Active) => {

userService.update(u.copy(status = UserStatus.Admin))
val subject = s"[${AppConfiguration.getDisplayName}] admin access granted"
val body = views.html.emails.userAdmin(u, admin = true)(request)
util.Mail.sendEmail(subject, request.user, u, body)

case Some(u: ClowderUser) => {
if (u.status != UserStatus.Admin) {
userService.update(u.copy(status = UserStatus.Admin))
val subject = s"[${AppConfiguration.getDisplayName}] account is now an admin"
val body = views.html.emails.userChanged(u, "an admin account")(request)
util.Mail.sendEmail(subject, request.user, u, body)
}
}
case _ => Logger.error(s"Could not update user with id=${id}")
}))
(request.body \ "unadmin").asOpt[List[String]].foreach(list =>
(request.body \ "readonly").asOpt[List[String]].foreach(list =>
list.foreach(id =>
userService.findById(UUID(id)) match {
case Some(u: ClowderUser) if (u.status == UserStatus.Admin) => {
userService.update(u.copy(status = UserStatus.Active))
val subject = s"[${AppConfiguration.getDisplayName}] admin access revoked"
val body = views.html.emails.userAdmin(u, admin = false)(request)
util.Mail.sendEmail(subject, request.user, u, body)
case Some(u: ClowderUser) => {
if (u.status != UserStatus.ReadOnly) {
userService.update(u.copy(status = UserStatus.ReadOnly))
val subject = s"[${AppConfiguration.getDisplayName}] account is now an read-only"
val body = views.html.emails.userChanged(u, "read-only")(request)
util.Mail.sendEmail(subject, request.user, u, body)
}
}

case _ => Logger.error(s"Could not update user with id=${id}")
}))
Ok(toJson(Map("status" -> "success")))
Expand Down
3 changes: 2 additions & 1 deletion app/api/ApiController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,13 @@ trait ApiController extends Controller {
userRequest.user match {
case Some(u) if !AppConfiguration.acceptedTermsOfServices(u.termsOfServices) => Future.successful(Unauthorized("Terms of Service not accepted"))
case Some(u) if (u.status == UserStatus.Inactive) => Future.successful(Unauthorized("Account is not activated"))
case Some(u) if (u.status == UserStatus.ReadOnly && !api.Permission.READONLY.contains(permission) && permission != Permission.DownloadFiles) => Future.successful(Unauthorized("Account is ReadOnly"))
case Some(u) if u.superAdminMode || Permission.checkPermission(userRequest.user, permission, resourceRef) => block(userRequest)
case Some(u) => {
affectedResource match {
case Some(resource) if Permission.checkOwner(u, resource) => block(userRequest)
case _ => Future.successful(Unauthorized("Not authorized"))
}
}
}
case None if Permission.checkPermission(userRequest.user, permission, resourceRef) => block(userRequest)
case _ => Future.successful(Unauthorized("Not authorized"))
Expand Down
10 changes: 5 additions & 5 deletions app/api/Metadata.scala
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ class Metadata @Inject() (

// Given a list of terms, create a new standard vocabulary from the list
// Expects a JSON array of Strings as the request body
def createVocabulary() = AuthenticatedAction(parse.json) {
def createVocabulary() = PermissionAction(Permission.CreateVocabulary)(parse.json) {
implicit request =>
request.user match {
case None => BadRequest(toJson("Invalid user"))
Expand All @@ -278,7 +278,7 @@ class Metadata @Inject() (

// Given an ID, replace the entire terms list of a standard vocabulary
// Expects a JSON array of Strings as the request body
def updateVocabulary(id: UUID) = AuthenticatedAction(parse.json) {
def updateVocabulary(id: UUID) = PermissionAction(Permission.EditVocabulary)(parse.json) {
implicit request =>
request.user match {
case None => BadRequest(toJson("Invalid user"))
Expand All @@ -304,7 +304,7 @@ class Metadata @Inject() (
}

// Given an ID, delete the standard vocabulary with that ID
def deleteVocabulary(id: UUID) = AuthenticatedAction(parse.empty) {
def deleteVocabulary(id: UUID) = PermissionAction(Permission.DeleteVocabulary)(parse.empty) {
implicit request =>
request.user match {
case None => BadRequest(toJson("Invalid user"))
Expand Down Expand Up @@ -341,7 +341,7 @@ class Metadata @Inject() (
}
}

def editDefinition(id: UUID, spaceId: Option[String]) = AuthenticatedAction(parse.json) {
def editDefinition(id: UUID, spaceId: Option[String]) = PermissionAction(Permission.EditVocabulary)(parse.json) {
implicit request =>
request.user match {
case Some(user) => {
Expand Down Expand Up @@ -387,7 +387,7 @@ class Metadata @Inject() (
}
}

def deleteDefinition(id: UUID) = AuthenticatedAction { implicit request =>
def deleteDefinition(id: UUID) = PermissionAction(Permission.CreateVocabulary) { implicit request =>
implicit val user = request.user
user match {
case Some(user) => {
Expand Down
1 change: 1 addition & 0 deletions app/api/Permissions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,7 @@ object Permission extends Enumeration {
def checkPermission(user: User, permission: Permission, resourceRef: ResourceRef): Boolean = {
// check if user is owner, in that case they can do what they want.
if (user.superAdminMode) return true
if (user.status == UserStatus.ReadOnly && !READONLY.contains(permission) && permission != Permission.DownloadFiles) return false
if (checkOwner(users.findByIdentity(user), resourceRef)) return true

resourceRef match {
Expand Down
50 changes: 27 additions & 23 deletions app/api/Spaces.scala
Original file line number Diff line number Diff line change
Expand Up @@ -32,35 +32,39 @@ class Spaces @Inject()(spaces: SpaceService,
val spaceTitle: String = Messages("space.title")

//TODO- Minimal Space created with Name and description. URLs are not yet put in
def createSpace() = AuthenticatedAction(parse.json) { implicit request =>
def createSpace() = PermissionAction(Permission.CreateSpace)(parse.json) { implicit request =>
Logger.debug("Creating new space")
val nameOpt = (request.body \ "name").asOpt[String]
val descOpt = (request.body \ "description").asOpt[String]
(nameOpt, descOpt) match {
case (Some(name), Some(description)) => {
// TODO: add creator
val userId = request.user.get.id
val c = ProjectSpace(name = name, description = description, created = new Date(), creator = userId,
homePage = List.empty, logoURL = None, bannerURL = None, collectionCount = 0,
datasetCount = 0, fileCount = 0, userCount = 0, spaceBytes = 0, metadata = List.empty)
spaces.insert(c) match {
case Some(id) => {
appConfig.incrementCount('spaces, 1)
events.addObjectEvent(request.user, c.id, c.name, "create_space")
userService.findRoleByName("Admin") match {
case Some(realRole) => {
spaces.addUser(userId, realRole, UUID(id))
}
case None => Logger.info("No admin role found")
if(request.user.get.status == UserStatus.ReadOnly) {
BadRequest(toJson("User is Read-Only"))
} else {
val nameOpt = (request.body \ "name").asOpt[String]
val descOpt = (request.body \ "description").asOpt[String]
(nameOpt, descOpt) match {
case (Some(name), Some(description)) => {
// TODO: add creator
val userId = request.user.get.id
val c = ProjectSpace(name = name, description = description, created = new Date(), creator = userId,
homePage = List.empty, logoURL = None, bannerURL = None, collectionCount = 0,
datasetCount = 0, fileCount = 0, userCount = 0, spaceBytes = 0, metadata = List.empty)
spaces.insert(c) match {
case Some(id) => {
appConfig.incrementCount('spaces, 1)
events.addObjectEvent(request.user, c.id, c.name, "create_space")
userService.findRoleByName("Admin") match {
case Some(realRole) => {
spaces.addUser(userId, realRole, UUID(id))
}
case None => Logger.info("No admin role found")

}
Ok(toJson(Map("id" -> id)))
}
Ok(toJson(Map("id" -> id)))
case None => Ok(toJson(Map("status" -> "error")))
}
case None => Ok(toJson(Map("status" -> "error")))
}

}
case (_, _) => BadRequest(toJson("Missing required parameters"))
}
case (_, _) => BadRequest(toJson("Missing required parameters"))
}
}

Expand Down
3 changes: 3 additions & 0 deletions app/controllers/SecuredController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ trait SecuredController extends Controller {
userRequest.user match {
case Some(u) if !AppConfiguration.acceptedTermsOfServices(u.termsOfServices) => Future.successful(Results.Redirect(routes.Application.tos(Some(request.uri))))
case Some(u) if (u.status==UserStatus.Inactive) => Future.successful(Results.Redirect(routes.Error.notActivated()))
case Some(u) if (u.status==UserStatus.ReadOnly && !api.Permission.READONLY.contains(permission) && permission != Permission.DownloadFiles) => {
Future.successful(Results.Redirect(routes.Error.notAuthorized("Account is ReadOnly", "", "")))
}
case Some(u) if u.superAdminMode || Permission.checkPermission(userRequest.user, permission, resourceRef) => block(userRequest)
case Some(u) => notAuthorizedMessage(userRequest.user, resourceRef)
case None if Permission.checkPermission(userRequest.user, permission, resourceRef) => block(userRequest)
Expand Down
8 changes: 4 additions & 4 deletions app/controllers/Spaces.scala
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ class Spaces @Inject() (spaces: SpaceService, users: UserService, events: EventS
}
}

def newSpace() = AuthenticatedAction { implicit request =>
def newSpace() = PermissionAction(Permission.CreateSpace) { implicit request =>
implicit val user = request.user
Ok(views.html.spaces.newSpace(spaceForm))
}
Expand Down Expand Up @@ -396,10 +396,10 @@ class Spaces @Inject() (spaces: SpaceService, users: UserService, events: EventS
* Submit action for new or edit space
*/
// TODO this should check to see if user has editspace for specific space
def submit() = AuthenticatedAction { implicit request =>
def submit() = PermissionAction(Permission.CreateSpace) { implicit request =>
implicit val user = request.user
user match {
case Some(identity) => {
case Some(identity) if identity.status != UserStatus.ReadOnly => {
val userId = request.user.get.id
//need to get the submitValue before binding form data, in case of errors we want to trigger different forms
request.body.asMultipartFormData.get.dataParts.get("submitValue").headOption match {
Expand Down Expand Up @@ -483,7 +483,7 @@ class Spaces @Inject() (spaces: SpaceService, users: UserService, events: EventS
case None => { BadRequest("Did not get any submit button value.") }
}
} //some identity
case None => Redirect(routes.Spaces.list()).flashing("error" -> "You are not authorized to create/edit $spaceTitle.")
case _ => Redirect(routes.Spaces.list()).flashing("error" -> "You are not authorized to create/edit $spaceTitle.")
}
}
def followingSpaces(index: Int, limit: Int, mode: String) = PrivateServerAction { implicit request =>
Expand Down
2 changes: 1 addition & 1 deletion app/models/User.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import play.api.libs.json._

object UserStatus extends Enumeration {
type UserStatus = Value
val Inactive, Active, Admin = Value
val Inactive, Active, Admin, ReadOnly = Value
}

/**
Expand Down
Loading
Loading