diff --git a/src/main/resources/update/gitbucket-core_4.7.sql b/src/main/resources/update/gitbucket-core_4.7.sql new file mode 100644 index 0000000..ef13c70 --- /dev/null +++ b/src/main/resources/update/gitbucket-core_4.7.sql @@ -0,0 +1,2 @@ +-- DELETE COLLABORATORS IN GROUP REPOSITORIES +DELETE FROM COLLABORATOR WHERE USER_NAME IN (SELECT USER_NAME FROM ACCOUNT WHERE GROUP_ACCOUNT = TRUE) diff --git a/src/main/resources/update/gitbucket-core_4.7.xml b/src/main/resources/update/gitbucket-core_4.7.xml new file mode 100644 index 0000000..a128800 --- /dev/null +++ b/src/main/resources/update/gitbucket-core_4.7.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + ENABLE_WIKI = FALSE + + + + ENABLE_WIKI = TRUE AND ALLOW_WIKI_EDITING = FALSE + + + + ENABLE_WIKI = TRUE AND ALLOW_WIKI_EDITING = TRUE + + + + ENABLE_ISSUES = FALSE + + + + ENABLE_ISSUES = TRUE + + + + + diff --git a/src/main/scala/gitbucket/core/GitBucketCoreModule.scala b/src/main/scala/gitbucket/core/GitBucketCoreModule.scala index 6830d2d..c04a687 100644 --- a/src/main/scala/gitbucket/core/GitBucketCoreModule.scala +++ b/src/main/scala/gitbucket/core/GitBucketCoreModule.scala @@ -18,5 +18,9 @@ new Version("4.5.0"), new Version("4.6.0", new LiquibaseMigration("update/gitbucket-core_4.6.xml") + ), + new Version("4.7.0", + new LiquibaseMigration("update/gitbucket-core_4.7.xml"), + new SqlMigration("update/gitbucket-core_4.7.sql") ) ) diff --git a/src/main/scala/gitbucket/core/api/ApiUser.scala b/src/main/scala/gitbucket/core/api/ApiUser.scala index 7259c12..9b3dc9d 100644 --- a/src/main/scala/gitbucket/core/api/ApiUser.scala +++ b/src/main/scala/gitbucket/core/api/ApiUser.scala @@ -30,7 +30,7 @@ def apply(user: Account): ApiUser = ApiUser( login = user.userName, email = user.mailAddress, - `type` = if(user.isGroupAccount){ "Organization" }else{ "User" }, + `type` = if(user.isGroupAccount){ "Organization" } else { "User" }, site_admin = user.isAdmin, created_at = user.registeredDate ) diff --git a/src/main/scala/gitbucket/core/controller/AccountController.scala b/src/main/scala/gitbucket/core/controller/AccountController.scala index fa46b78..bfb9ccd 100644 --- a/src/main/scala/gitbucket/core/controller/AccountController.scala +++ b/src/main/scala/gitbucket/core/controller/AccountController.scala @@ -319,13 +319,13 @@ // Update GROUP_MEMBER updateGroupMembers(form.groupName, members) - // Update COLLABORATOR for group repositories - getRepositoryNamesOfUser(form.groupName).foreach { repositoryName => - removeCollaborators(form.groupName, repositoryName) - members.foreach { case (userName, isManager) => - addCollaborator(form.groupName, repositoryName, userName) - } - } +// // Update COLLABORATOR for group repositories +// getRepositoryNamesOfUser(form.groupName).foreach { repositoryName => +// removeCollaborators(form.groupName, repositoryName) +// members.foreach { case (userName, isManager) => +// addCollaborator(form.groupName, repositoryName, userName) +// } +// } updateImage(form.groupName, form.fileId, form.clearImage) redirect(s"/${form.groupName}") @@ -402,13 +402,13 @@ parentUserName = Some(repository.owner) ) - // Add collaborators for group repository - val ownerAccount = getAccountByUserName(accountName).get - if(ownerAccount.isGroupAccount){ - getGroupMembers(accountName).foreach { member => - addCollaborator(accountName, repository.name, member.userName) - } - } +// // Add collaborators for group repository +// val ownerAccount = getAccountByUserName(accountName).get +// if(ownerAccount.isGroupAccount){ +// getGroupMembers(accountName).foreach { member => +// addCollaborator(accountName, repository.name, member.userName) +// } +// } // Insert default labels insertDefaultLabels(accountName, repository.name) diff --git a/src/main/scala/gitbucket/core/controller/ApiController.scala b/src/main/scala/gitbucket/core/controller/ApiController.scala index 692a1f9..eddd941 100644 --- a/src/main/scala/gitbucket/core/controller/ApiController.scala +++ b/src/main/scala/gitbucket/core/controller/ApiController.scala @@ -35,7 +35,7 @@ with GroupManagerAuthenticator with ReferrerAuthenticator with ReadableUsersAuthenticator - with CollaboratorsAuthenticator + with WritableUsersAuthenticator trait ApiControllerBase extends ControllerBase { self: RepositoryService @@ -52,7 +52,7 @@ with GroupManagerAuthenticator with ReferrerAuthenticator with ReadableUsersAuthenticator - with CollaboratorsAuthenticator => + with WritableUsersAuthenticator => /** * https://developer.github.com/v3/#root-endpoint @@ -177,7 +177,8 @@ * https://developer.github.com/v3/repos/collaborators/#list-collaborators */ get("/api/v3/repos/:owner/:repo/collaborators") (referrersOnly { repository => - JsonFormat(getCollaborators(params("owner"), params("repo")).map(u => ApiUser(getAccountByUserName(u).get))) + // TODO Should ApiUser take permission? getCollaboratorUserNames does not return owner group members. + JsonFormat(getCollaboratorUserNames(params("owner"), params("repo")).map(u => ApiUser(getAccountByUserName(u).get))) }) /** @@ -327,7 +328,7 @@ * Create a label * https://developer.github.com/v3/issues/labels/#create-a-label */ - post("/api/v3/repos/:owner/:repository/labels")(collaboratorsOnly { repository => + post("/api/v3/repos/:owner/:repository/labels")(writableUsersOnly { repository => (for{ data <- extractFromJsonBody[CreateALabel] if data.isValid } yield { @@ -352,7 +353,7 @@ * Update a label * https://developer.github.com/v3/issues/labels/#update-a-label */ - patch("/api/v3/repos/:owner/:repository/labels/:labelName")(collaboratorsOnly { repository => + patch("/api/v3/repos/:owner/:repository/labels/:labelName")(writableUsersOnly { repository => (for{ data <- extractFromJsonBody[CreateALabel] if data.isValid } yield { @@ -378,7 +379,7 @@ * Delete a label * https://developer.github.com/v3/issues/labels/#delete-a-label */ - delete("/api/v3/repos/:owner/:repository/labels/:labelName")(collaboratorsOnly { repository => + delete("/api/v3/repos/:owner/:repository/labels/:labelName")(writableUsersOnly { repository => LockUtil.lock(RepositoryName(repository).fullName) { getLabel(repository.owner, repository.name, params("labelName")).map { label => deleteLabel(repository.owner, repository.name, label.labelId) @@ -466,7 +467,7 @@ /** * https://developer.github.com/v3/repos/statuses/#create-a-status */ - post("/api/v3/repos/:owner/:repo/statuses/:sha")(collaboratorsOnly { repository => + post("/api/v3/repos/:owner/:repo/statuses/:sha")(writableUsersOnly { repository => (for{ ref <- params.get("sha") sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref) diff --git a/src/main/scala/gitbucket/core/controller/IndexController.scala b/src/main/scala/gitbucket/core/controller/IndexController.scala index 6bc2751..0d69ebe 100644 --- a/src/main/scala/gitbucket/core/controller/IndexController.scala +++ b/src/main/scala/gitbucket/core/controller/IndexController.scala @@ -108,18 +108,29 @@ */ get("/_user/proposals")(usersOnly { contentType = formats("json") + val user = params("user").toBoolean + val group = params("group").toBoolean org.json4s.jackson.Serialization.write( - Map("options" -> getAllUsers(false).filter(!_.isGroupAccount).map(_.userName).toArray) + Map("options" -> ( + getAllUsers(false) + .withFilter { t => (user, group) match { + case (true, true) => true + case (true, false) => !t.isGroupAccount + case (false, true) => t.isGroupAccount + case (false, false) => false + }}.map { t => t.userName } + )) ) }) /** - * JSON API for checking user existence. + * JSON API for checking user or group existence. + * Returns a single string which is any of "group", "user" or "". */ post("/_user/existence")(usersOnly { getAccountByUserName(params("userName")).map { account => - if(params.get("userOnly").isDefined) !account.isGroupAccount else true - } getOrElse false + if(account.isGroupAccount) "group" else "user" + } getOrElse "" }) // TODO Move to RepositoryViwerController? diff --git a/src/main/scala/gitbucket/core/controller/IssuesController.scala b/src/main/scala/gitbucket/core/controller/IssuesController.scala index 6ddcd40..90aca13 100644 --- a/src/main/scala/gitbucket/core/controller/IssuesController.scala +++ b/src/main/scala/gitbucket/core/controller/IssuesController.scala @@ -2,24 +2,24 @@ import gitbucket.core.issues.html import gitbucket.core.service.IssuesService._ +import gitbucket.core.service.RepositoryService.RepositoryInfo import gitbucket.core.service._ import gitbucket.core.util.ControlUtil._ import gitbucket.core.util.Implicits._ import gitbucket.core.util._ import gitbucket.core.view import gitbucket.core.view.Markdown - import io.github.gitbucket.scalatra.forms._ import org.scalatra.Ok class IssuesController extends IssuesControllerBase with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService with HandleCommentService - with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with WebHookIssueCommentService + with ReadableUsersAuthenticator with ReferrerAuthenticator with WritableUsersAuthenticator with PullRequestService with WebHookIssueCommentService trait IssuesControllerBase extends ControllerBase { self: IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService with HandleCommentService - with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with WebHookIssueCommentService => + with ReadableUsersAuthenticator with ReferrerAuthenticator with WritableUsersAuthenticator with PullRequestService with WebHookIssueCommentService => case class IssueCreateForm(title: String, content: Option[String], assignedUserName: Option[String], milestoneId: Option[Int], labelNames: Option[String]) @@ -67,72 +67,77 @@ _, getComments(owner, name, issueId.toInt), getIssueLabels(owner, name, issueId.toInt), - (getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).exists(_.isGroupAccount)) Nil else List(owner))).sorted, + getAssignableUserNames(owner, name), getMilestonesWithIssueCount(owner, name), getLabels(owner, name), - hasWritePermission(owner, name, context.loginAccount), + isEditable(repository), + isManageable(repository), repository) } getOrElse NotFound() } }) get("/:owner/:repository/issues/new")(readableUsersOnly { repository => - defining(repository.owner, repository.name){ case (owner, name) => - html.create( - (getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).exists(_.isGroupAccount)) Nil else List(owner))).sorted, + if(isEditable(repository)){ // TODO Should this check is provided by authenticator? + defining(repository.owner, repository.name){ case (owner, name) => + html.create( + getAssignableUserNames(owner, name), getMilestones(owner, name), getLabels(owner, name), hasWritePermission(owner, name, context.loginAccount), repository) - } + } + } else Unauthorized() }) post("/:owner/:repository/issues/new", issueCreateForm)(readableUsersOnly { (form, repository) => - defining(repository.owner, repository.name){ case (owner, name) => - val writable = hasWritePermission(owner, name, context.loginAccount) - val userName = context.loginAccount.get.userName + if(isEditable(repository)){ // TODO Should this check is provided by authenticator? + defining(repository.owner, repository.name){ case (owner, name) => + val manageable = isManageable(repository) + val userName = context.loginAccount.get.userName - // insert issue - val issueId = createIssue(owner, name, userName, form.title, form.content, - if(writable) form.assignedUserName else None, - if(writable) form.milestoneId else None) + // insert issue + val issueId = createIssue(owner, name, userName, form.title, form.content, + if (manageable) form.assignedUserName else None, + if (manageable) form.milestoneId else None) - // insert labels - if(writable){ - form.labelNames.map { value => - val labels = getLabels(owner, name) - value.split(",").foreach { labelName => - labels.find(_.labelName == labelName).map { label => - registerIssueLabel(owner, name, issueId, label.labelId) + // insert labels + if (manageable) { + form.labelNames.map { value => + val labels = getLabels(owner, name) + value.split(",").foreach { labelName => + labels.find(_.labelName == labelName).map { label => + registerIssueLabel(owner, name, issueId, label.labelId) + } } } } - } - // record activity - recordCreateIssueActivity(owner, name, userName, issueId, form.title) + // record activity + recordCreateIssueActivity(owner, name, userName, issueId, form.title) - getIssue(owner, name, issueId.toString).foreach { issue => - // extract references and create refer comment - createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""), context.loginAccount.get) + getIssue(owner, name, issueId.toString).foreach { issue => + // extract references and create refer comment + createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""), context.loginAccount.get) - // call web hooks - callIssuesWebHook("opened", repository, issue, context.baseUrl, context.loginAccount.get) + // call web hooks + callIssuesWebHook("opened", repository, issue, context.baseUrl, context.loginAccount.get) - // notifications - Notifier().toNotify(repository, issue, form.content.getOrElse("")){ - Notifier.msgIssue(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}") + // notifications + Notifier().toNotify(repository, issue, form.content.getOrElse("")) { + Notifier.msgIssue(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}") + } } - } - redirect(s"/${owner}/${name}/issues/${issueId}") - } + redirect(s"/${owner}/${name}/issues/${issueId}") + } + } else Unauthorized() }) ajaxPost("/:owner/:repository/issues/edit_title/:id", issueTitleEditForm)(readableUsersOnly { (title, repository) => defining(repository.owner, repository.name){ case (owner, name) => getIssue(owner, name, params("id")).map { issue => - if(isEditable(owner, name, issue.openedUserName)){ + if(isEditableContent(owner, name, issue.openedUserName)){ // update issue updateIssue(owner, name, issue.issueId, title, issue.content) // extract references and create refer comment @@ -147,7 +152,7 @@ ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (content, repository) => defining(repository.owner, repository.name){ case (owner, name) => getIssue(owner, name, params("id")).map { issue => - if(isEditable(owner, name, issue.openedUserName)){ + if(isEditableContent(owner, name, issue.openedUserName)){ // update issue updateIssue(owner, name, issue.issueId, issue.title, content) // extract references and create refer comment @@ -161,7 +166,7 @@ post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) => getIssue(repository.owner, repository.name, form.issueId.toString).flatMap { issue => - val actionOpt = params.get("action").filter(_ => isEditable(issue.userName, issue.repositoryName, issue.openedUserName)) + val actionOpt = params.get("action").filter(_ => isEditableContent(issue.userName, issue.repositoryName, issue.openedUserName)) handleComment(issue, Some(form.content), repository, actionOpt) map { case (issue, id) => redirect(s"/${repository.owner}/${repository.name}/${ if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}") @@ -171,7 +176,7 @@ post("/:owner/:repository/issue_comments/state", issueStateForm)(readableUsersOnly { (form, repository) => getIssue(repository.owner, repository.name, form.issueId.toString).flatMap { issue => - val actionOpt = params.get("action").filter(_ => isEditable(issue.userName, issue.repositoryName, issue.openedUserName)) + val actionOpt = params.get("action").filter(_ => isEditableContent(issue.userName, issue.repositoryName, issue.openedUserName)) handleComment(issue, form.content, repository, actionOpt) map { case (issue, id) => redirect(s"/${repository.owner}/${repository.name}/${ if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}") @@ -182,7 +187,7 @@ ajaxPost("/:owner/:repository/issue_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) => defining(repository.owner, repository.name){ case (owner, name) => getComment(owner, name, params("id")).map { comment => - if(isEditable(owner, name, comment.commentedUserName)){ + if(isEditableContent(owner, name, comment.commentedUserName)){ updateComment(comment.commentId, form.content) redirect(s"/${owner}/${name}/issue_comments/_data/${comment.commentId}") } else Unauthorized() @@ -193,7 +198,7 @@ ajaxPost("/:owner/:repository/issue_comments/delete/:id")(readableUsersOnly { repository => defining(repository.owner, repository.name){ case (owner, name) => getComment(owner, name, params("id")).map { comment => - if(isEditable(owner, name, comment.commentedUserName)){ + if(isEditableContent(owner, name, comment.commentedUserName)){ Ok(deleteComment(comment.commentId)) } else Unauthorized() } getOrElse NotFound() @@ -202,7 +207,7 @@ ajaxGet("/:owner/:repository/issues/_data/:id")(readableUsersOnly { repository => getIssue(repository.owner, repository.name, params("id")) map { x => - if(isEditable(x.userName, x.repositoryName, x.openedUserName)){ + if(isEditableContent(x.userName, x.repositoryName, x.openedUserName)){ params.get("dataType") collect { case t if t == "html" => html.editissue(x.content, x.issueId, repository) } getOrElse { @@ -218,7 +223,7 @@ enableAnchor = true, enableLineBreaks = true, enableTaskList = true, - hasWritePermission = isEditable(x.userName, x.repositoryName, x.openedUserName) + hasWritePermission = true ) ) ) @@ -229,7 +234,7 @@ ajaxGet("/:owner/:repository/issue_comments/_data/:id")(readableUsersOnly { repository => getComment(repository.owner, repository.name, params("id")) map { x => - if(isEditable(x.userName, x.repositoryName, x.commentedUserName)){ + if(isEditableContent(x.userName, x.repositoryName, x.commentedUserName)){ params.get("dataType") collect { case t if t == "html" => html.editcomment(x.content, x.commentId, repository) } getOrElse { @@ -244,7 +249,7 @@ enableAnchor = true, enableLineBreaks = true, enableTaskList = true, - hasWritePermission = isEditable(x.userName, x.repositoryName, x.commentedUserName) + hasWritePermission = true ) ) ) @@ -253,32 +258,32 @@ } getOrElse NotFound() }) - ajaxPost("/:owner/:repository/issues/new/label")(collaboratorsOnly { repository => + ajaxPost("/:owner/:repository/issues/new/label")(writableUsersOnly { repository => val labelNames = params("labelNames").split(",") val labels = getLabels(repository.owner, repository.name).filter(x => labelNames.contains(x.labelName)) html.labellist(labels) }) - ajaxPost("/:owner/:repository/issues/:id/label/new")(collaboratorsOnly { repository => + ajaxPost("/:owner/:repository/issues/:id/label/new")(writableUsersOnly { repository => defining(params("id").toInt){ issueId => registerIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt) html.labellist(getIssueLabels(repository.owner, repository.name, issueId)) } }) - ajaxPost("/:owner/:repository/issues/:id/label/delete")(collaboratorsOnly { repository => + ajaxPost("/:owner/:repository/issues/:id/label/delete")(writableUsersOnly { repository => defining(params("id").toInt){ issueId => deleteIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt) html.labellist(getIssueLabels(repository.owner, repository.name, issueId)) } }) - ajaxPost("/:owner/:repository/issues/:id/assign")(collaboratorsOnly { repository => + ajaxPost("/:owner/:repository/issues/:id/assign")(writableUsersOnly { repository => updateAssignedUserName(repository.owner, repository.name, params("id").toInt, assignedUserName("assignedUserName")) Ok("updated") }) - ajaxPost("/:owner/:repository/issues/:id/milestone")(collaboratorsOnly { repository => + ajaxPost("/:owner/:repository/issues/:id/milestone")(writableUsersOnly { repository => updateMilestoneId(repository.owner, repository.name, params("id").toInt, milestoneId("milestoneId")) milestoneId("milestoneId").map { milestoneId => getMilestonesWithIssueCount(repository.owner, repository.name) @@ -288,7 +293,7 @@ } getOrElse Ok() }) - post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository => + post("/:owner/:repository/issues/batchedit/state")(writableUsersOnly { repository => defining(params.get("value")){ action => action match { case Some("open") => executeBatch(repository) { issueId => @@ -306,7 +311,7 @@ } }) - post("/:owner/:repository/issues/batchedit/label")(collaboratorsOnly { repository => + post("/:owner/:repository/issues/batchedit/label")(writableUsersOnly { repository => params("value").toIntOpt.map{ labelId => executeBatch(repository) { issueId => getIssueLabel(repository.owner, repository.name, issueId, labelId) getOrElse { @@ -316,7 +321,7 @@ } getOrElse NotFound() }) - post("/:owner/:repository/issues/batchedit/assign")(collaboratorsOnly { repository => + post("/:owner/:repository/issues/batchedit/assign")(writableUsersOnly { repository => defining(assignedUserName("value")){ value => executeBatch(repository) { updateAssignedUserName(repository.owner, repository.name, _, value) @@ -324,7 +329,7 @@ } }) - post("/:owner/:repository/issues/batchedit/milestone")(collaboratorsOnly { repository => + post("/:owner/:repository/issues/batchedit/milestone")(writableUsersOnly { repository => defining(milestoneId("value")){ value => executeBatch(repository) { updateMilestoneId(repository.owner, repository.name, _, value) @@ -346,9 +351,6 @@ val assignedUserName = (key: String) => params.get(key) filter (_.trim != "") val milestoneId: String => Option[Int] = (key: String) => params.get(key).flatMap(_.toIntOpt) - private def isEditable(owner: String, repository: String, author: String)(implicit context: Context): Boolean = - hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName - private def executeBatch(repository: RepositoryService.RepositoryInfo)(execute: Int => Unit) = { params("checked").split(',') map(_.toInt) foreach execute params("from") match { @@ -359,8 +361,7 @@ private def searchIssues(repository: RepositoryService.RepositoryInfo) = { defining(repository.owner, repository.name){ case (owner, repoName) => - val page = IssueSearchCondition.page(request) - val sessionKey = Keys.Session.Issues(owner, repoName) + val page = IssueSearchCondition.page(request) // retrieve search condition val condition = IssueSearchCondition(request) @@ -369,18 +370,41 @@ "issues", searchIssue(condition, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName), page, - if(!getAccountByUserName(owner).exists(_.isGroupAccount)){ - (getCollaborators(owner, repoName) :+ owner).sorted - } else { - getCollaborators(owner, repoName) - }, + getAssignableUserNames(owner, repoName), getMilestones(owner, repoName), getLabels(owner, repoName), countIssue(condition.copy(state = "open" ), false, owner -> repoName), countIssue(condition.copy(state = "closed"), false, owner -> repoName), condition, repository, - hasWritePermission(owner, repoName, context.loginAccount)) + isEditable(repository), + isManageable(repository)) } } + + /** + * Tests whether an logged-in user can manage issues. + */ + private def isManageable(repository: RepositoryInfo)(implicit context: Context): Boolean = { + hasWritePermission(repository.owner, repository.name, context.loginAccount) + } + + /** + * Tests whether an logged-in user can post issues. + */ + private def isEditable(repository: RepositoryInfo)(implicit context: Context): Boolean = { + repository.repository.options.issuesOption match { + case "PUBLIC" => hasReadPermission(repository.owner, repository.name, context.loginAccount) + case "PRIVATE" => hasWritePermission(repository.owner, repository.name, context.loginAccount) + case "DISABLE" => false + } + } + + /** + * Tests whether an issue or a comment is editable by a logged-in user. + */ + private def isEditableContent(owner: String, repository: String, author: String)(implicit context: Context): Boolean = { + hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName + } + } diff --git a/src/main/scala/gitbucket/core/controller/LabelsController.scala b/src/main/scala/gitbucket/core/controller/LabelsController.scala index 9b17841..ab658eb 100644 --- a/src/main/scala/gitbucket/core/controller/LabelsController.scala +++ b/src/main/scala/gitbucket/core/controller/LabelsController.scala @@ -2,7 +2,7 @@ import gitbucket.core.issues.labels.html import gitbucket.core.service.{RepositoryService, AccountService, IssuesService, LabelsService} -import gitbucket.core.util.{ReferrerAuthenticator, CollaboratorsAuthenticator} +import gitbucket.core.util.{ReferrerAuthenticator, WritableUsersAuthenticator} import gitbucket.core.util.Implicits._ import io.github.gitbucket.scalatra.forms._ import org.scalatra.i18n.Messages @@ -10,11 +10,11 @@ class LabelsController extends LabelsControllerBase with LabelsService with IssuesService with RepositoryService with AccountService -with ReferrerAuthenticator with CollaboratorsAuthenticator +with ReferrerAuthenticator with WritableUsersAuthenticator trait LabelsControllerBase extends ControllerBase { self: LabelsService with IssuesService with RepositoryService - with ReferrerAuthenticator with CollaboratorsAuthenticator => + with ReferrerAuthenticator with WritableUsersAuthenticator => case class LabelForm(labelName: String, color: String) @@ -32,11 +32,11 @@ hasWritePermission(repository.owner, repository.name, context.loginAccount)) }) - ajaxGet("/:owner/:repository/issues/labels/new")(collaboratorsOnly { repository => + ajaxGet("/:owner/:repository/issues/labels/new")(writableUsersOnly { repository => html.edit(None, repository) }) - ajaxPost("/:owner/:repository/issues/labels/new", labelForm)(collaboratorsOnly { (form, repository) => + ajaxPost("/:owner/:repository/issues/labels/new", labelForm)(writableUsersOnly { (form, repository) => val labelId = createLabel(repository.owner, repository.name, form.labelName, form.color.substring(1)) html.label( getLabel(repository.owner, repository.name, labelId).get, @@ -46,13 +46,13 @@ hasWritePermission(repository.owner, repository.name, context.loginAccount)) }) - ajaxGet("/:owner/:repository/issues/labels/:labelId/edit")(collaboratorsOnly { repository => + ajaxGet("/:owner/:repository/issues/labels/:labelId/edit")(writableUsersOnly { repository => getLabel(repository.owner, repository.name, params("labelId").toInt).map { label => html.edit(Some(label), repository) } getOrElse NotFound() }) - ajaxPost("/:owner/:repository/issues/labels/:labelId/edit", labelForm)(collaboratorsOnly { (form, repository) => + ajaxPost("/:owner/:repository/issues/labels/:labelId/edit", labelForm)(writableUsersOnly { (form, repository) => updateLabel(repository.owner, repository.name, params("labelId").toInt, form.labelName, form.color.substring(1)) html.label( getLabel(repository.owner, repository.name, params("labelId").toInt).get, @@ -62,7 +62,7 @@ hasWritePermission(repository.owner, repository.name, context.loginAccount)) }) - ajaxPost("/:owner/:repository/issues/labels/:labelId/delete")(collaboratorsOnly { repository => + ajaxPost("/:owner/:repository/issues/labels/:labelId/delete")(writableUsersOnly { repository => deleteLabel(repository.owner, repository.name, params("labelId").toInt) Ok() }) diff --git a/src/main/scala/gitbucket/core/controller/MilestonesController.scala b/src/main/scala/gitbucket/core/controller/MilestonesController.scala index 323b114..f75ea60 100644 --- a/src/main/scala/gitbucket/core/controller/MilestonesController.scala +++ b/src/main/scala/gitbucket/core/controller/MilestonesController.scala @@ -2,17 +2,17 @@ import gitbucket.core.issues.milestones.html import gitbucket.core.service.{RepositoryService, MilestonesService, AccountService} -import gitbucket.core.util.{ReferrerAuthenticator, CollaboratorsAuthenticator} +import gitbucket.core.util.{ReferrerAuthenticator, WritableUsersAuthenticator} import gitbucket.core.util.Implicits._ import io.github.gitbucket.scalatra.forms._ class MilestonesController extends MilestonesControllerBase with MilestonesService with RepositoryService with AccountService - with ReferrerAuthenticator with CollaboratorsAuthenticator + with ReferrerAuthenticator with WritableUsersAuthenticator trait MilestonesControllerBase extends ControllerBase { self: MilestonesService with RepositoryService - with ReferrerAuthenticator with CollaboratorsAuthenticator => + with ReferrerAuthenticator with WritableUsersAuthenticator => case class MilestoneForm(title: String, description: Option[String], dueDate: Option[java.util.Date]) @@ -30,22 +30,22 @@ hasWritePermission(repository.owner, repository.name, context.loginAccount)) }) - get("/:owner/:repository/issues/milestones/new")(collaboratorsOnly { + get("/:owner/:repository/issues/milestones/new")(writableUsersOnly { html.edit(None, _) }) - post("/:owner/:repository/issues/milestones/new", milestoneForm)(collaboratorsOnly { (form, repository) => + post("/:owner/:repository/issues/milestones/new", milestoneForm)(writableUsersOnly { (form, repository) => createMilestone(repository.owner, repository.name, form.title, form.description, form.dueDate) redirect(s"/${repository.owner}/${repository.name}/issues/milestones") }) - get("/:owner/:repository/issues/milestones/:milestoneId/edit")(collaboratorsOnly { repository => + get("/:owner/:repository/issues/milestones/:milestoneId/edit")(writableUsersOnly { repository => params("milestoneId").toIntOpt.map{ milestoneId => html.edit(getMilestone(repository.owner, repository.name, milestoneId), repository) } getOrElse NotFound() }) - post("/:owner/:repository/issues/milestones/:milestoneId/edit", milestoneForm)(collaboratorsOnly { (form, repository) => + post("/:owner/:repository/issues/milestones/:milestoneId/edit", milestoneForm)(writableUsersOnly { (form, repository) => params("milestoneId").toIntOpt.flatMap{ milestoneId => getMilestone(repository.owner, repository.name, milestoneId).map { milestone => updateMilestone(milestone.copy(title = form.title, description = form.description, dueDate = form.dueDate)) @@ -54,7 +54,7 @@ } getOrElse NotFound() }) - get("/:owner/:repository/issues/milestones/:milestoneId/close")(collaboratorsOnly { repository => + get("/:owner/:repository/issues/milestones/:milestoneId/close")(writableUsersOnly { repository => params("milestoneId").toIntOpt.flatMap{ milestoneId => getMilestone(repository.owner, repository.name, milestoneId).map { milestone => closeMilestone(milestone) @@ -63,7 +63,7 @@ } getOrElse NotFound() }) - get("/:owner/:repository/issues/milestones/:milestoneId/open")(collaboratorsOnly { repository => + get("/:owner/:repository/issues/milestones/:milestoneId/open")(writableUsersOnly { repository => params("milestoneId").toIntOpt.flatMap{ milestoneId => getMilestone(repository.owner, repository.name, milestoneId).map { milestone => openMilestone(milestone) @@ -72,7 +72,7 @@ } getOrElse NotFound() }) - get("/:owner/:repository/issues/milestones/:milestoneId/delete")(collaboratorsOnly { repository => + get("/:owner/:repository/issues/milestones/:milestoneId/delete")(writableUsersOnly { repository => params("milestoneId").toIntOpt.flatMap{ milestoneId => getMilestone(repository.owner, repository.name, milestoneId).map { milestone => deleteMilestone(repository.owner, repository.name, milestone.milestoneId) diff --git a/src/main/scala/gitbucket/core/controller/PullRequestsController.scala b/src/main/scala/gitbucket/core/controller/PullRequestsController.scala index f3bd241..548da42 100644 --- a/src/main/scala/gitbucket/core/controller/PullRequestsController.scala +++ b/src/main/scala/gitbucket/core/controller/PullRequestsController.scala @@ -6,6 +6,7 @@ import gitbucket.core.service.MergeService import gitbucket.core.service.IssuesService._ import gitbucket.core.service.PullRequestService._ +import gitbucket.core.service.RepositoryService.RepositoryInfo import gitbucket.core.service._ import gitbucket.core.util.ControlUtil._ import gitbucket.core.util.Directory._ @@ -14,28 +15,26 @@ import gitbucket.core.util._ import gitbucket.core.view import gitbucket.core.view.helpers - import io.github.gitbucket.scalatra.forms._ import org.eclipse.jgit.api.Git import org.eclipse.jgit.lib.PersonIdent -import org.slf4j.LoggerFactory import scala.collection.JavaConverters._ class PullRequestsController extends PullRequestsControllerBase with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with LabelsService - with CommitsService with ActivityService with WebHookPullRequestService with ReferrerAuthenticator with CollaboratorsAuthenticator + with CommitsService with ActivityService with WebHookPullRequestService + with ReadableUsersAuthenticator with ReferrerAuthenticator with WritableUsersAuthenticator with CommitStatusService with MergeService with ProtectedBranchService trait PullRequestsControllerBase extends ControllerBase { self: RepositoryService with AccountService with IssuesService with MilestonesService with LabelsService - with CommitsService with ActivityService with PullRequestService with WebHookPullRequestService with ReferrerAuthenticator with CollaboratorsAuthenticator + with CommitsService with ActivityService with PullRequestService with WebHookPullRequestService + with ReadableUsersAuthenticator with ReferrerAuthenticator with WritableUsersAuthenticator with CommitStatusService with MergeService with ProtectedBranchService => - private val logger = LoggerFactory.getLogger(classOf[PullRequestsControllerBase]) - val pullRequestForm = mapping( "title" -> trim(label("Title" , text(required, maxlength(100)))), "content" -> trim(label("Content", optional(text()))), @@ -94,12 +93,13 @@ (commits.flatten.map(commit => getCommitComments(owner, name, commit.id, true)).flatten.toList ::: getComments(owner, name, issueId)) .sortWith((a, b) => a.registeredDate before b.registeredDate), getIssueLabels(owner, name, issueId), - (getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted, + getAssignableUserNames(owner, name), getMilestonesWithIssueCount(owner, name), getLabels(owner, name), commits, diffs, - hasWritePermission(owner, name, context.loginAccount), + isEditable(repository), + isManageable(repository), repository, flash.toMap.map(f => f._1 -> f._2.toString)) } @@ -141,7 +141,7 @@ } getOrElse NotFound() }) - get("/:owner/:repository/pull/:id/delete/*")(collaboratorsOnly { repository => + get("/:owner/:repository/pull/:id/delete/*")(writableUsersOnly { repository => params("id").toIntOpt.map { issueId => val branchName = multiParams("splat").head val userName = context.loginAccount.get.userName @@ -225,7 +225,7 @@ }) getOrElse NotFound() }) - post("/:owner/:repository/pull/:id/merge", mergeForm)(collaboratorsOnly { (form, repository) => + post("/:owner/:repository/pull/:id/merge", mergeForm)(writableUsersOnly { (form, repository) => params("id").toIntOpt.flatMap { issueId => val owner = repository.owner val name = repository.name @@ -375,7 +375,7 @@ originRepository, forkedRepository, hasWritePermission(originRepository.owner, originRepository.name, context.loginAccount), - (getCollaborators(originRepository.owner, originRepository.name) ::: (if(getAccountByUserName(originRepository.owner).get.isGroupAccount) Nil else List(originRepository.owner))).sorted, + getAssignableUserNames(originRepository.owner, originRepository.name), getMilestones(originRepository.owner, originRepository.name), getLabels(originRepository.owner, originRepository.name) ) @@ -389,7 +389,7 @@ }) getOrElse NotFound() }) - ajaxGet("/:owner/:repository/compare/*...*/mergecheck")(collaboratorsOnly { forkedRepository => + ajaxGet("/:owner/:repository/compare/*...*/mergecheck")(writableUsersOnly { forkedRepository => val Seq(origin, forked) = multiParams("splat") val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, forkedRepository.owner) val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, forkedRepository.owner) @@ -419,64 +419,68 @@ }) getOrElse NotFound() }) - post("/:owner/:repository/pulls/new", pullRequestForm)(referrersOnly { (form, repository) => + post("/:owner/:repository/pulls/new", pullRequestForm)(readableUsersOnly { (form, repository) => defining(repository.owner, repository.name){ case (owner, name) => - val writable = hasWritePermission(owner, name, context.loginAccount) - val loginUserName = context.loginAccount.get.userName + val manageable = isManageable(repository) + val editable = isEditable(repository) - val issueId = createIssue( - owner = repository.owner, - repository = repository.name, - loginUser = loginUserName, - title = form.title, - content = form.content, - assignedUserName = if(writable) form.assignedUserName else None, - milestoneId = if(writable) form.milestoneId else None, - isPullRequest = true) + if(editable) { + val loginUserName = context.loginAccount.get.userName - createPullRequest( - originUserName = repository.owner, - originRepositoryName = repository.name, - issueId = issueId, - originBranch = form.targetBranch, - requestUserName = form.requestUserName, - requestRepositoryName = form.requestRepositoryName, - requestBranch = form.requestBranch, - commitIdFrom = form.commitIdFrom, - commitIdTo = form.commitIdTo) + val issueId = createIssue( + owner = repository.owner, + repository = repository.name, + loginUser = loginUserName, + title = form.title, + content = form.content, + assignedUserName = if (manageable) form.assignedUserName else None, + milestoneId = if (manageable) form.milestoneId else None, + isPullRequest = true) - // insert labels - if(writable){ - form.labelNames.map { value => - val labels = getLabels(owner, name) - value.split(",").foreach { labelName => - labels.find(_.labelName == labelName).map { label => - registerIssueLabel(repository.owner, repository.name, issueId, label.labelId) + createPullRequest( + originUserName = repository.owner, + originRepositoryName = repository.name, + issueId = issueId, + originBranch = form.targetBranch, + requestUserName = form.requestUserName, + requestRepositoryName = form.requestRepositoryName, + requestBranch = form.requestBranch, + commitIdFrom = form.commitIdFrom, + commitIdTo = form.commitIdTo) + + // insert labels + if (manageable) { + form.labelNames.map { value => + val labels = getLabels(owner, name) + value.split(",").foreach { labelName => + labels.find(_.labelName == labelName).map { label => + registerIssueLabel(repository.owner, repository.name, issueId, label.labelId) + } } } } - } - // fetch requested branch - fetchAsPullRequest(owner, name, form.requestUserName, form.requestRepositoryName, form.requestBranch, issueId) + // fetch requested branch + fetchAsPullRequest(owner, name, form.requestUserName, form.requestRepositoryName, form.requestBranch, issueId) - // record activity - recordPullRequestActivity(owner, name, loginUserName, issueId, form.title) + // record activity + recordPullRequestActivity(owner, name, loginUserName, issueId, form.title) - // call web hook - callPullRequestWebHook("opened", repository, issueId, context.baseUrl, context.loginAccount.get) + // call web hook + callPullRequestWebHook("opened", repository, issueId, context.baseUrl, context.loginAccount.get) - getIssue(owner, name, issueId.toString) foreach { issue => - // extract references and create refer comment - createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""), context.loginAccount.get) + getIssue(owner, name, issueId.toString) foreach { issue => + // extract references and create refer comment + createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""), context.loginAccount.get) - // notifications - Notifier().toNotify(repository, issue, form.content.getOrElse("")){ - Notifier.msgPullRequest(s"${context.baseUrl}/${owner}/${name}/pull/${issueId}") + // notifications + Notifier().toNotify(repository, issue, form.content.getOrElse("")) { + Notifier.msgPullRequest(s"${context.baseUrl}/${owner}/${name}/pull/${issueId}") + } } - } - redirect(s"/${owner}/${name}/pull/${issueId}") + redirect(s"/${owner}/${name}/pull/${issueId}") + } else Unauthorized() } }) @@ -516,8 +520,7 @@ private def searchPullRequests(userName: Option[String], repository: RepositoryService.RepositoryInfo) = defining(repository.owner, repository.name){ case (owner, repoName) => - val page = IssueSearchCondition.page(request) - val sessionKey = Keys.Session.Pulls(owner, repoName) + val page = IssueSearchCondition.page(request) // retrieve search condition val condition = IssueSearchCondition(request) @@ -526,18 +529,33 @@ "pulls", searchIssue(condition, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName), page, - if(!getAccountByUserName(owner).exists(_.isGroupAccount)){ - (getCollaborators(owner, repoName) :+ owner).sorted - } else { - getCollaborators(owner, repoName) - }, + getAssignableUserNames(owner, repoName), getMilestones(owner, repoName), getLabels(owner, repoName), countIssue(condition.copy(state = "open" ), true, owner -> repoName), countIssue(condition.copy(state = "closed"), true, owner -> repoName), condition, repository, - hasWritePermission(owner, repoName, context.loginAccount)) + isEditable(repository), + isManageable(repository)) } + /** + * Tests whether an logged-in user can manage pull requests. + */ + private def isManageable(repository: RepositoryInfo)(implicit context: Context): Boolean = { + hasWritePermission(repository.owner, repository.name, context.loginAccount) + } + + /** + * Tests whether an logged-in user can post pull requests. + */ + private def isEditable(repository: RepositoryInfo)(implicit context: Context): Boolean = { + repository.repository.options.issuesOption match { + case "PUBLIC" => hasReadPermission(repository.owner, repository.name, context.loginAccount) + case "PRIVATE" => hasWritePermission(repository.owner, repository.name, context.loginAccount) + case "DISABLE" => false + } + } + } diff --git a/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala b/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala index 41455cc..5a2ca91 100644 --- a/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala +++ b/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala @@ -31,10 +31,9 @@ repositoryName: String, description: Option[String], isPrivate: Boolean, - enableIssues: Boolean, + issuesOption: String, externalIssuesUrl: Option[String], - enableWiki: Boolean, - allowWikiEditing: Boolean, + wikiOption: String, externalWikiUrl: Option[String], allowFork: Boolean ) @@ -43,10 +42,9 @@ "repositoryName" -> trim(label("Repository Name" , text(required, maxlength(40), identifier, renameRepositoryName))), "description" -> trim(label("Description" , optional(text()))), "isPrivate" -> trim(label("Repository Type" , boolean())), - "enableIssues" -> trim(label("Enable Issues" , boolean())), + "issuesOption" -> trim(label("Issues Option" , text(required, featureOption))), "externalIssuesUrl" -> trim(label("External Issues URL", optional(text(maxlength(200))))), - "enableWiki" -> trim(label("Enable Wiki" , boolean())), - "allowWikiEditing" -> trim(label("Allow Wiki Editing" , boolean())), + "wikiOption" -> trim(label("Wiki Option" , text(required, featureOption))), "externalWikiUrl" -> trim(label("External Wiki URL" , optional(text(maxlength(200))))), "allowFork" -> trim(label("Allow Forking" , boolean())) )(OptionsForm.apply) @@ -58,12 +56,12 @@ "defaultBranch" -> trim(label("Default Branch" , text(required, maxlength(100)))) )(DefaultBranchForm.apply) - // for collaborator addition - case class CollaboratorForm(userName: String) - - val collaboratorForm = mapping( - "userName" -> trim(label("Username", text(required, collaborator))) - )(CollaboratorForm.apply) +// // for collaborator addition +// case class CollaboratorForm(userName: String) +// +// val collaboratorForm = mapping( +// "userName" -> trim(label("Username", text(required, collaborator))) +// )(CollaboratorForm.apply) // for web hook url addition case class WebHookForm(url: String, events: Set[WebHook.Event], ctype: WebHookContentType, token: Option[String]) @@ -109,10 +107,9 @@ repository.repository.parentUserName.map { _ => repository.repository.isPrivate } getOrElse form.isPrivate, - form.enableIssues, + form.issuesOption, form.externalIssuesUrl, - form.enableWiki, - form.allowWikiEditing, + form.wikiOption, form.externalWikiUrl, form.allowFork ) @@ -178,22 +175,12 @@ repository) }) - /** - * Add the collaborator. - */ - post("/:owner/:repository/settings/collaborators/add", collaboratorForm)(ownerOnly { (form, repository) => - if(!getAccountByUserName(repository.owner).get.isGroupAccount){ - addCollaborator(repository.owner, repository.name, form.userName) - } - redirect(s"/${repository.owner}/${repository.name}/settings/collaborators") - }) - - /** - * Add the collaborator. - */ - get("/:owner/:repository/settings/collaborators/remove")(ownerOnly { repository => - if(!getAccountByUserName(repository.owner).get.isGroupAccount){ - removeCollaborator(repository.owner, repository.name, params("name")) + post("/:owner/:repository/settings/collaborators")(ownerOnly { repository => + val collaborators = params("collaborators") + removeCollaborators(repository.owner, repository.name) + collaborators.split(",").withFilter(_.nonEmpty).map { collaborator => + val userName :: permission :: Nil = collaborator.split(":").toList + addCollaborator(repository.owner, repository.name, userName, permission) } redirect(s"/${repository.owner}/${repository.name}/settings/collaborators") }) @@ -397,20 +384,20 @@ } } - /** - * Provides Constraint to validate the collaborator name. - */ - private def collaborator: Constraint = new Constraint(){ - override def validate(name: String, value: String, messages: Messages): Option[String] = - getAccountByUserName(value) match { - case None => Some("User does not exist.") - case Some(x) if(x.isGroupAccount) - => Some("User does not exist.") - case Some(x) if(x.userName == params("owner") || getCollaborators(params("owner"), params("repository")).contains(x.userName)) - => Some("User can access this repository already.") - case _ => None - } - } +// /** +// * Provides Constraint to validate the collaborator name. +// */ +// private def collaborator: Constraint = new Constraint(){ +// override def validate(name: String, value: String, messages: Messages): Option[String] = +// getAccountByUserName(value) match { +// case None => Some("User does not exist.") +//// case Some(x) if(x.isGroupAccount) +//// => Some("User does not exist.") +// case Some(x) if(x.userName == params("owner") || getCollaborators(params("owner"), params("repository")).contains(x.userName)) +// => Some(value + " is repository owner.") // TODO also group members? +// case _ => None +// } +// } /** * Duplicate check for the rename repository name. @@ -425,6 +412,15 @@ } /** + * + */ + private def featureOption: Constraint = new Constraint(){ + override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] = + if(Seq("DISABLE", "PRIVATE", "PUBLIC").contains(value)) None else Some("Option is invalid.") + } + + + /** * Provides Constraint to validate the repository transfer user. */ private def transferUser: Constraint = new Constraint(){ diff --git a/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala b/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala index 63b904e..d575c68 100644 --- a/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala +++ b/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala @@ -31,7 +31,7 @@ class RepositoryViewerController extends RepositoryViewerControllerBase with RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with CommitsService - with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with CommitStatusService + with ReadableUsersAuthenticator with ReferrerAuthenticator with WritableUsersAuthenticator with PullRequestService with CommitStatusService with WebHookPullRequestService with WebHookPullRequestReviewCommentService with ProtectedBranchService /** @@ -39,7 +39,7 @@ */ trait RepositoryViewerControllerBase extends ControllerBase { self: RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with CommitsService - with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with CommitStatusService + with ReadableUsersAuthenticator with ReferrerAuthenticator with WritableUsersAuthenticator with PullRequestService with CommitStatusService with WebHookPullRequestService with WebHookPullRequestReviewCommentService with ProtectedBranchService => ArchiveCommand.registerFormat("zip", new ZipFormat) @@ -157,7 +157,7 @@ } }) - get("/:owner/:repository/new/*")(collaboratorsOnly { repository => + get("/:owner/:repository/new/*")(writableUsersOnly { repository => val (branch, path) = repository.splitPath(multiParams("splat").head) val protectedBranch = getProtectedBranchInfo(repository.owner, repository.name, branch).needStatusCheck(context.loginAccount.get.userName) html.editor(branch, repository, if(path.length == 0) Nil else path.split("/").toList, @@ -165,7 +165,7 @@ protectedBranch) }) - get("/:owner/:repository/edit/*")(collaboratorsOnly { repository => + get("/:owner/:repository/edit/*")(writableUsersOnly { repository => val (branch, path) = repository.splitPath(multiParams("splat").head) val protectedBranch = getProtectedBranchInfo(repository.owner, repository.name, branch).needStatusCheck(context.loginAccount.get.userName) @@ -181,7 +181,7 @@ } }) - get("/:owner/:repository/remove/*")(collaboratorsOnly { repository => + get("/:owner/:repository/remove/*")(writableUsersOnly { repository => val (branch, path) = repository.splitPath(multiParams("splat").head) using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(branch)) @@ -194,7 +194,7 @@ } }) - post("/:owner/:repository/create", editorForm)(collaboratorsOnly { (form, repository) => + post("/:owner/:repository/create", editorForm)(writableUsersOnly { (form, repository) => commitFile( repository = repository, branch = form.branch, @@ -211,7 +211,7 @@ }") }) - post("/:owner/:repository/update", editorForm)(collaboratorsOnly { (form, repository) => + post("/:owner/:repository/update", editorForm)(writableUsersOnly { (form, repository) => commitFile( repository = repository, branch = form.branch, @@ -232,7 +232,7 @@ }") }) - post("/:owner/:repository/remove", deleteForm)(collaboratorsOnly { (form, repository) => + post("/:owner/:repository/remove", deleteForm)(writableUsersOnly { (form, repository) => commitFile(repository, form.branch, form.path, None, Some(form.fileName), "", "", form.message.getOrElse(s"Delete ${form.fileName}")) @@ -443,7 +443,7 @@ /** * Creates a branch. */ - post("/:owner/:repository/branches")(collaboratorsOnly { repository => + post("/:owner/:repository/branches")(writableUsersOnly { repository => val newBranchName = params.getOrElse("new", halt(400)) val fromBranchName = params.getOrElse("from", halt(400)) using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => @@ -461,7 +461,7 @@ /** * Deletes branch. */ - get("/:owner/:repository/delete/*")(collaboratorsOnly { repository => + get("/:owner/:repository/delete/*")(writableUsersOnly { repository => val branchName = multiParams("splat").head val userName = context.loginAccount.get.userName if(repository.repository.defaultBranch != branchName){ diff --git a/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala b/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala index 7a68746..bfca858 100644 --- a/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala +++ b/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala @@ -279,13 +279,13 @@ } else { // Update GROUP_MEMBER updateGroupMembers(form.groupName, members) - // Update COLLABORATOR for group repositories - getRepositoryNamesOfUser(form.groupName).foreach { repositoryName => - removeCollaborators(form.groupName, repositoryName) - members.foreach { case (userName, isManager) => - addCollaborator(form.groupName, repositoryName, userName) - } - } +// // Update COLLABORATOR for group repositories +// getRepositoryNamesOfUser(form.groupName).foreach { repositoryName => +// removeCollaborators(form.groupName, repositoryName) +// members.foreach { case (userName, isManager) => +// addCollaborator(form.groupName, repositoryName, userName) +// } +// } } updateImage(form.groupName, form.fileId, form.clearImage) diff --git a/src/main/scala/gitbucket/core/controller/WikiController.scala b/src/main/scala/gitbucket/core/controller/WikiController.scala index 29f0569..64d0b27 100644 --- a/src/main/scala/gitbucket/core/controller/WikiController.scala +++ b/src/main/scala/gitbucket/core/controller/WikiController.scala @@ -14,10 +14,10 @@ class WikiController extends WikiControllerBase with WikiService with RepositoryService with AccountService with ActivityService - with CollaboratorsAuthenticator with ReferrerAuthenticator + with ReadableUsersAuthenticator with ReferrerAuthenticator trait WikiControllerBase extends ControllerBase { - self: WikiService with RepositoryService with ActivityService with CollaboratorsAuthenticator with ReferrerAuthenticator => + self: WikiService with RepositoryService with ActivityService with ReadableUsersAuthenticator with ReferrerAuthenticator => case class WikiPageEditForm(pageName: String, content: String, message: Option[String], currentPageName: String, id: String) @@ -62,7 +62,7 @@ using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git => JGitUtil.getCommitLog(git, "master", path = pageName + ".md") match { - case Right((logs, hasNext)) => html.history(Some(pageName), logs, repository) + case Right((logs, hasNext)) => html.history(Some(pageName), logs, repository, isEditable(repository)) case Left(_) => NotFound() } } @@ -87,7 +87,7 @@ } }) - get("/:owner/:repository/wiki/:page/_revert/:commitId")(referrersOnly { repository => + get("/:owner/:repository/wiki/:page/_revert/:commitId")(readableUsersOnly { repository => if(isEditable(repository)){ val pageName = StringUtil.urlDecode(params("page")) val Array(from, to) = params("commitId").split("\\.\\.\\.") @@ -101,7 +101,7 @@ } else Unauthorized() }) - get("/:owner/:repository/wiki/_revert/:commitId")(referrersOnly { repository => + get("/:owner/:repository/wiki/_revert/:commitId")(readableUsersOnly { repository => if(isEditable(repository)){ val Array(from, to) = params("commitId").split("\\.\\.\\.") @@ -114,14 +114,14 @@ } else Unauthorized() }) - get("/:owner/:repository/wiki/:page/_edit")(referrersOnly { repository => + get("/:owner/:repository/wiki/:page/_edit")(readableUsersOnly { repository => if(isEditable(repository)){ val pageName = StringUtil.urlDecode(params("page")) html.edit(pageName, getWikiPage(repository.owner, repository.name, pageName), repository) } else Unauthorized() }) - post("/:owner/:repository/wiki/_edit", editForm)(referrersOnly { (form, repository) => + post("/:owner/:repository/wiki/_edit", editForm)(readableUsersOnly { (form, repository) => if(isEditable(repository)){ defining(context.loginAccount.get){ loginAccount => saveWikiPage( @@ -146,13 +146,13 @@ } else Unauthorized() }) - get("/:owner/:repository/wiki/_new")(referrersOnly { repository => + get("/:owner/:repository/wiki/_new")(readableUsersOnly { repository => if(isEditable(repository)){ html.edit("", None, repository) } else Unauthorized() }) - post("/:owner/:repository/wiki/_new", newForm)(referrersOnly { (form, repository) => + post("/:owner/:repository/wiki/_new", newForm)(readableUsersOnly { (form, repository) => if(isEditable(repository)){ defining(context.loginAccount.get){ loginAccount => saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName, @@ -170,7 +170,7 @@ } else Unauthorized() }) - get("/:owner/:repository/wiki/:page/_delete")(referrersOnly { repository => + get("/:owner/:repository/wiki/:page/_delete")(readableUsersOnly { repository => if(isEditable(repository)){ val pageName = StringUtil.urlDecode(params("page")) @@ -182,7 +182,7 @@ } } else Unauthorized() }) - + get("/:owner/:repository/wiki/_pages")(referrersOnly { repository => html.pages(getWikiPageList(repository.owner, repository.name), repository, isEditable(repository)) }) @@ -190,7 +190,7 @@ get("/:owner/:repository/wiki/_history")(referrersOnly { repository => using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git => JGitUtil.getCommitLog(git, "master") match { - case Right((logs, hasNext)) => html.history(None, logs, repository) + case Right((logs, hasNext)) => html.history(None, logs, repository, isEditable(repository)) case Left(_) => NotFound() } } @@ -240,9 +240,13 @@ private def targetWikiPage = getWikiPage(params("owner"), params("repository"), params("pageName")) - private def isEditable(repository: RepositoryInfo)(implicit context: Context): Boolean = - repository.repository.options.allowWikiEditing || ( - hasWritePermission(repository.owner, repository.name, context.loginAccount) - ) + private def isEditable(repository: RepositoryInfo)(implicit context: Context): Boolean = { + repository.repository.options.wikiOption match { +// case "ALL" => repository.repository.isPrivate == false || hasReadPermission(repository.owner, repository.name, context.loginAccount) + case "PUBLIC" => hasReadPermission(repository.owner, repository.name, context.loginAccount) + case "PRIVATE" => hasWritePermission(repository.owner, repository.name, context.loginAccount) + case "DISABLE" => false + } + } } diff --git a/src/main/scala/gitbucket/core/model/Collaborator.scala b/src/main/scala/gitbucket/core/model/Collaborator.scala index 55ae80f..5036e3a 100644 --- a/src/main/scala/gitbucket/core/model/Collaborator.scala +++ b/src/main/scala/gitbucket/core/model/Collaborator.scala @@ -7,7 +7,8 @@ class Collaborators(tag: Tag) extends Table[Collaborator](tag, "COLLABORATOR") with BasicTemplate { val collaboratorName = column[String]("COLLABORATOR_NAME") - def * = (userName, repositoryName, collaboratorName) <> (Collaborator.tupled, Collaborator.unapply) + val permission = column[String]("PERMISSION") + def * = (userName, repositoryName, collaboratorName, permission) <> (Collaborator.tupled, Collaborator.unapply) def byPrimaryKey(owner: String, repository: String, collaborator: String) = byRepository(owner, repository) && (collaboratorName === collaborator.bind) @@ -17,5 +18,23 @@ case class Collaborator( userName: String, repositoryName: String, - collaboratorName: String + collaboratorName: String, + permission: String ) + +sealed abstract class Permission(val name: String) + +object Permission { + object ADMIN extends Permission("ADMIN") + object WRITE extends Permission("WRITE") + object READ extends Permission("READ") + +// val values: Vector[Permission] = Vector(ADMIN, WRITE, READ) +// +// private val map: Map[String, Permission] = values.map(enum => enum.name -> enum).toMap +// +// def apply(name: String): Permission = map(name) +// +// def valueOf(name: String): Option[Permission] = map.get(name) + +} \ No newline at end of file diff --git a/src/main/scala/gitbucket/core/model/Repository.scala b/src/main/scala/gitbucket/core/model/Repository.scala index c5c61bb..387b8ff 100644 --- a/src/main/scala/gitbucket/core/model/Repository.scala +++ b/src/main/scala/gitbucket/core/model/Repository.scala @@ -17,17 +17,16 @@ val originRepositoryName = column[String]("ORIGIN_REPOSITORY_NAME") val parentUserName = column[String]("PARENT_USER_NAME") val parentRepositoryName = column[String]("PARENT_REPOSITORY_NAME") - val enableIssues = column[Boolean]("ENABLE_ISSUES") + val issuesOption = column[String]("ISSUES_OPTION") val externalIssuesUrl = column[String]("EXTERNAL_ISSUES_URL") - val enableWiki = column[Boolean]("ENABLE_WIKI") - val allowWikiEditing = column[Boolean]("ALLOW_WIKI_EDITING") + val wikiOption = column[String]("WIKI_OPTION") val externalWikiUrl = column[String]("EXTERNAL_WIKI_URL") val allowFork = column[Boolean]("ALLOW_FORK") def * = ( (userName, repositoryName, isPrivate, description.?, defaultBranch, registeredDate, updatedDate, lastActivityDate, originUserName.?, originRepositoryName.?, parentUserName.?, parentRepositoryName.?), - (enableIssues, externalIssuesUrl.?, enableWiki, allowWikiEditing, externalWikiUrl.?, allowFork) + (issuesOption, externalIssuesUrl.?, wikiOption, externalWikiUrl.?, allowFork) ).shaped <> ( { case (repository, options) => Repository( @@ -85,10 +84,9 @@ ) case class RepositoryOptions( - enableIssues: Boolean, + issuesOption: String, externalIssuesUrl: Option[String], - enableWiki: Boolean, - allowWikiEditing: Boolean, + wikiOption: String, externalWikiUrl: Option[String], allowFork: Boolean ) diff --git a/src/main/scala/gitbucket/core/service/HandleCommentService.scala b/src/main/scala/gitbucket/core/service/HandleCommentService.scala index 6a1b068..22cfc88 100644 --- a/src/main/scala/gitbucket/core/service/HandleCommentService.scala +++ b/src/main/scala/gitbucket/core/service/HandleCommentService.scala @@ -13,7 +13,7 @@ with WebHookService with WebHookIssueCommentService with WebHookPullRequestService => /** - * @see [[https://github.com/takezoe/gitbucket/wiki/CommentAction]] + * @see [[https://github.com/gitbucket/gitbucket/wiki/CommentAction]] */ def handleComment(issue: Issue, content: Option[String], repository: RepositoryService.RepositoryInfo, actionOpt: Option[String]) (implicit context: Context, s: Session) = { @@ -54,18 +54,20 @@ // call web hooks action match { - case None => commentId.map{ commentIdSome => callIssueCommentWebHook(repository, issue, commentIdSome, context.loginAccount.get) } - case Some(act) => val webHookAction = act match { - case "open" => "opened" - case "reopen" => "reopened" - case "close" => "closed" - case _ => act - } - if(issue.isPullRequest){ + case None => commentId.map { commentIdSome => callIssueCommentWebHook(repository, issue, commentIdSome, context.loginAccount.get) } + case Some(act) => { + val webHookAction = act match { + case "open" => "opened" + case "reopen" => "reopened" + case "close" => "closed" + case _ => act + } + if (issue.isPullRequest) { callPullRequestWebHook(webHookAction, repository, issue.issueId, context.baseUrl, context.loginAccount.get) } else { callIssuesWebHook(webHookAction, repository, issue, context.baseUrl, context.loginAccount.get) } + } } // notifications diff --git a/src/main/scala/gitbucket/core/service/IssuesService.scala b/src/main/scala/gitbucket/core/service/IssuesService.scala index b9566a0..ecbfa55 100644 --- a/src/main/scala/gitbucket/core/service/IssuesService.scala +++ b/src/main/scala/gitbucket/core/service/IssuesService.scala @@ -14,7 +14,7 @@ trait IssuesService { - self: AccountService => + self: AccountService with RepositoryService => import IssuesService._ def getIssue(owner: String, repository: String, issueId: String)(implicit s: Session) = @@ -433,6 +433,11 @@ } } + def getAssignableUserNames(owner: String, repository: String)(implicit s: Session): List[String] = { + (getCollaboratorUserNames(owner, repository, Seq(Permission.ADMIN, Permission.WRITE)) ::: + (if (getAccountByUserName(owner).get.isGroupAccount) getGroupMembers(owner).map(_.userName) else List(owner))).sorted + } + } object IssuesService { diff --git a/src/main/scala/gitbucket/core/service/RepositoryCreationService.scala b/src/main/scala/gitbucket/core/service/RepositoryCreationService.scala index 90e0afd..04aef50 100644 --- a/src/main/scala/gitbucket/core/service/RepositoryCreationService.scala +++ b/src/main/scala/gitbucket/core/service/RepositoryCreationService.scala @@ -21,12 +21,12 @@ // Insert to the database at first insertRepository(name, owner, description, isPrivate) - // Add collaborators for group repository - if(ownerAccount.isGroupAccount){ - getGroupMembers(owner).foreach { member => - addCollaborator(owner, name, member.userName) - } - } +// // Add collaborators for group repository +// if(ownerAccount.isGroupAccount){ +// getGroupMembers(owner).foreach { member => +// addCollaborator(owner, name, member.userName) +// } +// } // Insert default labels insertDefaultLabels(owner, name) diff --git a/src/main/scala/gitbucket/core/service/RepositoryService.scala b/src/main/scala/gitbucket/core/service/RepositoryService.scala index 3541c07..26fa159 100644 --- a/src/main/scala/gitbucket/core/service/RepositoryService.scala +++ b/src/main/scala/gitbucket/core/service/RepositoryService.scala @@ -1,7 +1,7 @@ package gitbucket.core.service import gitbucket.core.controller.Context -import gitbucket.core.model.{Collaborator, Repository, RepositoryOptions, Account} +import gitbucket.core.model.{Collaborator, Repository, RepositoryOptions, Account, Permission} import gitbucket.core.model.Profile._ import gitbucket.core.util.JGitUtil import profile.simple._ @@ -38,10 +38,9 @@ parentUserName = parentUserName, parentRepositoryName = parentRepositoryName, options = RepositoryOptions( - enableIssues = true, + issuesOption = "PUBLIC", // TODO DISABLE for the forked repository? externalIssuesUrl = None, - enableWiki = true, - allowWikiEditing = true, + wikiOption = "PUBLIC", // TODO DISABLE for the forked repository? externalWikiUrl = None, allowFork = true ) @@ -124,11 +123,8 @@ repositoryName = newRepositoryName )) :_*) - if(account.isGroupAccount){ - Collaborators.insertAll(getGroupMembers(newUserName).map(m => Collaborator(newUserName, newRepositoryName, m.userName)) :_*) - } else { - Collaborators.insertAll(collaborators.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) - } + // TODO Drop transfered owner from collaborators? + Collaborators.insertAll(collaborators.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) // Update activity messages Activities.filter { t => @@ -323,12 +319,12 @@ */ def saveRepositoryOptions(userName: String, repositoryName: String, description: Option[String], isPrivate: Boolean, - enableIssues: Boolean, externalIssuesUrl: Option[String], - enableWiki: Boolean, allowWikiEditing: Boolean, externalWikiUrl: Option[String], + issuesOption: String, externalIssuesUrl: Option[String], + wikiOption: String, externalWikiUrl: Option[String], allowFork: Boolean)(implicit s: Session): Unit = Repositories.filter(_.byRepository(userName, repositoryName)) - .map { r => (r.description.?, r.isPrivate, r.enableIssues, r.externalIssuesUrl.?, r.enableWiki, r.allowWikiEditing, r.externalWikiUrl.?, r.allowFork, r.updatedDate) } - .update (description, isPrivate, enableIssues, externalIssuesUrl, enableWiki, allowWikiEditing, externalWikiUrl, allowFork, currentDate) + .map { r => (r.description.?, r.isPrivate, r.issuesOption, r.externalIssuesUrl.?, r.wikiOption, r.externalWikiUrl.?, r.allowFork, r.updatedDate) } + .update (description, isPrivate, issuesOption, externalIssuesUrl, wikiOption, externalWikiUrl, allowFork, currentDate) def saveRepositoryDefaultBranch(userName: String, repositoryName: String, defaultBranch: String)(implicit s: Session): Unit = @@ -337,49 +333,64 @@ .update (defaultBranch) /** - * Add collaborator to the repository. - * - * @param userName the user name of the repository owner - * @param repositoryName the repository name - * @param collaboratorName the collaborator name + * Add collaborator (user or group) to the repository. */ - def addCollaborator(userName: String, repositoryName: String, collaboratorName: String)(implicit s: Session): Unit = - Collaborators insert Collaborator(userName, repositoryName, collaboratorName) - - /** - * Remove collaborator from the repository. - * - * @param userName the user name of the repository owner - * @param repositoryName the repository name - * @param collaboratorName the collaborator name - */ - def removeCollaborator(userName: String, repositoryName: String, collaboratorName: String)(implicit s: Session): Unit = - Collaborators.filter(_.byPrimaryKey(userName, repositoryName, collaboratorName)).delete + def addCollaborator(userName: String, repositoryName: String, collaboratorName: String, permission: String)(implicit s: Session): Unit = + Collaborators insert Collaborator(userName, repositoryName, collaboratorName, permission) /** * Remove all collaborators from the repository. - * - * @param userName the user name of the repository owner - * @param repositoryName the repository name */ def removeCollaborators(userName: String, repositoryName: String)(implicit s: Session): Unit = Collaborators.filter(_.byRepository(userName, repositoryName)).delete /** - * Returns the list of collaborators name which is sorted with ascending order. - * - * @param userName the user name of the repository owner - * @param repositoryName the repository name - * @return the list of collaborators name + * Returns the list of collaborators name (user name or group name) which is sorted with ascending order. */ - def getCollaborators(userName: String, repositoryName: String)(implicit s: Session): List[String] = - Collaborators.filter(_.byRepository(userName, repositoryName)).sortBy(_.collaboratorName).map(_.collaboratorName).list + def getCollaborators(userName: String, repositoryName: String)(implicit s: Session): List[(Collaborator, Boolean)] = + Collaborators + .innerJoin(Accounts).on(_.collaboratorName === _.userName) + .filter { case (t1, t2) => t1.byRepository(userName, repositoryName) } + .map { case (t1, t2) => (t1, t2.groupAccount) } + .sortBy { case (t1, t2) => t1.collaboratorName } + .list + + /** + * Returns the list of all collaborator name and permission which is sorted with ascending order. + * If a group is added as a collaborator, this method returns users who are belong to that group. + */ + def getCollaboratorUserNames(userName: String, repositoryName: String, filter: Seq[Permission] = Nil)(implicit s: Session): List[String] = { + val q1 = Collaborators + .innerJoin(Accounts).on { case (t1, t2) => (t1.collaboratorName === t2.userName) && (t2.groupAccount === false.bind) } + .filter { case (t1, t2) => t1.byRepository(userName, repositoryName) } + .map { case (t1, t2) => (t1.collaboratorName, t1.permission) } + + val q2 = Collaborators + .innerJoin(Accounts).on { case (t1, t2) => (t1.collaboratorName === t2.userName) && (t2.groupAccount === true.bind) } + .innerJoin(GroupMembers).on { case ((t1, t2), t3) => t2.userName === t3.groupName } + .filter { case ((t1, t2), t3) => t1.byRepository(userName, repositoryName) } + .map { case ((t1, t2), t3) => (t3.userName, t1.permission) } + + q1.union(q2).list.filter { x => filter.isEmpty || filter.exists(_.name == x._2) }.map(_._1) + } + def hasWritePermission(owner: String, repository: String, loginAccount: Option[Account])(implicit s: Session): Boolean = { loginAccount match { case Some(a) if(a.isAdmin) => true case Some(a) if(a.userName == owner) => true - case Some(a) if(getCollaborators(owner, repository).contains(a.userName)) => true + case Some(a) if(getGroupMembers(owner).exists(_.userName == a.userName)) => true + case Some(a) if(getCollaboratorUserNames(owner, repository, Seq(Permission.ADMIN, Permission.WRITE)).contains(a.userName)) => true + case _ => false + } + } + + def hasReadPermission(owner: String, repository: String, loginAccount: Option[Account])(implicit s: Session): Boolean = { + loginAccount match { + case Some(a) if(a.isAdmin) => true + case Some(a) if(a.userName == owner) => true + case Some(a) if(getGroupMembers(owner).exists(_.userName == a.userName)) => true + case Some(a) if(getCollaboratorUserNames(owner, repository, Seq(Permission.ADMIN, Permission.WRITE, Permission.READ)).contains(a.userName)) => true case _ => false } } diff --git a/src/main/scala/gitbucket/core/service/RequestCache.scala b/src/main/scala/gitbucket/core/service/RequestCache.scala index 768a3b4..bd03cd8 100644 --- a/src/main/scala/gitbucket/core/service/RequestCache.scala +++ b/src/main/scala/gitbucket/core/service/RequestCache.scala @@ -11,7 +11,7 @@ * It may be called many times in one request, so each method stores * its result into the cache which available during a request. */ -trait RequestCache extends SystemSettingsService with AccountService with IssuesService { +trait RequestCache extends SystemSettingsService with AccountService with IssuesService with RepositoryService { private implicit def context2Session(implicit context: Context): Session = request2Session(context.request) diff --git a/src/main/scala/gitbucket/core/util/Authenticator.scala b/src/main/scala/gitbucket/core/util/Authenticator.scala index 0035494..b4cfef5 100644 --- a/src/main/scala/gitbucket/core/util/Authenticator.scala +++ b/src/main/scala/gitbucket/core/util/Authenticator.scala @@ -1,11 +1,14 @@ package gitbucket.core.util import gitbucket.core.controller.ControllerBase -import gitbucket.core.service.{RepositoryService, AccountService} +import gitbucket.core.service.{AccountService, RepositoryService} +import gitbucket.core.model.Permission import RepositoryService.RepositoryInfo import Implicits._ import ControlUtil._ +import scala.collection.Searching.search + /** * Allows only oneself and administrators. */ @@ -40,9 +43,9 @@ context.loginAccount match { case Some(x) if(x.isAdmin) => action(repository) case Some(x) if(repository.owner == x.userName) => action(repository) - case Some(x) if(getGroupMembers(repository.owner).exists { member => - member.userName == x.userName && member.isManager == true - }) => action(repository) + // TODO Repository management is allowed for only group managers? + case Some(x) if(getGroupMembers(repository.owner).exists { m => m.userName == x.userName && m.isManager == true }) => action(repository) + case Some(x) if(getCollaboratorUserNames(paths(0), paths(1), Seq(Permission.ADMIN)).contains(x.userName)) => action(repository) case _ => Unauthorized() } } getOrElse NotFound() @@ -86,32 +89,9 @@ } /** - * Allows only collaborators and administrators. + * Allows only guests and signed in users who can access the repository. */ -trait CollaboratorsAuthenticator { self: ControllerBase with RepositoryService => - protected def collaboratorsOnly(action: (RepositoryInfo) => Any) = { authenticate(action) } - protected def collaboratorsOnly[T](action: (T, RepositoryInfo) => Any) = (form: T) => { authenticate(action(form, _)) } - - private def authenticate(action: (RepositoryInfo) => Any) = { - { - defining(request.paths){ paths => - getRepository(paths(0), paths(1)).map { repository => - context.loginAccount match { - case Some(x) if(x.isAdmin) => action(repository) - case Some(x) if(paths(0) == x.userName) => action(repository) - case Some(x) if(getCollaborators(paths(0), paths(1)).contains(x.userName)) => action(repository) - case _ => Unauthorized() - } - } getOrElse NotFound() - } - } - } -} - -/** - * Allows only the repository owner (or manager for group repository) and administrators. - */ -trait ReferrerAuthenticator { self: ControllerBase with RepositoryService => +trait ReferrerAuthenticator { self: ControllerBase with RepositoryService with AccountService => protected def referrersOnly(action: (RepositoryInfo) => Any) = { authenticate(action) } protected def referrersOnly[T](action: (T, RepositoryInfo) => Any) = (form: T) => { authenticate(action(form, _)) } @@ -125,7 +105,8 @@ context.loginAccount match { case Some(x) if(x.isAdmin) => action(repository) case Some(x) if(paths(0) == x.userName) => action(repository) - case Some(x) if(getCollaborators(paths(0), paths(1)).contains(x.userName)) => action(repository) + case Some(x) if(getGroupMembers(repository.owner).exists(_.userName == x.userName)) => action(repository) + case Some(x) if(getCollaboratorUserNames(paths(0), paths(1)).contains(x.userName)) => action(repository) case _ => Unauthorized() } } @@ -136,9 +117,9 @@ } /** - * Allows only signed in users which can access the repository. + * Allows only signed in users who have read permission for the repository. */ -trait ReadableUsersAuthenticator { self: ControllerBase with RepositoryService => +trait ReadableUsersAuthenticator { self: ControllerBase with RepositoryService with AccountService => protected def readableUsersOnly(action: (RepositoryInfo) => Any) = { authenticate(action) } protected def readableUsersOnly[T](action: (T, RepositoryInfo) => Any) = (form: T) => { authenticate(action(form, _)) } @@ -150,7 +131,32 @@ case Some(x) if(x.isAdmin) => action(repository) case Some(x) if(!repository.repository.isPrivate) => action(repository) case Some(x) if(paths(0) == x.userName) => action(repository) - case Some(x) if(getCollaborators(paths(0), paths(1)).contains(x.userName)) => action(repository) + case Some(x) if(getGroupMembers(repository.owner).exists(_.userName == x.userName)) => action(repository) + case Some(x) if(getCollaboratorUserNames(paths(0), paths(1)).contains(x.userName)) => action(repository) + case _ => Unauthorized() + } + } getOrElse NotFound() + } + } + } +} + +/** + * Allows only signed in users who have write permission for the repository. + */ +trait WritableUsersAuthenticator { self: ControllerBase with RepositoryService with AccountService => + protected def writableUsersOnly(action: (RepositoryInfo) => Any) = { authenticate(action) } + protected def writableUsersOnly[T](action: (T, RepositoryInfo) => Any) = (form: T) => { authenticate(action(form, _)) } + + private def authenticate(action: (RepositoryInfo) => Any) = { + { + defining(request.paths){ paths => + getRepository(paths(0), paths(1)).map { repository => + context.loginAccount match { + case Some(x) if(x.isAdmin) => action(repository) + case Some(x) if(paths(0) == x.userName) => action(repository) + case Some(x) if(getGroupMembers(repository.owner).exists(_.userName == x.userName)) => action(repository) + case Some(x) if(getCollaboratorUserNames(paths(0), paths(1), Seq(Permission.ADMIN, Permission.WRITE)).contains(x.userName)) => action(repository) case _ => Unauthorized() } } getOrElse NotFound() diff --git a/src/main/scala/gitbucket/core/util/Notifier.scala b/src/main/scala/gitbucket/core/util/Notifier.scala index 36ec89d..24a883d 100644 --- a/src/main/scala/gitbucket/core/util/Notifier.scala +++ b/src/main/scala/gitbucket/core/util/Notifier.scala @@ -22,8 +22,10 @@ ( // individual repository's owner issue.userName :: + // group members of group repository + getGroupMembers(issue.userName).map(_.userName) ::: // collaborators - getCollaborators(issue.userName, issue.repositoryName) ::: + getCollaboratorUserNames(issue.userName, issue.repositoryName) ::: // participants issue.openedUserName :: getComments(issue.userName, issue.repositoryName, issue.issueId).map(_.commentedUserName) diff --git a/src/main/twirl/gitbucket/core/account/group.scala.html b/src/main/twirl/gitbucket/core/account/group.scala.html index 1bda182..797c669 100644 --- a/src/main/twirl/gitbucket/core/account/group.scala.html +++ b/src/main/twirl/gitbucket/core/account/group.scala.html @@ -31,7 +31,7 @@ - @gitbucket.core.helper.html.account("memberName", 200) + @gitbucket.core.helper.html.account("memberName", 200, true, false)
@@ -80,15 +80,14 @@ } // check existence - $.post('@context.path/_user/existence', { - 'userName': userName - }, function(data, status){ - if(data == 'true'){ - addMemberHTML(userName, false); - } else { - $('#error-members').text('User does not exist.'); - } - }); + $.post('@context.path/_user/existence', { 'userName': userName }, + function(data, status){ + if(data == 'user'){ + addMemberHTML(userName, false); + } else { + $('#error-members').text('User does not exist.'); + } + }); }); $(document).on('click', '.remove', function(){ diff --git a/src/main/twirl/gitbucket/core/admin/usergroup.scala.html b/src/main/twirl/gitbucket/core/admin/usergroup.scala.html index ec99a73..b72ae1d 100644 --- a/src/main/twirl/gitbucket/core/admin/usergroup.scala.html +++ b/src/main/twirl/gitbucket/core/admin/usergroup.scala.html @@ -34,7 +34,7 @@ - @gitbucket.core.helper.html.account("memberName", 200) + @gitbucket.core.helper.html.account("memberName", 200, true, false)
@@ -75,16 +75,14 @@ } // check existence - $.post('@context.path/_user/existence', { - 'userName': userName, - 'userOnly': true - }, function(data, status){ - if(data == 'true'){ - addMemberHTML(userName, false); - } else { - $('#error-members').text('User does not exist.'); - } - }); + $.post('@context.path/_user/existence', { 'userName': userName }, + function(data, status){ + if(data == 'user'){ + addMemberHTML(userName, false); + } else { + $('#error-members').text('User does not exist.'); + } + }); }); $(document).on('click', '.remove', function(){ diff --git a/src/main/twirl/gitbucket/core/helper/account.scala.html b/src/main/twirl/gitbucket/core/helper/account.scala.html index e3dc42c..0e14ced 100644 --- a/src/main/twirl/gitbucket/core/helper/account.scala.html +++ b/src/main/twirl/gitbucket/core/helper/account.scala.html @@ -1,12 +1,19 @@ -@(id: String, width: Int)(implicit context: gitbucket.core.controller.Context) +@(id: String, width: Int, user: Boolean, group: Boolean)(implicit context: gitbucket.core.controller.Context) \ No newline at end of file diff --git a/src/main/twirl/gitbucket/core/settings/danger.scala.html b/src/main/twirl/gitbucket/core/settings/danger.scala.html index 60b4007..7df1170 100644 --- a/src/main/twirl/gitbucket/core/settings/danger.scala.html +++ b/src/main/twirl/gitbucket/core/settings/danger.scala.html @@ -13,7 +13,7 @@
Transfer this repo to another user or to group.
- @gitbucket.core.helper.html.account("newOwner", 200) + @gitbucket.core.helper.html.account("newOwner", 200, true, true)
diff --git a/src/main/twirl/gitbucket/core/settings/options.scala.html b/src/main/twirl/gitbucket/core/settings/options.scala.html index 7b26310..f978b58 100644 --- a/src/main/twirl/gitbucket/core/settings/options.scala.html +++ b/src/main/twirl/gitbucket/core/settings/options.scala.html @@ -39,41 +39,6 @@
-
-
-
-
Features
-
-
- - - -
-
- - - - -
+
+
Issues
+
+
+
+ +
+
+ +
+
+ +
+ + +
+
+
+
+
Wiki
+
+
+
+ +
+
+ +
+
+ +
+ + +
+
+
@@ -96,14 +113,13 @@ $(function(){ updateFeatures(); - $('#enableIssues, #enableWiki').click(function(){ + $('input[name=issuesOption], input[name=wikiOption]').click(function(){ updateFeatures(); }); }); function updateFeatures() { - $('#externalIssuesUrl').prop('disabled', $('#enableIssues').prop('checked')); - $('#allowWikiEditing').prop('disabled', !$('#enableWiki').prop('checked')); - $('#externalWikiUrl').prop('disabled', $('#enableWiki').prop('checked')); + $('#externalIssuesUrl').prop('disabled', !$('input[name=issuesOption]').select('[value=DISABLE]').prop('checked')); + $('#externalWikiUrl').prop('disabled', !$('input[name=wikiOption]').select('[value=DISABLE]').prop('checked')); } diff --git a/src/main/twirl/gitbucket/core/wiki/compare.scala.html b/src/main/twirl/gitbucket/core/wiki/compare.scala.html index 3745cb7..e2fe163 100644 --- a/src/main/twirl/gitbucket/core/wiki/compare.scala.html +++ b/src/main/twirl/gitbucket/core/wiki/compare.scala.html @@ -3,7 +3,7 @@ to: String, diffs: Seq[gitbucket.core.util.JGitUtil.DiffInfo], repository: gitbucket.core.service.RepositoryService.RepositoryInfo, - hasWritePermission: Boolean, + isEditable: Boolean, info: Option[Any])(implicit context: gitbucket.core.controller.Context) @import gitbucket.core.view.helpers @gitbucket.core.html.main(s"Compare Revisions - ${repository.owner}/${repository.name}", Some(repository)){ @@ -27,7 +27,7 @@
@gitbucket.core.helper.html.diff(diffs, repository, None, None, false, None, false, false)
- @if(hasWritePermission){ + @if(isEditable){
@if(pageName.isDefined){ Revert Changes diff --git a/src/main/twirl/gitbucket/core/wiki/edit.scala.html b/src/main/twirl/gitbucket/core/wiki/edit.scala.html index 5a0dc7d..05b3264 100644 --- a/src/main/twirl/gitbucket/core/wiki/edit.scala.html +++ b/src/main/twirl/gitbucket/core/wiki/edit.scala.html @@ -44,7 +44,7 @@ } } -