(refs #1286) Prototyping of new permission system

This commit is contained in:
Naoki Takezoe
2016-10-31 18:18:24 +09:00
parent 7d3bda42e2
commit 60ff046823
9 changed files with 69 additions and 51 deletions

View File

@@ -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)

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<changeSet>
<addColumn tableName="COLLABORATOR">
<column name="PERMISSION" type="varchar(10)" nullable="false" defaultValue="ADMIN"/>
</addColumn>
<addColumn tableName="REPOSITORY">
<column name="ALLOW_CREATE_ISSUE" type="boolean" nullable="false" defaultValueBoolean="false"/>
</addColumn>
</changeSet>

View File

@@ -18,5 +18,9 @@ object GitBucketCoreModule extends Module("gitbucket-core",
new Version("4.5.0"), new Version("4.5.0"),
new Version("4.6.0", new Version("4.6.0",
new LiquibaseMigration("update/gitbucket-core_4.6.xml") 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")
) )
) )

View File

@@ -109,7 +109,11 @@ trait IndexControllerBase extends ControllerBase {
get("/_user/proposals")(usersOnly { get("/_user/proposals")(usersOnly {
contentType = formats("json") contentType = formats("json")
org.json4s.jackson.Serialization.write( org.json4s.jackson.Serialization.write(
Map("options" -> getAllUsers(false).filter(!_.isGroupAccount).map(_.userName).toArray) Map("options" -> (if(params.get("userOnly").isDefined) {
getAllUsers(false).filter(!_.isGroupAccount).map(_.userName).toArray
} else {
getAllUsers(false).map(_.userName).toArray
}))
) )
}) })

View File

@@ -182,7 +182,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
* Add the collaborator. * Add the collaborator.
*/ */
post("/:owner/:repository/settings/collaborators/add", collaboratorForm)(ownerOnly { (form, repository) => post("/:owner/:repository/settings/collaborators/add", collaboratorForm)(ownerOnly { (form, repository) =>
if(!getAccountByUserName(repository.owner).get.isGroupAccount){ getAccountByUserName(repository.owner).foreach { _ =>
addCollaborator(repository.owner, repository.name, form.userName) addCollaborator(repository.owner, repository.name, form.userName)
} }
redirect(s"/${repository.owner}/${repository.name}/settings/collaborators") redirect(s"/${repository.owner}/${repository.name}/settings/collaborators")
@@ -192,9 +192,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
* Add the collaborator. * Add the collaborator.
*/ */
get("/:owner/:repository/settings/collaborators/remove")(ownerOnly { repository => get("/:owner/:repository/settings/collaborators/remove")(ownerOnly { repository =>
if(!getAccountByUserName(repository.owner).get.isGroupAccount){
removeCollaborator(repository.owner, repository.name, params("name")) removeCollaborator(repository.owner, repository.name, params("name"))
}
redirect(s"/${repository.owner}/${repository.name}/settings/collaborators") redirect(s"/${repository.owner}/${repository.name}/settings/collaborators")
}) })
@@ -404,10 +402,10 @@ trait RepositorySettingsControllerBase extends ControllerBase {
override def validate(name: String, value: String, messages: Messages): Option[String] = override def validate(name: String, value: String, messages: Messages): Option[String] =
getAccountByUserName(value) match { getAccountByUserName(value) match {
case None => Some("User does not exist.") case None => Some("User does not exist.")
case Some(x) if(x.isGroupAccount) // case Some(x) if(x.isGroupAccount)
=> Some("User does not exist.") // => Some("User does not exist.")
case Some(x) if(x.userName == params("owner") || getCollaborators(params("owner"), params("repository")).contains(x.userName)) case Some(x) if(x.userName == params("owner") || getCollaborators(params("owner"), params("repository")).contains(x.userName))
=> Some("User can access this repository already.") => Some(value + " is repository owner.") // TODO also group members?
case _ => None case _ => None
} }
} }

View File

@@ -337,49 +337,53 @@ trait RepositoryService { self: AccountService =>
.update (defaultBranch) .update (defaultBranch)
/** /**
* Add collaborator to the repository. * Add collaborator (user or group) to the repository.
*
* @param userName the user name of the repository owner
* @param repositoryName the repository name
* @param collaboratorName the collaborator name
*/ */
def addCollaborator(userName: String, repositoryName: String, collaboratorName: String)(implicit s: Session): Unit = def addCollaborator(userName: String, repositoryName: String, collaboratorName: String)(implicit s: Session): Unit =
Collaborators insert Collaborator(userName, repositoryName, collaboratorName) Collaborators insert Collaborator(userName, repositoryName, collaboratorName)
/** /**
* Remove collaborator from the repository. * Remove collaborator (user or group) 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 = def removeCollaborator(userName: String, repositoryName: String, collaboratorName: String)(implicit s: Session): Unit =
Collaborators.filter(_.byPrimaryKey(userName, repositoryName, collaboratorName)).delete Collaborators.filter(_.byPrimaryKey(userName, repositoryName, collaboratorName)).delete
/** /**
* Remove all collaborators from the repository. * 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 = def removeCollaborators(userName: String, repositoryName: String)(implicit s: Session): Unit =
Collaborators.filter(_.byRepository(userName, repositoryName)).delete Collaborators.filter(_.byRepository(userName, repositoryName)).delete
/** /**
* Returns the list of collaborators name which is sorted with ascending order. * Returns the list of collaborators name (user name or group 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
*/ */
def getCollaborators(userName: String, repositoryName: String)(implicit s: Session): List[String] = def getCollaborators(userName: String, repositoryName: String)(implicit s: Session): List[String] =
Collaborators.filter(_.byRepository(userName, repositoryName)).sortBy(_.collaboratorName).map(_.collaboratorName).list Collaborators.filter(_.byRepository(userName, repositoryName)).sortBy(_.collaboratorName).map(_.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[String] = Nil)(implicit s: Session): List[(String, String)] = {
val q1 = Collaborators.filter(_.byRepository(userName, repositoryName))
.innerJoin(Accounts).on { case (t1, t2) => (t1.collaboratorName === t2.userName) && (t2.groupAccount === false.bind) }
.map { case (t1, t2) => (t1.collaboratorName, "ADMIN") }
val q2 = Collaborators.filter(_.byRepository(userName, repositoryName))
.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 }
.map { case ((t1, t2), t3) => (t3.userName, "ADMIN") }
q1.union(q2).list
}
def hasWritePermission(owner: String, repository: String, loginAccount: Option[Account])(implicit s: Session): Boolean = { def hasWritePermission(owner: String, repository: String, loginAccount: Option[Account])(implicit s: Session): Boolean = {
loginAccount match { loginAccount match {
case Some(a) if(a.isAdmin) => true case Some(a) if(a.isAdmin) => true
case Some(a) if(a.userName == owner) => 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).contains((a.userName, "ADMIN"))) => true // TODO ADMIN|WRITE
case _ => false case _ => false
} }
} }

View File

@@ -1,11 +1,13 @@
package gitbucket.core.util package gitbucket.core.util
import gitbucket.core.controller.ControllerBase import gitbucket.core.controller.ControllerBase
import gitbucket.core.service.{RepositoryService, AccountService} import gitbucket.core.service.{AccountService, RepositoryService}
import RepositoryService.RepositoryInfo import RepositoryService.RepositoryInfo
import Implicits._ import Implicits._
import ControlUtil._ import ControlUtil._
import scala.collection.Searching.search
/** /**
* Allows only oneself and administrators. * Allows only oneself and administrators.
*/ */
@@ -40,9 +42,9 @@ trait OwnerAuthenticator { self: ControllerBase with RepositoryService with Acco
context.loginAccount match { context.loginAccount match {
case Some(x) if(x.isAdmin) => action(repository) case Some(x) if(x.isAdmin) => action(repository)
case Some(x) if(repository.owner == x.userName) => action(repository) case Some(x) if(repository.owner == x.userName) => action(repository)
case Some(x) if(getGroupMembers(repository.owner).exists { member => // TODO Repository management is allowed for only group managers?
member.userName == x.userName && member.isManager == true case Some(x) if(getGroupMembers(repository.owner).exists { m => m.userName == x.userName && m.isManager == true }) => action(repository)
}) => action(repository) case Some(x) if(getCollaboratorUserNames(paths(0), paths(1), Seq("ADMIN")).exists(_._1 == x.userName)) => action(repository)
case _ => Unauthorized() case _ => Unauthorized()
} }
} getOrElse NotFound() } getOrElse NotFound()
@@ -88,7 +90,7 @@ trait AdminAuthenticator { self: ControllerBase =>
/** /**
* Allows only collaborators and administrators. * Allows only collaborators and administrators.
*/ */
trait CollaboratorsAuthenticator { self: ControllerBase with RepositoryService => trait CollaboratorsAuthenticator { self: ControllerBase with RepositoryService with AccountService =>
protected def collaboratorsOnly(action: (RepositoryInfo) => Any) = { authenticate(action) } protected def collaboratorsOnly(action: (RepositoryInfo) => Any) = { authenticate(action) }
protected def collaboratorsOnly[T](action: (T, RepositoryInfo) => Any) = (form: T) => { authenticate(action(form, _)) } protected def collaboratorsOnly[T](action: (T, RepositoryInfo) => Any) = (form: T) => { authenticate(action(form, _)) }
@@ -99,7 +101,8 @@ trait CollaboratorsAuthenticator { self: ControllerBase with RepositoryService =
context.loginAccount match { context.loginAccount match {
case Some(x) if(x.isAdmin) => action(repository) case Some(x) if(x.isAdmin) => action(repository)
case Some(x) if(paths(0) == x.userName) => 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), Seq("ADMIN", "WRITE")).exists(_._1 == x.userName)) => action(repository)
case _ => Unauthorized() case _ => Unauthorized()
} }
} getOrElse NotFound() } getOrElse NotFound()
@@ -109,9 +112,9 @@ trait CollaboratorsAuthenticator { self: ControllerBase with RepositoryService =
} }
/** /**
* Allows only the repository owner (or manager for group repository) and administrators. * Allows only guests and signed in users who can access the repository.
*/ */
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(action: (RepositoryInfo) => Any) = { authenticate(action) }
protected def referrersOnly[T](action: (T, RepositoryInfo) => Any) = (form: T) => { authenticate(action(form, _)) } protected def referrersOnly[T](action: (T, RepositoryInfo) => Any) = (form: T) => { authenticate(action(form, _)) }
@@ -125,7 +128,8 @@ trait ReferrerAuthenticator { self: ControllerBase with RepositoryService =>
context.loginAccount match { context.loginAccount match {
case Some(x) if(x.isAdmin) => action(repository) case Some(x) if(x.isAdmin) => action(repository)
case Some(x) if(paths(0) == x.userName) => 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)).exists(_._1 == x.userName)) => action(repository)
case _ => Unauthorized() case _ => Unauthorized()
} }
} }
@@ -136,9 +140,9 @@ trait ReferrerAuthenticator { self: ControllerBase with RepositoryService =>
} }
/** /**
* Allows only signed in users which can access the repository. * Allows only signed in users who can access 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(action: (RepositoryInfo) => Any) = { authenticate(action) }
protected def readableUsersOnly[T](action: (T, RepositoryInfo) => Any) = (form: T) => { authenticate(action(form, _)) } protected def readableUsersOnly[T](action: (T, RepositoryInfo) => Any) = (form: T) => { authenticate(action(form, _)) }
@@ -150,7 +154,8 @@ trait ReadableUsersAuthenticator { self: ControllerBase with RepositoryService =
case Some(x) if(x.isAdmin) => action(repository) case Some(x) if(x.isAdmin) => action(repository)
case Some(x) if(!repository.repository.isPrivate) => 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(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)).exists(_._1 == x.userName)) => action(repository)
case _ => Unauthorized() case _ => Unauthorized()
} }
} getOrElse NotFound() } getOrElse NotFound()

View File

@@ -1,4 +1,4 @@
@(id: String, width: Int)(implicit context: gitbucket.core.controller.Context) @(id: String, width: Int, userOnly: Boolean = true)(implicit context: gitbucket.core.controller.Context)
<span style="margin-right: 0px;"> <span style="margin-right: 0px;">
<input type="text" name="@id" id="@id" class="form-control" autocomplete="off" style="width: @{width}px; margin-bottom: 0px; display: inline; vertical-align: middle;"/> <input type="text" name="@id" id="@id" class="form-control" autocomplete="off" style="width: @{width}px; margin-bottom: 0px; display: inline; vertical-align: middle;"/>
</span> </span>
@@ -6,7 +6,7 @@
$(function(){ $(function(){
$('#@id').typeahead({ $('#@id').typeahead({
source: function (query, process) { source: function (query, process) {
return $.get('@context.path/_user/proposals', { query: query }, return $.get('@context.path/_user/proposals@if(userOnly){?userOnly}', { query: query },
function (data) { function (data) {
return process(data.options); return process(data.options);
}); });

View File

@@ -10,25 +10,17 @@
@collaborators.map { collaboratorName => @collaborators.map { collaboratorName =>
<li> <li>
<a href="@helpers.url(collaboratorName)">@collaboratorName</a> <a href="@helpers.url(collaboratorName)">@collaboratorName</a>
@if(!isGroupRepository){
<a href="@helpers.url(repository)/settings/collaborators/remove?name=@collaboratorName" class="remove">(remove)</a> <a href="@helpers.url(repository)/settings/collaborators/remove?name=@collaboratorName" class="remove">(remove)</a>
} else {
@if(repository.managers.contains(collaboratorName)){
(Manager)
}
}
</li> </li>
} }
</ul> </ul>
@if(!isGroupRepository){
<form method="POST" action="@helpers.url(repository)/settings/collaborators/add" validate="true" autocomplete="off"> <form method="POST" action="@helpers.url(repository)/settings/collaborators/add" validate="true" autocomplete="off">
<div> <div>
<span class="error" id="error-userName"></span> <span class="error" id="error-userName"></span>
</div> </div>
@gitbucket.core.helper.html.account("userName", 300) @gitbucket.core.helper.html.account("userName", 300, false)
<input type="submit" class="btn btn-default" value="Add"/> <input type="submit" class="btn btn-default" value="Add"/>
</form> </form>
} }
} }
}
} }