Skip to content

Commit e57bf56

Browse files
robkooperlmarini
andauthored
add readonly mode (#406)
* add readonly mode tested with create spaces/datasets/files but might be missing special cases. fixes #405 * more readonly checks * send email with correct status * allow to download (based on permission) * fix user status email * typo * fix page for user change * use latest checkout action * bump java action to v3 * update cache --------- Co-authored-by: Luigi Marini <[email protected]>
1 parent e09f44f commit e57bf56

26 files changed

+241
-223
lines changed

.github/workflows/ci.yml

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ jobs:
3030
build:
3131
runs-on: ubuntu-latest
3232
steps:
33-
- uses: actions/checkout@v2
33+
- uses: actions/checkout@v3
3434
- name: github branch
3535
run: |
3636
if [ "${{ github.event.release.target_commitish }}" != "" ]; then
@@ -46,9 +46,10 @@ jobs:
4646
else
4747
echo "CLOWDER_VERSION=testing" >> $GITHUB_ENV
4848
fi
49-
- uses: actions/setup-java@v1
49+
- uses: actions/setup-java@v3
5050
with:
51-
java-version: 1.8
51+
distribution: 'zulu'
52+
java-version: 8
5253
- name: Cache SBT ivy cache
5354
uses: actions/cache@v1
5455
with:
@@ -84,7 +85,7 @@ jobs:
8485
ports:
8586
- 27017:27017
8687
steps:
87-
- uses: actions/checkout@v2
88+
- uses: actions/checkout@v3
8889
- name: github branch
8990
run: |
9091
if [ "${{ github.event.release.target_commitish }}" != "" ]; then
@@ -100,16 +101,17 @@ jobs:
100101
else
101102
echo "CLOWDER_VERSION=testing" >> $GITHUB_ENV
102103
fi
103-
- uses: actions/setup-java@v1
104+
- uses: actions/setup-java@v3
104105
with:
105-
java-version: 1.8
106+
distribution: 'zulu'
107+
java-version: 8
106108
- name: Cache SBT ivy cache
107-
uses: actions/cache@v1
109+
uses: actions/cache@v3
108110
with:
109111
path: ~/.ivy2/cache
110112
key: ${{ runner.os }}-sbt-ivy-cache-${{ hashFiles('project/Build.scala') }}
111113
- name: Cache SBT
112-
uses: actions/cache@v1
114+
uses: actions/cache@v3
113115
with:
114116
path: ~/.sbt
115117
key: ${{ runner.os }}-sbt-${{ hashFiles('project/Build.scala') }}
@@ -128,7 +130,7 @@ jobs:
128130
runs-on: ubuntu-latest
129131
needs: build
130132
steps:
131-
- uses: actions/checkout@v2
133+
- uses: actions/checkout@v3
132134
- name: github branch
133135
run: |
134136
if [ "${{ github.event.release.target_commitish }}" != "" ]; then
@@ -144,16 +146,17 @@ jobs:
144146
else
145147
echo "CLOWDER_VERSION=testing" >> $GITHUB_ENV
146148
fi
147-
- uses: actions/setup-java@v1
149+
- uses: actions/setup-java@v3
148150
with:
149-
java-version: 1.8
151+
distribution: 'zulu'
152+
java-version: 8
150153
- name: Cache SBT ivy cache
151-
uses: actions/cache@v1
154+
uses: actions/cache@v3
152155
with:
153156
path: ~/.ivy2/cache
154157
key: ${{ runner.os }}-sbt-ivy-cache-${{ hashFiles('project/Build.scala') }}
155158
- name: Cache SBT
156-
uses: actions/cache@v1
159+
uses: actions/cache@v3
157160
with:
158161
path: ~/.sbt
159162
key: ${{ runner.os }}-sbt-${{ hashFiles('project/Build.scala') }}
@@ -204,7 +207,7 @@ jobs:
204207
runs-on: ubuntu-latest
205208
needs: build
206209
steps:
207-
- uses: actions/checkout@v2
210+
- uses: actions/checkout@v3
208211
- name: github branch
209212
run: |
210213
if [ "${{ github.event.release.target_commitish }}" != "" ]; then
@@ -220,16 +223,17 @@ jobs:
220223
else
221224
echo "CLOWDER_VERSION=testing" >> $GITHUB_ENV
222225
fi
223-
- uses: actions/setup-java@v1
226+
- uses: actions/setup-java@v3
224227
with:
225-
java-version: 1.8
228+
distribution: 'zulu'
229+
java-version: 8
226230
- name: Cache SBT ivy cache
227-
uses: actions/cache@v1
231+
uses: actions/cache@v3
228232
with:
229233
path: ~/.ivy2/cache
230234
key: ${{ runner.os }}-sbt-ivy-cache-${{ hashFiles('project/Build.scala') }}
231235
- name: Cache SBT
232-
uses: actions/cache@v1
236+
uses: actions/cache@v3
233237
with:
234238
path: ~/.sbt
235239
key: ${{ runner.os }}-sbt-${{ hashFiles('project/Build.scala') }}

.github/workflows/swagger.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ jobs:
2020
lint:
2121
runs-on: ubuntu-latest
2222
steps:
23-
- uses: actions/checkout@v2
23+
- uses: actions/checkout@v3
2424

2525
- name: openapi-lint
2626
uses: mbowman100/swagger-validator-action@master

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
99
## Unreleased
1010

1111
### Added
12+
- Users can be marked as ReadOnly [#405](https://github.com/clowder-framework/clowder/issues/405)
1213
- Added Trash button to delete section [#347](https://github.com/clowder-framework/clowder/issues/347)
1314
- Add "when" parameter in a few GET API endpoints to enable pagination [#266](https://github.com/clowder-framework/clowder/issues/266)
1415
- Extractors can now specify an extractor_key and an owner (email address) when sending a

app/api/Admin.scala

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -125,10 +125,10 @@ class Admin @Inject() (userService: UserService,
125125
list.foreach(id =>
126126
userService.findById(UUID(id)) match {
127127
case Some(u: ClowderUser) => {
128-
if (u.status == UserStatus.Inactive) {
128+
if (u.status != UserStatus.Active) {
129129
userService.update(u.copy(status = UserStatus.Active))
130-
val subject = s"[${AppConfiguration.getDisplayName}] account activated"
131-
val body = views.html.emails.userActivated(u, active = true)(request)
130+
val subject = s"[${AppConfiguration.getDisplayName}] account is now active"
131+
val body = views.html.emails.userChanged(u, "activated")(request)
132132
util.Mail.sendEmail(subject, request.user, u, body)
133133
}
134134
}
@@ -138,10 +138,10 @@ class Admin @Inject() (userService: UserService,
138138
list.foreach(id =>
139139
userService.findById(UUID(id)) match {
140140
case Some(u: ClowderUser) => {
141-
if (!(u.status == UserStatus.Inactive)) {
141+
if (u.status != UserStatus.Inactive) {
142142
userService.update(u.copy(status = UserStatus.Inactive))
143-
val subject = s"[${AppConfiguration.getDisplayName}] account deactivated"
144-
val body = views.html.emails.userActivated(u, active = false)(request)
143+
val subject = s"[${AppConfiguration.getDisplayName}] account is deactivated"
144+
val body = views.html.emails.userChanged(u, "deactivated")(request)
145145
util.Mail.sendEmail(subject, request.user, u, body)
146146
}
147147
}
@@ -150,26 +150,27 @@ class Admin @Inject() (userService: UserService,
150150
(request.body \ "admin").asOpt[List[String]].foreach(list =>
151151
list.foreach(id =>
152152
userService.findById(UUID(id)) match {
153-
case Some(u: ClowderUser) if (u.status == UserStatus.Active) => {
154-
155-
userService.update(u.copy(status = UserStatus.Admin))
156-
val subject = s"[${AppConfiguration.getDisplayName}] admin access granted"
157-
val body = views.html.emails.userAdmin(u, admin = true)(request)
158-
util.Mail.sendEmail(subject, request.user, u, body)
159-
153+
case Some(u: ClowderUser) => {
154+
if (u.status != UserStatus.Admin) {
155+
userService.update(u.copy(status = UserStatus.Admin))
156+
val subject = s"[${AppConfiguration.getDisplayName}] account is now an admin"
157+
val body = views.html.emails.userChanged(u, "an admin account")(request)
158+
util.Mail.sendEmail(subject, request.user, u, body)
159+
}
160160
}
161161
case _ => Logger.error(s"Could not update user with id=${id}")
162162
}))
163-
(request.body \ "unadmin").asOpt[List[String]].foreach(list =>
163+
(request.body \ "readonly").asOpt[List[String]].foreach(list =>
164164
list.foreach(id =>
165165
userService.findById(UUID(id)) match {
166-
case Some(u: ClowderUser) if (u.status == UserStatus.Admin) => {
167-
userService.update(u.copy(status = UserStatus.Active))
168-
val subject = s"[${AppConfiguration.getDisplayName}] admin access revoked"
169-
val body = views.html.emails.userAdmin(u, admin = false)(request)
170-
util.Mail.sendEmail(subject, request.user, u, body)
166+
case Some(u: ClowderUser) => {
167+
if (u.status != UserStatus.ReadOnly) {
168+
userService.update(u.copy(status = UserStatus.ReadOnly))
169+
val subject = s"[${AppConfiguration.getDisplayName}] account is now an read-only"
170+
val body = views.html.emails.userChanged(u, "read-only")(request)
171+
util.Mail.sendEmail(subject, request.user, u, body)
172+
}
171173
}
172-
173174
case _ => Logger.error(s"Could not update user with id=${id}")
174175
}))
175176
Ok(toJson(Map("status" -> "success")))

app/api/ApiController.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,12 +88,13 @@ trait ApiController extends Controller {
8888
userRequest.user match {
8989
case Some(u) if !AppConfiguration.acceptedTermsOfServices(u.termsOfServices) => Future.successful(Unauthorized("Terms of Service not accepted"))
9090
case Some(u) if (u.status == UserStatus.Inactive) => Future.successful(Unauthorized("Account is not activated"))
91+
case Some(u) if (u.status == UserStatus.ReadOnly && !api.Permission.READONLY.contains(permission) && permission != Permission.DownloadFiles) => Future.successful(Unauthorized("Account is ReadOnly"))
9192
case Some(u) if u.superAdminMode || Permission.checkPermission(userRequest.user, permission, resourceRef) => block(userRequest)
9293
case Some(u) => {
9394
affectedResource match {
9495
case Some(resource) if Permission.checkOwner(u, resource) => block(userRequest)
9596
case _ => Future.successful(Unauthorized("Not authorized"))
96-
}
97+
}
9798
}
9899
case None if Permission.checkPermission(userRequest.user, permission, resourceRef) => block(userRequest)
99100
case _ => Future.successful(Unauthorized("Not authorized"))

app/api/Metadata.scala

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ class Metadata @Inject() (
257257

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

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

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

344-
def editDefinition(id: UUID, spaceId: Option[String]) = AuthenticatedAction(parse.json) {
344+
def editDefinition(id: UUID, spaceId: Option[String]) = PermissionAction(Permission.EditVocabulary)(parse.json) {
345345
implicit request =>
346346
request.user match {
347347
case Some(user) => {
@@ -387,7 +387,7 @@ class Metadata @Inject() (
387387
}
388388
}
389389

390-
def deleteDefinition(id: UUID) = AuthenticatedAction { implicit request =>
390+
def deleteDefinition(id: UUID) = PermissionAction(Permission.CreateVocabulary) { implicit request =>
391391
implicit val user = request.user
392392
user match {
393393
case Some(user) => {

app/api/Permissions.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,7 @@ object Permission extends Enumeration {
426426
def checkPermission(user: User, permission: Permission, resourceRef: ResourceRef): Boolean = {
427427
// check if user is owner, in that case they can do what they want.
428428
if (user.superAdminMode) return true
429+
if (user.status == UserStatus.ReadOnly && !READONLY.contains(permission) && permission != Permission.DownloadFiles) return false
429430
if (checkOwner(users.findByIdentity(user), resourceRef)) return true
430431

431432
resourceRef match {

app/api/Spaces.scala

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -32,35 +32,39 @@ class Spaces @Inject()(spaces: SpaceService,
3232
val spaceTitle: String = Messages("space.title")
3333

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

59+
}
60+
Ok(toJson(Map("id" -> id)))
5661
}
57-
Ok(toJson(Map("id" -> id)))
62+
case None => Ok(toJson(Map("status" -> "error")))
5863
}
59-
case None => Ok(toJson(Map("status" -> "error")))
60-
}
6164

65+
}
66+
case (_, _) => BadRequest(toJson("Missing required parameters"))
6267
}
63-
case (_, _) => BadRequest(toJson("Missing required parameters"))
6468
}
6569
}
6670

app/controllers/SecuredController.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,9 @@ trait SecuredController extends Controller {
104104
userRequest.user match {
105105
case Some(u) if !AppConfiguration.acceptedTermsOfServices(u.termsOfServices) => Future.successful(Results.Redirect(routes.Application.tos(Some(request.uri))))
106106
case Some(u) if (u.status==UserStatus.Inactive) => Future.successful(Results.Redirect(routes.Error.notActivated()))
107+
case Some(u) if (u.status==UserStatus.ReadOnly && !api.Permission.READONLY.contains(permission) && permission != Permission.DownloadFiles) => {
108+
Future.successful(Results.Redirect(routes.Error.notAuthorized("Account is ReadOnly", "", "")))
109+
}
107110
case Some(u) if u.superAdminMode || Permission.checkPermission(userRequest.user, permission, resourceRef) => block(userRequest)
108111
case Some(u) => notAuthorizedMessage(userRequest.user, resourceRef)
109112
case None if Permission.checkPermission(userRequest.user, permission, resourceRef) => block(userRequest)

app/controllers/Spaces.scala

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ class Spaces @Inject() (spaces: SpaceService, users: UserService, events: EventS
222222
}
223223
}
224224

225-
def newSpace() = AuthenticatedAction { implicit request =>
225+
def newSpace() = PermissionAction(Permission.CreateSpace) { implicit request =>
226226
implicit val user = request.user
227227
Ok(views.html.spaces.newSpace(spaceForm))
228228
}
@@ -396,10 +396,10 @@ class Spaces @Inject() (spaces: SpaceService, users: UserService, events: EventS
396396
* Submit action for new or edit space
397397
*/
398398
// TODO this should check to see if user has editspace for specific space
399-
def submit() = AuthenticatedAction { implicit request =>
399+
def submit() = PermissionAction(Permission.CreateSpace) { implicit request =>
400400
implicit val user = request.user
401401
user match {
402-
case Some(identity) => {
402+
case Some(identity) if identity.status != UserStatus.ReadOnly => {
403403
val userId = request.user.get.id
404404
//need to get the submitValue before binding form data, in case of errors we want to trigger different forms
405405
request.body.asMultipartFormData.get.dataParts.get("submitValue").headOption match {
@@ -483,7 +483,7 @@ class Spaces @Inject() (spaces: SpaceService, users: UserService, events: EventS
483483
case None => { BadRequest("Did not get any submit button value.") }
484484
}
485485
} //some identity
486-
case None => Redirect(routes.Spaces.list()).flashing("error" -> "You are not authorized to create/edit $spaceTitle.")
486+
case _ => Redirect(routes.Spaces.list()).flashing("error" -> "You are not authorized to create/edit $spaceTitle.")
487487
}
488488
}
489489
def followingSpaces(index: Int, limit: Int, mode: String) = PrivateServerAction { implicit request =>

0 commit comments

Comments
 (0)