Reject direct push to branch if branch protection is enabled (#3791)

This commit is contained in:
Naoki Takezoe
2025-08-03 11:58:28 +09:00
committed by GitHub
parent fda67a32e2
commit 0c1e8b932b
21 changed files with 726 additions and 350 deletions

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<changeSet>
<!--================================================================================================-->
<!-- PROTECTED_BRANCH -->
<!--================================================================================================-->
<addColumn tableName="PROTECTED_BRANCH">
<column name="REQUIRED_STATUS_CHECK" type="boolean" nullable="false" defaultValue="false"/>
<column name="RESTRICTIONS" type="boolean" nullable="false" defaultValue="false"/>
</addColumn>
<sql>
UPDATE PROTECTED_BRANCH SET REQUIRED_STATUS_CHECK = TRUE
WHERE EXISTS (SELECT * FROM PROTECTED_BRANCH_REQUIRE_CONTEXT
WHERE PROTECTED_BRANCH.USER_NAME = PROTECTED_BRANCH_REQUIRE_CONTEXT.USER_NAME
AND PROTECTED_BRANCH.REPOSITORY_NAME = PROTECTED_BRANCH_REQUIRE_CONTEXT.REPOSITORY_NAME
AND PROTECTED_BRANCH.BRANCH = PROTECTED_BRANCH_REQUIRE_CONTEXT.BRANCH)
</sql>
<!--================================================================================================-->
<!-- PROTECTED_BRANCH_RESTRICTIONS_USER -->
<!--================================================================================================-->
<createTable tableName="PROTECTED_BRANCH_RESTRICTION">
<column name="USER_NAME" type="varchar(100)" nullable="false"/>
<column name="REPOSITORY_NAME" type="varchar(100)" nullable="false"/>
<column name="BRANCH" type="varchar(100)" nullable="false"/>
<column name="ALLOWED_USER" type="varchar(255)" nullable="false"/>
</createTable>
<addPrimaryKey constraintName="IDX_PROTECTED_BRANCH_RESTRICTION_PK" tableName="PROTECTED_BRANCH_RESTRICTION" columnNames="USER_NAME, REPOSITORY_NAME, BRANCH, ALLOWED_USER"/>
<addForeignKeyConstraint constraintName="IDX_PROTECTED_BRANCH_RESTRICTION_FK0" baseTableName="PROTECTED_BRANCH_RESTRICTION" baseColumnNames="USER_NAME, REPOSITORY_NAME, BRANCH" referencedTableName="PROTECTED_BRANCH" referencedColumnNames="USER_NAME, REPOSITORY_NAME, BRANCH" onDelete="CASCADE" onUpdate="CASCADE"/>
<addForeignKeyConstraint constraintName="IDX_PROTECTED_BRANCH_RESTRICTION_FK1" baseTableName="PROTECTED_BRANCH_RESTRICTION" baseColumnNames="ALLOWED_USER" referencedTableName="ACCOUNT" referencedColumnNames="USER_NAME"/>
</changeSet>

View File

@@ -120,7 +120,8 @@ object GitBucketCoreModule
new Version("4.41.0"), new Version("4.41.0"),
new Version("4.42.0", new LiquibaseMigration("update/gitbucket-core_4.42.xml")), new Version("4.42.0", new LiquibaseMigration("update/gitbucket-core_4.42.xml")),
new Version("4.42.1"), new Version("4.42.1"),
new Version("4.43.0") new Version("4.43.0"),
new Version("4.44.0", new LiquibaseMigration("update/gitbucket-core_4.44.xml"))
) { ) {
java.util.logging.Logger.getLogger("liquibase").setLevel(Level.SEVERE) java.util.logging.Logger.getLogger("liquibase").setLevel(Level.SEVERE)
} }

View File

@@ -6,7 +6,7 @@ import gitbucket.core.util.RepositoryName
* https://developer.github.com/v3/repos/#get-branch * https://developer.github.com/v3/repos/#get-branch
* https://developer.github.com/v3/repos/#enabling-and-disabling-branch-protection * https://developer.github.com/v3/repos/#enabling-and-disabling-branch-protection
*/ */
case class ApiBranch(name: String, commit: ApiBranchCommit, protection: ApiBranchProtection)( case class ApiBranch(name: String, commit: ApiBranchCommit, protection: ApiBranchProtectionResponse)(
repositoryName: RepositoryName repositoryName: RepositoryName
) extends FieldSerializable { ) extends FieldSerializable {
val _links = val _links =

View File

@@ -0,0 +1,21 @@
package gitbucket.core.api
/** https://developer.github.com/v3/repos/#enabling-and-disabling-branch-protection */
case class ApiBranchProtectionRequest(
enabled: Boolean,
required_status_checks: Option[ApiBranchProtectionRequest.Status],
restrictions: Option[ApiBranchProtectionRequest.Restrictions],
enforce_admins: Option[Boolean]
)
object ApiBranchProtectionRequest {
/** form for enabling-and-disabling-branch-protection */
case class EnablingAndDisabling(protection: ApiBranchProtectionRequest)
case class Status(
contexts: Seq[String]
)
case class Restrictions(users: Seq[String])
}

View File

@@ -4,55 +4,68 @@ import gitbucket.core.service.ProtectedBranchService
import org.json4s._ import org.json4s._
/** https://developer.github.com/v3/repos/#enabling-and-disabling-branch-protection */ /** https://developer.github.com/v3/repos/#enabling-and-disabling-branch-protection */
case class ApiBranchProtection( case class ApiBranchProtectionResponse(
url: Option[ApiPath], // for output url: Option[ApiPath], // for output
enabled: Boolean, enabled: Boolean,
required_status_checks: Option[ApiBranchProtection.Status] required_status_checks: Option[ApiBranchProtectionResponse.Status],
restrictions: Option[ApiBranchProtectionResponse.Restrictions],
enforce_admins: Option[ApiBranchProtectionResponse.EnforceAdmins]
) { ) {
def status: ApiBranchProtection.Status = required_status_checks.getOrElse(ApiBranchProtection.statusNone) def status: ApiBranchProtectionResponse.Status =
required_status_checks.getOrElse(ApiBranchProtectionResponse.statusNone)
} }
object ApiBranchProtection { object ApiBranchProtectionResponse {
/** form for enabling-and-disabling-branch-protection */ case class EnforceAdmins(enabled: Boolean)
case class EnablingAndDisabling(protection: ApiBranchProtection)
def apply(info: ProtectedBranchService.ProtectedBranchInfo): ApiBranchProtection = // /** form for enabling-and-disabling-branch-protection */
ApiBranchProtection( // case class EnablingAndDisabling(protection: ApiBranchProtectionResponse)
def apply(info: ProtectedBranchService.ProtectedBranchInfo): ApiBranchProtectionResponse =
ApiBranchProtectionResponse(
url = Some( url = Some(
ApiPath( ApiPath(
s"/api/v3/repos/${info.owner}/${info.repository}/branches/${info.branch}/protection" s"/api/v3/repos/${info.owner}/${info.repository}/branches/${info.branch}/protection"
) )
), ),
enabled = info.enabled, enabled = info.enabled,
required_status_checks = Some( required_status_checks = info.contexts.map { contexts =>
Status( Status(
Some( Some(
ApiPath( ApiPath(
s"/api/v3/repos/${info.owner}/${info.repository}/branches/${info.branch}/protection/required_status_checks" s"/api/v3/repos/${info.owner}/${info.repository}/branches/${info.branch}/protection/required_status_checks"
) )
), ),
EnforcementLevel(info.enabled && info.contexts.nonEmpty, info.includeAdministrators), EnforcementLevel(info.enabled && info.contexts.nonEmpty, info.enforceAdmins),
info.contexts, contexts,
Some( Some(
ApiPath( ApiPath(
s"/api/v3/repos/${info.owner}/${info.repository}/branches/${info.branch}/protection/required_status_checks/contexts" s"/api/v3/repos/${info.owner}/${info.repository}/branches/${info.branch}/protection/required_status_checks/contexts"
) )
) )
) )
},
restrictions = info.restrictionsUsers.map { restrictionsUsers =>
Restrictions(restrictionsUsers)
},
enforce_admins = if (info.enabled) Some(EnforceAdmins(info.enforceAdmins)) else None
) )
)
val statusNone = Status(None, Off, Seq.empty, None) val statusNone: Status = Status(None, Off, Seq.empty, None)
case class Status( case class Status(
url: Option[ApiPath], // for output url: Option[ApiPath], // for output
enforcement_level: EnforcementLevel, enforcement_level: EnforcementLevel,
contexts: Seq[String], contexts: Seq[String],
contexts_url: Option[ApiPath] // for output contexts_url: Option[ApiPath] // for output
) )
sealed class EnforcementLevel(val name: String) sealed class EnforcementLevel(val name: String)
case object Off extends EnforcementLevel("off") case object Off extends EnforcementLevel("off")
case object NonAdmins extends EnforcementLevel("non_admins") case object NonAdmins extends EnforcementLevel("non_admins")
case object Everyone extends EnforcementLevel("everyone") case object Everyone extends EnforcementLevel("everyone")
object EnforcementLevel { object EnforcementLevel {
def apply(enabled: Boolean, includeAdministrators: Boolean): EnforcementLevel = def apply(enabled: Boolean, includeAdministrators: Boolean): EnforcementLevel =
if (enabled) { if (enabled) {
@@ -66,6 +79,8 @@ object ApiBranchProtection {
} }
} }
case class Restrictions(users: Seq[String])
implicit val enforcementLevelSerializer: CustomSerializer[EnforcementLevel] = implicit val enforcementLevelSerializer: CustomSerializer[EnforcementLevel] =
new CustomSerializer[EnforcementLevel](format => new CustomSerializer[EnforcementLevel](format =>
( (

View File

@@ -44,7 +44,7 @@ object JsonFormat {
FieldSerializer[ApiCommits.File]() + FieldSerializer[ApiCommits.File]() +
FieldSerializer[ApiRelease]() + FieldSerializer[ApiRelease]() +
FieldSerializer[ApiReleaseAsset]() + FieldSerializer[ApiReleaseAsset]() +
ApiBranchProtection.enforcementLevelSerializer ApiBranchProtectionResponse.enforcementLevelSerializer
def apiPathSerializer(c: Context) = def apiPathSerializer(c: Context) =
new CustomSerializer[ApiPath](_ => new CustomSerializer[ApiPath](_ =>

View File

@@ -261,11 +261,22 @@ trait IndexControllerBase extends ControllerBase {
/** /**
* JSON API for checking user or group existence. * JSON API for checking user or group existence.
*
* Returns a single string which is any of "group", "user" or "". * Returns a single string which is any of "group", "user" or "".
* Additionally, check whether the user is writable to the repository
* if "owner" and "repository" are given,
*/ */
post("/_user/existence")(usersOnly { post("/_user/existence")(usersOnly {
getAccountByUserNameIgnoreCase(params("userName")).map { account => getAccountByUserNameIgnoreCase(params("userName")).map { account =>
if (!account.isGroupAccount && params.get("repository").isDefined && params.get("owner").isDefined) {
getRepository(params("owner"), params("repository"))
.collect {
case repository if isWritable(repository.repository, Some(account)) => "user"
}
.getOrElse("")
} else {
if (account.isGroupAccount) "group" else "user" if (account.isGroupAccount) "group" else "user"
}
} getOrElse "" } getOrElse ""
}) })

View File

@@ -203,7 +203,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
if (!repository.branchList.contains(branch)) { if (!repository.branchList.contains(branch)) {
redirect(s"/${repository.owner}/${repository.name}/settings/branches") redirect(s"/${repository.owner}/${repository.name}/settings/branches")
} else { } else {
val protection = ApiBranchProtection(getProtectedBranchInfo(repository.owner, repository.name, branch)) val protection = ApiBranchProtectionResponse(getProtectedBranchInfo(repository.owner, repository.name, branch))
val lastWeeks = getRecentStatusContexts( val lastWeeks = getRecentStatusContexts(
repository.owner, repository.owner,
repository.name, repository.name,

View File

@@ -5,7 +5,7 @@ import gitbucket.core.service.{AccountService, ProtectedBranchService, Repositor
import gitbucket.core.util.* import gitbucket.core.util.*
import gitbucket.core.util.Directory.* import gitbucket.core.util.Directory.*
import gitbucket.core.util.Implicits.* import gitbucket.core.util.Implicits.*
import gitbucket.core.util.JGitUtil.getBranchesNoMergeInfo import gitbucket.core.util.JGitUtil.{getBranchesNoMergeInfo, processTree}
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.scalatra.NoContent import org.scalatra.NoContent
@@ -43,7 +43,9 @@ trait ApiRepositoryBranchControllerBase extends ControllerBase {
} yield { } yield {
val protection = getProtectedBranchInfo(repository.owner, repository.name, branch) val protection = getProtectedBranchInfo(repository.owner, repository.name, branch)
JsonFormat( JsonFormat(
ApiBranch(branch, ApiBranchCommit(br.commitId), ApiBranchProtection(protection))(RepositoryName(repository)) ApiBranch(branch, ApiBranchCommit(br.commitId), ApiBranchProtectionResponse(protection))(
RepositoryName(repository)
)
) )
}) getOrElse NotFound() }) getOrElse NotFound()
} }
@@ -58,7 +60,7 @@ trait ApiRepositoryBranchControllerBase extends ControllerBase {
if (repository.branchList.contains(branch)) { if (repository.branchList.contains(branch)) {
val protection = getProtectedBranchInfo(repository.owner, repository.name, branch) val protection = getProtectedBranchInfo(repository.owner, repository.name, branch)
JsonFormat( JsonFormat(
ApiBranchProtection(protection) ApiBranchProtectionResponse(protection)
) )
} else { NotFound() } } else { NotFound() }
}) })
@@ -138,7 +140,7 @@ trait ApiRepositoryBranchControllerBase extends ControllerBase {
if (repository.branchList.contains(branch)) { if (repository.branchList.contains(branch)) {
val protection = getProtectedBranchInfo(repository.owner, repository.name, branch) val protection = getProtectedBranchInfo(repository.owner, repository.name, branch)
JsonFormat( JsonFormat(
ApiBranchProtection(protection).required_status_checks ApiBranchProtectionResponse(protection).required_status_checks
) )
} else { NotFound() } } else { NotFound() }
}) })
@@ -262,7 +264,7 @@ trait ApiRepositoryBranchControllerBase extends ControllerBase {
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git => Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
(for { (for {
branch <- params.get("splat") if repository.branchList.contains(branch) branch <- params.get("splat") if repository.branchList.contains(branch)
protection <- extractFromJsonBody[ApiBranchProtection.EnablingAndDisabling].map(_.protection) protection <- extractFromJsonBody[ApiBranchProtectionRequest.EnablingAndDisabling].map(_.protection)
br <- getBranchesNoMergeInfo(git).find(_.name == branch) br <- getBranchesNoMergeInfo(git).find(_.name == branch)
} yield { } yield {
if (protection.enabled) { if (protection.enabled) {
@@ -270,13 +272,17 @@ trait ApiRepositoryBranchControllerBase extends ControllerBase {
repository.owner, repository.owner,
repository.name, repository.name,
branch, branch,
protection.status.enforcement_level == ApiBranchProtection.Everyone, protection.enforce_admins.getOrElse(false),
protection.status.contexts protection.required_status_checks.isDefined,
protection.required_status_checks.map(_.contexts).getOrElse(Nil),
protection.restrictions.isDefined,
protection.restrictions.map(_.users).getOrElse(Nil)
) )
} else { } else {
disableBranchProtection(repository.owner, repository.name, branch) disableBranchProtection(repository.owner, repository.name, branch)
} }
JsonFormat(ApiBranch(branch, ApiBranchCommit(br.commitId), protection)(RepositoryName(repository))) val response = ApiBranchProtectionResponse(getProtectedBranchInfo(repository.owner, repository.name, branch))
JsonFormat(ApiBranch(branch, ApiBranchCommit(br.commitId), response)(RepositoryName(repository)))
}) getOrElse NotFound() }) getOrElse NotFound()
} }
}) })

View File

@@ -1,16 +1,18 @@
package gitbucket.core.model package gitbucket.core.model
trait ProtectedBranchComponent extends TemplateComponent { self: Profile => trait ProtectedBranchComponent extends TemplateComponent { self: Profile =>
import profile.api._ import profile.api.*
import self._
lazy val ProtectedBranches = TableQuery[ProtectedBranches] lazy val ProtectedBranches = TableQuery[ProtectedBranches]
class ProtectedBranches(tag: Tag) extends Table[ProtectedBranch](tag, "PROTECTED_BRANCH") with BranchTemplate { class ProtectedBranches(tag: Tag) extends Table[ProtectedBranch](tag, "PROTECTED_BRANCH") with BranchTemplate {
val statusCheckAdmin = column[Boolean]("STATUS_CHECK_ADMIN") val statusCheckAdmin = column[Boolean]("STATUS_CHECK_ADMIN") // enforceAdmins
def * = (userName, repositoryName, branch, statusCheckAdmin).mapTo[ProtectedBranch] val requiredStatusCheck = column[Boolean]("REQUIRED_STATUS_CHECK")
def byPrimaryKey(userName: String, repositoryName: String, branch: String) = val restrictions = column[Boolean]("RESTRICTIONS")
def * =
(userName, repositoryName, branch, statusCheckAdmin, requiredStatusCheck, restrictions).mapTo[ProtectedBranch]
def byPrimaryKey(userName: String, repositoryName: String, branch: String): Rep[Boolean] =
byBranch(userName, repositoryName, branch) byBranch(userName, repositoryName, branch)
def byPrimaryKey(userName: Rep[String], repositoryName: Rep[String], branch: Rep[String]) = def byPrimaryKey(userName: Rep[String], repositoryName: Rep[String], branch: Rep[String]): Rep[Boolean] =
byBranch(userName, repositoryName, branch) byBranch(userName, repositoryName, branch)
} }
@@ -22,8 +24,27 @@ trait ProtectedBranchComponent extends TemplateComponent { self: Profile =>
def * = def * =
(userName, repositoryName, branch, context).mapTo[ProtectedBranchContext] (userName, repositoryName, branch, context).mapTo[ProtectedBranchContext]
} }
lazy val ProtectedBranchRestrictions = TableQuery[ProtectedBranchRestrictions]
class ProtectedBranchRestrictions(tag: Tag)
extends Table[ProtectedBranchRestriction](tag, "PROTECTED_BRANCH_RESTRICTION")
with BranchTemplate {
val allowedUser = column[String]("ALLOWED_USER")
def * = (userName, repositoryName, branch, allowedUser).mapTo[ProtectedBranchRestriction]
def byPrimaryKey(userName: String, repositoryName: String, branch: String, allowedUser: String): Rep[Boolean] =
this.userName === userName.bind && this.repositoryName === repositoryName.bind && this.branch === branch.bind && this.allowedUser === allowedUser.bind
}
} }
case class ProtectedBranch(userName: String, repositoryName: String, branch: String, statusCheckAdmin: Boolean) case class ProtectedBranch(
userName: String,
repositoryName: String,
branch: String,
enforceAdmins: Boolean,
requiredStatusCheck: Boolean,
restrictions: Boolean
)
case class ProtectedBranchContext(userName: String, repositoryName: String, branch: String, context: String) case class ProtectedBranchContext(userName: String, repositoryName: String, branch: String, context: String)
case class ProtectedBranchRestriction(userName: String, repositoryName: String, branch: String, allowedUser: String)

View File

@@ -1,9 +1,10 @@
package gitbucket.core.service package gitbucket.core.service
import gitbucket.core.model.{Session => _, _}
import gitbucket.core.plugin.ReceiveHook import gitbucket.core.plugin.ReceiveHook
import gitbucket.core.model.Profile._ import gitbucket.core.model.Profile.*
import gitbucket.core.model.Profile.profile.blockingApi._ import gitbucket.core.model.Profile.profile.blockingApi.*
import gitbucket.core.model.{CommitState, ProtectedBranch, ProtectedBranchContext, ProtectedBranchRestriction, Role}
import gitbucket.core.util.SyntaxSugars.*
import org.eclipse.jgit.transport.{ReceiveCommand, ReceivePack} import org.eclipse.jgit.transport.{ReceiveCommand, ReceivePack}
trait ProtectedBranchService { trait ProtectedBranchService {
@@ -13,17 +14,27 @@ trait ProtectedBranchService {
): Option[ProtectedBranchInfo] = ): Option[ProtectedBranchInfo] =
ProtectedBranches ProtectedBranches
.joinLeft(ProtectedBranchContexts) .joinLeft(ProtectedBranchContexts)
.on { case (pb, c) => pb.byBranch(c.userName, c.repositoryName, c.branch) } .on { case pb ~ c => pb.byBranch(c.userName, c.repositoryName, c.branch) }
.map { case (pb, c) => pb -> c.map(_.context) } .joinLeft(ProtectedBranchRestrictions)
.on { case pb ~ c ~ r => pb.byBranch(r.userName, r.repositoryName, r.branch) }
.map { case pb ~ c ~ r => pb -> (c.map(_.context), r.map(_.allowedUser)) }
.filter(_._1.byPrimaryKey(owner, repository, branch)) .filter(_._1.byPrimaryKey(owner, repository, branch))
.list .list
.groupBy(_._1) .groupBy(_._1)
.headOption .headOption
.map { p => .map { (p: (ProtectedBranch, List[(ProtectedBranch, (Option[String], Option[String]))])) =>
p._1 -> p._2.flatMap(_._2) p._1 -> (p._2.flatMap(_._2._1), p._2.flatMap(_._2._2))
} }
.map { case (t1, contexts) => .map { case (t1, (contexts, users)) =>
new ProtectedBranchInfo(t1.userName, t1.repositoryName, t1.branch, true, contexts, t1.statusCheckAdmin) new ProtectedBranchInfo(
t1.userName,
t1.repositoryName,
t1.branch,
true,
if (t1.requiredStatusCheck) Some(contexts) else None,
t1.enforceAdmins,
if (t1.restrictions) Some(users) else None
)
} }
def getProtectedBranchInfo(owner: String, repository: String, branch: String)(implicit def getProtectedBranchInfo(owner: String, repository: String, branch: String)(implicit
@@ -40,19 +51,32 @@ trait ProtectedBranchService {
owner: String, owner: String,
repository: String, repository: String,
branch: String, branch: String,
includeAdministrators: Boolean, enforceAdmins: Boolean,
contexts: Seq[String] requiredStatusCheck: Boolean,
contexts: Seq[String],
restrictions: Boolean,
restrictionsUsers: Seq[String]
)(implicit session: Session): Unit = { )(implicit session: Session): Unit = {
disableBranchProtection(owner, repository, branch) disableBranchProtection(owner, repository, branch)
ProtectedBranches.insert(new ProtectedBranch(owner, repository, branch, includeAdministrators && contexts.nonEmpty)) ProtectedBranches.insert(
contexts.map { context => ProtectedBranch(owner, repository, branch, enforceAdmins, requiredStatusCheck, restrictions)
ProtectedBranchContexts.insert(new ProtectedBranchContext(owner, repository, branch, context)) )
if (restrictions) {
restrictionsUsers.foreach { user =>
ProtectedBranchRestrictions.insert(ProtectedBranchRestriction(owner, repository, branch, user))
}
}
if (requiredStatusCheck) {
contexts.foreach { context =>
ProtectedBranchContexts.insert(ProtectedBranchContext(owner, repository, branch, context))
}
} }
} }
def disableBranchProtection(owner: String, repository: String, branch: String)(implicit session: Session): Unit = def disableBranchProtection(owner: String, repository: String, branch: String)(implicit session: Session): Unit =
ProtectedBranches.filter(_.byPrimaryKey(owner, repository, branch)).delete ProtectedBranches.filter(_.byPrimaryKey(owner, repository, branch)).delete
} }
object ProtectedBranchService { object ProtectedBranchService {
@@ -101,6 +125,7 @@ object ProtectedBranchService {
) )
} }
} else { } else {
println("-> else")
None None
} }
} }
@@ -117,12 +142,16 @@ object ProtectedBranchService {
* When enabled, commits must first be pushed to another branch, * When enabled, commits must first be pushed to another branch,
* then merged or pushed directly to test after status checks have passed. * then merged or pushed directly to test after status checks have passed.
*/ */
contexts: Seq[String], contexts: Option[Seq[String]],
/** /**
* Include administrators * Include administrators
* Enforce required status checks for repository administrators. * Enforce required status checks for repository administrators.
*/ */
includeAdministrators: Boolean enforceAdmins: Boolean,
/**
* Users who can push to the branch.
*/
restrictionsUsers: Option[Seq[String]]
) extends AccountService ) extends AccountService
with RepositoryService with RepositoryService
with CommitStatusService { with CommitStatusService {
@@ -148,42 +177,66 @@ object ProtectedBranchService {
session: Session session: Session
): Option[String] = { ): Option[String] = {
if (enabled) { if (enabled) {
command.getType() match { command.getType match {
case ReceiveCommand.Type.UPDATE_NONFASTFORWARD if isAllowNonFastForwards => case ReceiveCommand.Type.UPDATE_NONFASTFORWARD if isAllowNonFastForwards =>
Some("Cannot force-push to a protected branch") Some("Cannot force-push to a protected branch")
case ReceiveCommand.Type.UPDATE | ReceiveCommand.Type.UPDATE_NONFASTFORWARD if !isPushAllowed(pusher) =>
Some("You do not have permission to push to this branch")
case ReceiveCommand.Type.UPDATE | ReceiveCommand.Type.UPDATE_NONFASTFORWARD if needStatusCheck(pusher) => case ReceiveCommand.Type.UPDATE | ReceiveCommand.Type.UPDATE_NONFASTFORWARD if needStatusCheck(pusher) =>
unSuccessedContexts(command.getNewId.name) match { unSuccessedContexts(command.getNewId.name) match {
case s if s.sizeIs == 1 => Some(s"""Required status check "${s.toSeq(0)}" is expected""") case s if s.sizeIs == 1 => Some(s"""Required status check "${s.head}" is expected""")
case s if s.sizeIs >= 1 => Some(s"${s.size} of ${contexts.size} required status checks are expected") case s if s.sizeIs >= 1 =>
Some(s"${s.size} of ${contexts.map(_.size).getOrElse(0)} required status checks are expected")
case _ => None case _ => None
} }
case ReceiveCommand.Type.DELETE => case ReceiveCommand.Type.DELETE =>
Some("Cannot delete a protected branch") Some("You do not have permission to push to this branch")
case _ => None case _ => None
} }
} else { } else {
None None
} }
} }
def unSuccessedContexts(sha1: String)(implicit session: Session): Set[String] =
if (contexts.isEmpty) { def unSuccessedContexts(sha1: String)(implicit session: Session): Set[String] = {
Set.empty contexts match {
} else { case None => Set.empty
contexts.toSet -- getCommitStatuses(owner, repository, sha1) case Some(x) if x.isEmpty => Set.empty
case Some(x) =>
x.toSet -- getCommitStatuses(owner, repository, sha1)
.filter(_.state == CommitState.SUCCESS) .filter(_.state == CommitState.SUCCESS)
.map(_.context) .map(_.context)
.toSet .toSet
} }
}
def needStatusCheck(pusher: String)(implicit session: Session): Boolean = pusher match { def needStatusCheck(pusher: String)(implicit session: Session): Boolean = pusher match {
case _ if !enabled => false case _ if !enabled => false
case _ if contexts.isEmpty => false case _ if contexts.isEmpty => false
case _ if includeAdministrators => true case _ if enforceAdmins => true
case p if isAdministrator(p) => false case p if isAdministrator(p) => false
case _ => true case _ => true
} }
def isPushAllowed(pusher: String)(implicit session: Session): Boolean = pusher match {
case _ if !enabled || restrictionsUsers.isEmpty => true
case _ if restrictionsUsers.get.contains(pusher) => true
case p if isAdministrator(p) && enforceAdmins => false
case _ => false
} }
}
object ProtectedBranchInfo { object ProtectedBranchInfo {
def disabled(owner: String, repository: String, branch: String): ProtectedBranchInfo = def disabled(owner: String, repository: String, branch: String): ProtectedBranchInfo = {
ProtectedBranchInfo(owner, repository, branch, false, Nil, false) ProtectedBranchInfo(
owner,
repository,
branch,
enabled = false,
contexts = None,
enforceAdmins = false,
restrictionsUsers = None
)
}
} }
} }

View File

@@ -653,18 +653,18 @@ object PullRequestService {
commitIdTo: String commitIdTo: String
) { ) {
val hasConflict = conflictMessage.isDefined val hasConflict: Boolean = conflictMessage.isDefined
val statuses: List[CommitStatus] = val statuses: List[CommitStatus] =
commitStatuses ++ (branchProtection.contexts.toSet -- commitStatuses.map(_.context).toSet) commitStatuses ++ (branchProtection.contexts.getOrElse(Nil).toSet -- commitStatuses.map(_.context).toSet)
.map(CommitStatus.pending(branchProtection.owner, branchProtection.repository, _)) .map(CommitStatus.pending(branchProtection.owner, branchProtection.repository, _))
val hasRequiredStatusProblem = needStatusCheck && branchProtection.contexts.exists(context => val hasRequiredStatusProblem: Boolean = needStatusCheck && branchProtection.contexts
statuses.find(_.context == context).map(_.state) != Some(CommitState.SUCCESS) .getOrElse(Nil)
) .exists(context => !statuses.find(_.context == context).map(_.state).contains(CommitState.SUCCESS))
val hasProblem = hasRequiredStatusProblem || hasConflict || (statuses.nonEmpty && CommitState.combine( val hasProblem: Boolean = hasRequiredStatusProblem || hasConflict || (statuses.nonEmpty && CommitState.combine(
statuses.map(_.state).toSet statuses.map(_.state).toSet
) != CommitState.SUCCESS) ) != CommitState.SUCCESS)
val canUpdate = branchIsOutOfDate && !hasConflict val canUpdate: Boolean = branchIsOutOfDate && !hasConflict
val canMerge = hasMergePermission && !hasConflict && !hasRequiredStatusProblem val canMerge: Boolean = hasMergePermission && !hasConflict && !hasRequiredStatusProblem
lazy val commitStateSummary: (CommitState, String) = { lazy val commitStateSummary: (CommitState, String) = {
val stateMap = statuses.groupBy(_.state) val stateMap = statuses.groupBy(_.state)
val state = CommitState.combine(stateMap.keySet) val state = CommitState.combine(stateMap.keySet)
@@ -672,8 +672,8 @@ object PullRequestService {
state -> summary state -> summary
} }
lazy val statusesAndRequired: List[(CommitStatus, Boolean)] = statuses.map { s => lazy val statusesAndRequired: List[(CommitStatus, Boolean)] = statuses.map { s =>
s -> branchProtection.contexts.contains(s.context) s -> branchProtection.contexts.getOrElse(Nil).contains(s.context)
} }
lazy val isAllSuccess = commitStateSummary._1 == CommitState.SUCCESS lazy val isAllSuccess: Boolean = commitStateSummary._1 == CommitState.SUCCESS
} }
} }

View File

@@ -1,7 +1,7 @@
package gitbucket.core.service package gitbucket.core.service
import gitbucket.core.api.JsonFormat import gitbucket.core.api.JsonFormat
import gitbucket.core.model.{Account, WebHook} import gitbucket.core.model.{Account, WebHook}
import gitbucket.core.model.Profile.profile.blockingApi._ import gitbucket.core.model.Profile.profile.blockingApi.*
import gitbucket.core.model.activity.{CloseIssueInfo, PushInfo} import gitbucket.core.model.activity.{CloseIssueInfo, PushInfo}
import gitbucket.core.plugin.PluginRegistry import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.service.SystemSettingsService.SystemSettings import gitbucket.core.service.SystemSettingsService.SystemSettings
@@ -11,14 +11,14 @@ import gitbucket.core.util.JGitUtil.CommitInfo
import gitbucket.core.util.{JGitUtil, LockUtil} import gitbucket.core.util.{JGitUtil, LockUtil}
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.eclipse.jgit.dircache.{DirCache, DirCacheBuilder} import org.eclipse.jgit.dircache.{DirCache, DirCacheBuilder}
import org.eclipse.jgit.lib._ import org.eclipse.jgit.lib.*
import org.eclipse.jgit.transport.{ReceiveCommand, ReceivePack} import org.eclipse.jgit.transport.{ReceiveCommand, ReceivePack}
import scala.util.Using import scala.util.Using
trait RepositoryCommitFileService { trait RepositoryCommitFileService {
self: AccountService & ActivityService & IssuesService & PullRequestService & WebHookPullRequestService & self: AccountService & ActivityService & IssuesService & PullRequestService & WebHookPullRequestService &
RepositoryService => RepositoryService & ProtectedBranchService =>
/** /**
* Create multiple files by callback function. * Create multiple files by callback function.
@@ -92,10 +92,10 @@ trait RepositoryCommitFileService {
)(implicit s: Session, c: JsonFormat.Context): Either[String, (ObjectId, Option[ObjectId])] = { )(implicit s: Session, c: JsonFormat.Context): Either[String, (ObjectId, Option[ObjectId])] = {
val newPath = newFileName.map { newFileName => val newPath = newFileName.map { newFileName =>
if (path.length == 0) newFileName else s"${path}/${newFileName}" if (path.isEmpty) newFileName else s"${path}/${newFileName}"
} }
val oldPath = oldFileName.map { oldFileName => val oldPath = oldFileName.map { oldFileName =>
if (path.length == 0) oldFileName else s"${path}/${oldFileName}" if (path.isEmpty) oldFileName else s"${path}/${oldFileName}"
} }
_createFiles(repository, branch, message, pusherAccount, committerName, committerMailAddress, settings) { _createFiles(repository, branch, message, pusherAccount, committerName, committerMailAddress, settings) {
@@ -139,7 +139,6 @@ trait RepositoryCommitFileService {
)( )(
f: (Git, ObjectId, DirCacheBuilder, ObjectInserter) => R f: (Git, ObjectId, DirCacheBuilder, ObjectInserter) => R
)(implicit s: Session, c: JsonFormat.Context): Either[String, (ObjectId, R)] = { )(implicit s: Session, c: JsonFormat.Context): Either[String, (ObjectId, R)] = {
LockUtil.lock(s"${repository.owner}/${repository.name}") { LockUtil.lock(s"${repository.owner}/${repository.name}") {
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git => Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
val builder = DirCache.newInCore.builder() val builder = DirCache.newInCore.builder()
@@ -168,7 +167,14 @@ trait RepositoryCommitFileService {
// call pre-commit hook // call pre-commit hook
val error = PluginRegistry().getReceiveHooks.flatMap { hook => val error = PluginRegistry().getReceiveHooks.flatMap { hook =>
hook.preReceive(repository.owner, repository.name, receivePack, receiveCommand, pusherAccount.userName, false) hook.preReceive(
repository.owner,
repository.name,
receivePack,
receiveCommand,
pusherAccount.userName,
mergePullRequest = false
)
}.headOption }.headOption
error match { error match {
@@ -194,7 +200,8 @@ trait RepositoryCommitFileService {
// record activity // record activity
updateLastActivityDate(repository.owner, repository.name) updateLastActivityDate(repository.owner, repository.name)
val commitInfo = new CommitInfo(JGitUtil.getRevCommitFromId(git, commitId)) val commitInfo = new CommitInfo(JGitUtil.getRevCommitFromId(git, commitId))
val pushInfo = PushInfo(repository.owner, repository.name, pusherAccount.userName, branch, List(commitInfo)) val pushInfo =
PushInfo(repository.owner, repository.name, pusherAccount.userName, branch, List(commitInfo))
recordActivity(pushInfo) recordActivity(pushInfo)
// create issue comment by commit message // create issue comment by commit message
@@ -221,7 +228,14 @@ trait RepositoryCommitFileService {
// call post-commit hook // call post-commit hook
PluginRegistry().getReceiveHooks.foreach { hook => PluginRegistry().getReceiveHooks.foreach { hook =>
hook.postReceive(repository.owner, repository.name, receivePack, receiveCommand, committerName, false) hook.postReceive(
repository.owner,
repository.name,
receivePack,
receiveCommand,
committerName,
mergePullRequest = false
)
} }
val commit = new JGitUtil.CommitInfo(JGitUtil.getRevCommitFromId(git, commitId)) val commit = new JGitUtil.CommitInfo(JGitUtil.getRevCommitFromId(git, commitId))

View File

@@ -654,10 +654,10 @@ trait RepositoryService {
def hasOwnerRole(owner: String, repository: String, loginAccount: Option[Account])(implicit s: Session): Boolean = { def hasOwnerRole(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 (getGroupMembers(owner).exists(_.userName == a.userName)) => true case Some(a) if getGroupMembers(owner).exists(_.userName == a.userName) => true
case Some(a) if (getCollaboratorUserNames(owner, repository, Seq(Role.ADMIN)).contains(a.userName)) => true case Some(a) if getCollaboratorUserNames(owner, repository, Seq(Role.ADMIN)).contains(a.userName) => true
case _ => false case _ => false
} }
} }
@@ -666,11 +666,11 @@ trait RepositoryService {
s: Session s: Session
): Boolean = { ): 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 (getGroupMembers(owner).exists(_.userName == a.userName)) => true case Some(a) if getGroupMembers(owner).exists(_.userName == a.userName) => true
case Some(a) case Some(a)
if (getCollaboratorUserNames(owner, repository, Seq(Role.ADMIN, Role.DEVELOPER)).contains(a.userName)) => if getCollaboratorUserNames(owner, repository, Seq(Role.ADMIN, Role.DEVELOPER)).contains(a.userName) =>
true true
case _ => false case _ => false
} }
@@ -678,12 +678,12 @@ trait RepositoryService {
def hasGuestRole(owner: String, repository: String, loginAccount: Option[Account])(implicit s: Session): Boolean = { def hasGuestRole(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 (getGroupMembers(owner).exists(_.userName == a.userName)) => true case Some(a) if getGroupMembers(owner).exists(_.userName == a.userName) => true
case Some(a) case Some(a)
if (getCollaboratorUserNames(owner, repository, Seq(Role.ADMIN, Role.DEVELOPER, Role.GUEST)) if getCollaboratorUserNames(owner, repository, Seq(Role.ADMIN, Role.DEVELOPER, Role.GUEST))
.contains(a.userName)) => .contains(a.userName) =>
true true
case _ => false case _ => false
} }
@@ -694,17 +694,29 @@ trait RepositoryService {
true true
} else { } else {
loginAccount match { loginAccount match {
case Some(x) if (x.isAdmin) => true case Some(x) if x.isAdmin => true
case Some(x) if (repository.userName == x.userName) => true case Some(x) if repository.userName == x.userName => true
case Some(x) if (getGroupMembers(repository.userName).exists(_.userName == x.userName)) => true case Some(x) if getGroupMembers(repository.userName).exists(_.userName == x.userName) => true
case Some(x) case Some(x) if getCollaboratorUserNames(repository.userName, repository.repositoryName).contains(x.userName) =>
if (getCollaboratorUserNames(repository.userName, repository.repositoryName).contains(x.userName)) =>
true true
case _ => false case _ => false
} }
} }
} }
def isWritable(repository: Repository, loginAccount: Option[Account])(implicit s: Session): Boolean = {
loginAccount match {
case Some(x) if x.isAdmin => true
case Some(x) if repository.userName == x.userName => true
case Some(x) if getGroupMembers(repository.userName).exists(_.userName == x.userName) => true
case Some(x)
if getCollaboratorUserNames(repository.userName, repository.repositoryName, Seq(Role.ADMIN, Role.DEVELOPER))
.contains(x.userName) =>
true
case _ => false
}
}
private def getForkedCount(userName: String, repositoryName: String)(implicit s: Session): Int = private def getForkedCount(userName: String, repositoryName: String)(implicit s: Session): Int =
Query(Repositories.filter { t => Query(Repositories.filter { t =>
(t.originUserName === userName.bind) && (t.originRepositoryName === repositoryName.bind) (t.originUserName === userName.bind) && (t.originRepositoryName === repositoryName.bind)

View File

@@ -9,11 +9,11 @@ import gitbucket.core.api.JsonFormat.Context
import gitbucket.core.model.WebHook import gitbucket.core.model.WebHook
import gitbucket.core.plugin.{GitRepositoryRouting, PluginRegistry} import gitbucket.core.plugin.{GitRepositoryRouting, PluginRegistry}
import gitbucket.core.service.IssuesService.IssueSearchCondition import gitbucket.core.service.IssuesService.IssueSearchCondition
import gitbucket.core.service.WebHookService._ import gitbucket.core.service.WebHookService.*
import gitbucket.core.service._ import gitbucket.core.service.*
import gitbucket.core.util.Implicits._ import gitbucket.core.util.Implicits.*
import gitbucket.core.util._ import gitbucket.core.util.*
import gitbucket.core.model.Profile.profile.blockingApi._ import gitbucket.core.model.Profile.profile.blockingApi.*
import gitbucket.core.model.activity.{ import gitbucket.core.model.activity.{
BaseActivityInfo, BaseActivityInfo,
CloseIssueInfo, CloseIssueInfo,
@@ -33,9 +33,9 @@ import gitbucket.core.servlet.Database
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.eclipse.jgit.http.server.GitServlet import org.eclipse.jgit.http.server.GitServlet
import org.eclipse.jgit.lib._ import org.eclipse.jgit.lib.*
import org.eclipse.jgit.transport._ import org.eclipse.jgit.transport.*
import org.eclipse.jgit.transport.resolver._ import org.eclipse.jgit.transport.resolver.*
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import javax.servlet.ServletConfig import javax.servlet.ServletConfig
import javax.servlet.http.{HttpServletRequest, HttpServletResponse} import javax.servlet.http.{HttpServletRequest, HttpServletResponse}
@@ -43,7 +43,7 @@ import org.eclipse.jgit.diff.DiffEntry.ChangeType
import org.eclipse.jgit.internal.storage.file.FileRepository import org.eclipse.jgit.internal.storage.file.FileRepository
import org.json4s.Formats import org.json4s.Formats
import org.json4s.convertToJsonInput import org.json4s.convertToJsonInput
import org.json4s.jackson.Serialization._ import org.json4s.jackson.Serialization.*
/** /**
* Provides Git repository via HTTP. * Provides Git repository via HTTP.
@@ -117,7 +117,7 @@ class GitRepositoryServlet extends GitServlet with SystemSettingsService {
GitLfs.BatchResponseObject( GitLfs.BatchResponseObject(
requestObject.oid, requestObject.oid,
requestObject.size, requestObject.size,
true, authenticated = true,
GitLfs.Actions( GitLfs.Actions(
upload = Some( upload = Some(
GitLfs.Action( GitLfs.Action(
@@ -138,7 +138,7 @@ class GitRepositoryServlet extends GitServlet with SystemSettingsService {
GitLfs.BatchResponseObject( GitLfs.BatchResponseObject(
requestObject.oid, requestObject.oid,
requestObject.size, requestObject.size,
true, authenticated = true,
GitLfs.Actions( GitLfs.Actions(
download = Some( download = Some(
GitLfs.Action( GitLfs.Action(
@@ -223,7 +223,7 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
} }
} }
import scala.jdk.CollectionConverters._ import scala.jdk.CollectionConverters.*
class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: String, sshUrl: Option[String]) class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: String, sshUrl: Option[String])
extends PostReceiveHook extends PostReceiveHook
@@ -242,6 +242,7 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
with WebHookPullRequestReviewCommentService with WebHookPullRequestReviewCommentService
with CommitsService with CommitsService
with SystemSettingsService with SystemSettingsService
with ProtectedBranchService
with RequestCache { with RequestCache {
private val logger = LoggerFactory.getLogger(classOf[CommitLogHook]) private val logger = LoggerFactory.getLogger(classOf[CommitLogHook])
@@ -253,7 +254,7 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
commands.asScala.foreach { command => commands.asScala.foreach { command =>
// call pre-commit hook // call pre-commit hook
PluginRegistry().getReceiveHooks PluginRegistry().getReceiveHooks
.flatMap(_.preReceive(owner, repository, receivePack, command, pusher, false)) .flatMap(_.preReceive(owner, repository, receivePack, command, pusher, mergePullRequest = false))
.headOption .headOption
.foreach { error => .foreach { error =>
command.setResult(ReceiveCommand.Result.REJECTED_OTHER_REASON, error) command.setResult(ReceiveCommand.Result.REJECTED_OTHER_REASON, error)
@@ -428,8 +429,8 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
repositoryInfo, repositoryInfo,
newCommits, newCommits,
ownerAccount, ownerAccount,
newId = command.getNewId(), newId = command.getNewId,
oldId = command.getOldId() oldId = command.getOldId
) )
} }
} }
@@ -453,7 +454,7 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
// call post-commit hook // call post-commit hook
PluginRegistry().getReceiveHooks PluginRegistry().getReceiveHooks
.foreach(_.postReceive(owner, repository, receivePack, command, pusher, false)) .foreach(_.postReceive(owner, repository, receivePack, command, pusher, mergePullRequest = false))
} }
} }
// update repository last modified time. // update repository last modified time.

View File

@@ -15,8 +15,8 @@ trait OneselfAuthenticator { self: ControllerBase =>
private def authenticate(action: => Any) = { private def authenticate(action: => Any) = {
context.loginAccount match { context.loginAccount match {
case Some(x) if (x.isAdmin) => action case Some(x) if x.isAdmin => action
case Some(x) if (request.paths(0) == x.userName) => action case Some(x) if request.paths(0) == x.userName => action
case _ => Unauthorized() case _ => Unauthorized()
} }
} }
@@ -26,7 +26,7 @@ trait OneselfAuthenticator { self: ControllerBase =>
* Allows only the repository owner and administrators. * Allows only the repository owner and administrators.
*/ */
trait OwnerAuthenticator { self: ControllerBase & RepositoryService & AccountService => trait OwnerAuthenticator { self: ControllerBase & RepositoryService & AccountService =>
protected def ownerOnly(action: (RepositoryInfo) => Any) = { authenticate(action) } protected def ownerOnly(action: RepositoryInfo => Any) = { authenticate(action) }
protected def ownerOnly[T](action: (T, RepositoryInfo) => Any) = (form: T) => { authenticate(action(form, _)) } protected def ownerOnly[T](action: (T, RepositoryInfo) => Any) = (form: T) => { authenticate(action(form, _)) }
private def authenticate(action: (RepositoryInfo) => Any) = { private def authenticate(action: (RepositoryInfo) => Any) = {
@@ -34,14 +34,14 @@ trait OwnerAuthenticator { self: ControllerBase & RepositoryService & AccountSer
val repoName = params("repository") val repoName = params("repository")
getRepository(userName, repoName).map { repository => getRepository(userName, repoName).map { repository =>
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)
// TODO Repository management is allowed for only group managers? // TODO Repository management is allowed for only group managers?
case Some(x) if (getGroupMembers(repository.owner).exists { m => case Some(x) if getGroupMembers(repository.owner).exists { m =>
m.userName == x.userName && m.isManager m.userName == x.userName && m.isManager
}) => } =>
action(repository) action(repository)
case Some(x) if (getCollaboratorUserNames(userName, repoName, Seq(Role.ADMIN)).contains(x.userName)) => case Some(x) if getCollaboratorUserNames(userName, repoName, Seq(Role.ADMIN)).contains(x.userName) =>
action(repository) action(repository)
case _ => Unauthorized() case _ => Unauthorized()
} }
@@ -83,10 +83,10 @@ trait AdminAuthenticator { self: ControllerBase =>
* Allows only guests and signed in users who can access the repository. * Allows only guests and signed in users who can access the repository.
*/ */
trait ReferrerAuthenticator { self: ControllerBase & RepositoryService & AccountService => trait ReferrerAuthenticator { self: ControllerBase & RepositoryService & 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, _)) }
private def authenticate(action: (RepositoryInfo) => Any) = { private def authenticate(action: RepositoryInfo => Any) = {
val userName = params("owner") val userName = params("owner")
val repoName = params("repository") val repoName = params("repository")
getRepository(userName, repoName).map { repository => getRepository(userName, repoName).map { repository =>
@@ -103,12 +103,12 @@ trait ReferrerAuthenticator { self: ControllerBase & RepositoryService & Account
* Allows only signed in users who have read permission for the repository. * Allows only signed in users who have read permission for the repository.
*/ */
trait ReadableUsersAuthenticator { self: ControllerBase & RepositoryService & AccountService => trait ReadableUsersAuthenticator { self: ControllerBase & RepositoryService & 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) => { protected def readableUsersOnly[T](action: (T, RepositoryInfo) => Any) = (form: T) => {
authenticate(action(form, _)) authenticate(action(form, _))
} }
private def authenticate(action: (RepositoryInfo) => Any) = { private def authenticate(action: RepositoryInfo => Any) = {
val userName = params("owner") val userName = params("owner")
val repoName = params("repository") val repoName = params("repository")
getRepository(userName, repoName).map { repository => getRepository(userName, repoName).map { repository =>
@@ -125,24 +125,19 @@ trait ReadableUsersAuthenticator { self: ControllerBase & RepositoryService & Ac
* Allows only signed in users who have write permission for the repository. * Allows only signed in users who have write permission for the repository.
*/ */
trait WritableUsersAuthenticator { self: ControllerBase & RepositoryService & AccountService => trait WritableUsersAuthenticator { self: ControllerBase & RepositoryService & AccountService =>
protected def writableUsersOnly(action: (RepositoryInfo) => Any) = { authenticate(action) } protected def writableUsersOnly(action: RepositoryInfo => Any) = { authenticate(action) }
protected def writableUsersOnly[T](action: (T, RepositoryInfo) => Any) = (form: T) => { protected def writableUsersOnly[T](action: (T, RepositoryInfo) => Any) = (form: T) => {
authenticate(action(form, _)) authenticate(action(form, _))
} }
private def authenticate(action: (RepositoryInfo) => Any) = { private def authenticate(action: RepositoryInfo => Any) = {
val userName = params("owner") val userName = params("owner")
val repoName = params("repository") val repoName = params("repository")
getRepository(userName, repoName).map { repository => getRepository(userName, repoName).map { repository =>
context.loginAccount match { if (isWritable(repository.repository, context.loginAccount)) {
case Some(x) if (x.isAdmin) => action(repository)
case Some(x) if (userName == x.userName) => action(repository)
case Some(x) if (getGroupMembers(repository.owner).exists(_.userName == x.userName)) => action(repository)
case Some(x)
if (getCollaboratorUserNames(userName, repoName, Seq(Role.ADMIN, Role.DEVELOPER))
.contains(x.userName)) =>
action(repository) action(repository)
case _ => Unauthorized() } else {
Unauthorized()
} }
} getOrElse NotFound() } getOrElse NotFound()
} }
@@ -159,9 +154,9 @@ trait GroupManagerAuthenticator { self: ControllerBase & AccountService =>
context.loginAccount match { context.loginAccount match {
case Some(x) if x.isAdmin => action case Some(x) if x.isAdmin => action
case Some(x) if x.userName == request.paths(0) => action case Some(x) if x.userName == request.paths(0) => action
case Some(x) if (getGroupMembers(request.paths(0)).exists { member => case Some(x) if getGroupMembers(request.paths(0)).exists { member =>
member.userName == x.userName && member.isManager member.userName == x.userName && member.isManager
}) => } =>
action action
case _ => Unauthorized() case _ => Unauthorized()
} }

View File

@@ -1,6 +1,6 @@
@(repository: gitbucket.core.service.RepositoryService.RepositoryInfo, @(repository: gitbucket.core.service.RepositoryService.RepositoryInfo,
branch: String, branch: String,
protection: gitbucket.core.api.ApiBranchProtection, protection: gitbucket.core.api.ApiBranchProtectionResponse,
knownContexts: Seq[String], knownContexts: Seq[String],
info: Option[Any])(implicit context: gitbucket.core.controller.Context) info: Option[Any])(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers @import gitbucket.core.view.helpers
@@ -22,9 +22,44 @@
</label> </label>
<p class="help-block">Disables force-pushes to this branch and prevents it from being deleted.</p> <p class="help-block">Disables force-pushes to this branch and prevents it from being deleted.</p>
</div> </div>
<!--====================================================================-->
<!-- Enforce administrators -->
<!--====================================================================-->
<div class="checkbox js-enabled" style="display:none"> <div class="checkbox js-enabled" style="display:none">
<label> <label>
<input type="checkbox" name="has_required_statuses" onclick="update()" @check(protection.status.enforcement_level.name!="off") @if(knownContexts.isEmpty){disabled }> <input type="checkbox" name="enforce_for_admins" onclick="update()" @check(protection.enforce_admins.exists(_.enabled))>
<span class="strong">Include administrators</span>
</label>
<p class="help-block">Enforce restrictions even for repository administrators.</p>
</div>
<!--====================================================================-->
<!-- Push restrictions -->
<!--====================================================================-->
<div class="checkbox js-enabled" style="display:none">
<label>
<input type="checkbox" name="restrictions" onclick="update()" @check(protection.restrictions.isDefined)>
<span class="strong">Restrict users for push</span>
</label>
<p class="help-block">Restrict users who can push to this branch</p>
<div class="js-restrictions_enabled" style="display: none;">
<ul id="restrictions-user-list">
</ul>
@gitbucket.core.helper.html.account("userName-restrictions-user", 200, true, false)
<input type="button" class="btn btn-default add-restrictions-user" value="Add"/>
<div>
<span class="error" id="error-restrictions-user"></span>
</div>
</div>
</div>
<!--====================================================================-->
<!-- Status check -->
<!--====================================================================-->
<div class="checkbox js-enabled" style="display:none">
<label>
<input type="checkbox" name="has_required_statuses" onclick="update()" @check(protection.required_status_checks.isDefined) @if(knownContexts.isEmpty){disabled }>
<span class="strong">Require status checks to pass before merging</span> <span class="strong">Require status checks to pass before merging</span>
</label> </label>
<p class="help-block">When enabled, commits must first be pushed to another branch, then merged or pushed directly to <b>@branch</b> after status checks have passed.</p> <p class="help-block">When enabled, commits must first be pushed to another branch, then merged or pushed directly to <b>@branch</b> after status checks have passed.</p>
@@ -48,14 +83,6 @@
} }
</div> </div>
</div> </div>
<div class="checkbox">
<label>
<input type="checkbox" name="enforce_for_admins" onclick="update()" @check(protection.status.enforcement_level.name=="everyone")>
<span class="strong">Include administrators</span>
</label>
<p class="help-block">Enforce required status checks for repository administrators.</p>
</div>
</div> </div>
} }
</div> </div>
@@ -68,59 +95,114 @@
} }
<script> <script>
function getValue(){ function getValue(){
var v = {}, contexts=[]; const v = {}, contexts = [];
let restrictions = undefined;
$("input[type=checkbox]:checked").each(function(){ $("input[type=checkbox]:checked").each(function(){
if(this.name === 'contexts'){ if(this.name === 'contexts') {
contexts.push(this.value); contexts.push(this.value);
} else if (this.name === 'restrictions') {
restrictions = $('#restrictions-user-list li').map(function(i, e){
return $(e).data('name');
}).get();
} else { } else {
v[this.name] = true; v[this.name] = true;
} }
}); });
if(v.enabled){ if(v.enabled){
return { return {
enabled: true, enabled: true,
required_status_checks: { enforce_admins: v.enforce_for_admins,
enforcement_level: v.has_required_statuses ? ((v.enforce_for_admins ? 'everyone' : 'non_admins')) : 'off', required_status_checks: v.has_required_statuses ? { contexts: contexts } : undefined,
contexts: v.has_required_statuses ? contexts : [] restrictions: restrictions ? { users: restrictions } : undefined
}
}; };
} else { } else {
return { return {
enabled: false, enabled: false
required_status_checks: {
enforcement_level: "off",
contexts: []
}
}; };
} }
} }
function updateView(protection){ function updateView(protection){
$('.js-enabled').toggle(protection.enabled); $('.js-enabled').toggle(protection.enabled);
$('.js-has_required_statuses').toggle(protection.required_status_checks.enforcement_level != 'off'); $('.js-restrictions_enabled').toggle(protection.restrictions !== undefined);
$('.js-submit-btn').attr('disabled',protection.required_status_checks.enforcement_level != 'off' && protection.required_status_checks.contexts.length == 0); $('.js-has_required_statuses').toggle(protection.required_status_checks !== undefined);
} }
function update(){ function update(){
var protection = getValue(); const protection = getValue();
updateView(protection); updateView(protection);
} }
$(update);
function submitForm(e){ function submitForm(e){
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
var protection = getValue(); const protection = getValue();
$.ajax({ $.ajax({
method:'PATCH', method: 'PATCH',
url:'@context.path/api/v3/repos/@repository.owner/@repository.name/branches/@helpers.urlEncode(branch)', url: '@context.path/api/v3/repos/@repository.owner/@repository.name/branches/@helpers.urlEncode(branch)',
contentType: 'application/json', contentType: 'application/json',
dataType: 'json', dataType: 'json',
data:JSON.stringify({protection:protection}), data: JSON.stringify({protection: protection}),
success:function(r){ success: function(r){
$('#saved-info').show(); $('#saved-info').show();
}, },
error:function(err){ error: function(err){
console.log(err); console.log(err);
alert('update error'); alert('update error');
} }
}); });
} }
function addUserToListHTML(userName, id){
$(id).append($('<li>').data('name', userName)
.append(' ')
.append(userName)
.append($('<a href="#" onclick="$(this).parent().remove();" class="remove">(remove)</a>')));
}
$(function() {
// Initialize
update();
@protection.restrictions.map(_.users).map { users =>
@users.map { user =>
addUserToListHTML('@user', '#restrictions-user-list');
}
}
$('.add-restrictions-user').click(function(){
$('#error-restrictions-user').text('');
const userName = $('#userName-restrictions-user').val();
// check empty
if($.trim(userName) === ''){
return false;
}
// check duplication
const exists = $('#restrictions-user-list li').filter(function(){
return $(this).data('name') === userName;
}).length > 0;
if(exists){
$('#error-restrictions-user').text('User has been already added.');
return false;
}
// check existence
$.post('@context.path/_user/existence', {
'userName': userName,
'owner': '@repository.owner',
'repository': '@repository.name'
},
function(data, status){
if(data !== ''){
addUserToListHTML(userName, '#restrictions-user-list');
$('#userName-restrictions-user').val('');
} else {
$('#error-restrictions-user').text("User does not exist or isn't writable to this repository.");
}
});
});
})
</script> </script>

View File

@@ -1,8 +1,9 @@
package gitbucket.core.api package gitbucket.core.api
import java.util.{Calendar, Date, TimeZone} import gitbucket.core.api.ApiBranchProtectionResponse.Restrictions
import gitbucket.core.model._ import java.util.{Calendar, Date, TimeZone}
import gitbucket.core.model.*
import gitbucket.core.plugin.PluginInfo import gitbucket.core.plugin.PluginInfo
import gitbucket.core.service.ProtectedBranchService.ProtectedBranchInfo import gitbucket.core.service.ProtectedBranchService.ProtectedBranchInfo
import gitbucket.core.service.RepositoryService.RepositoryInfo import gitbucket.core.service.RepositoryService.RepositoryInfo
@@ -15,7 +16,7 @@ object ApiSpecModels {
implicit val context: JsonFormat.Context = JsonFormat.Context("http://gitbucket.exmple.com", None) implicit val context: JsonFormat.Context = JsonFormat.Context("http://gitbucket.exmple.com", None)
val date1 = { val date1: Date = {
val d = Calendar.getInstance(TimeZone.getTimeZone("UTC")) val d = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
d.set(2011, 3, 14, 16, 0, 49) d.set(2011, 3, 14, 16, 0, 49)
d.getTime d.getTime
@@ -29,7 +30,7 @@ object ApiSpecModels {
// Models // Models
val account = Account( val account: Account = Account(
userName = "octocat", userName = "octocat",
fullName = "octocat", fullName = "octocat",
mailAddress = "octocat@example.com", mailAddress = "octocat@example.com",
@@ -45,10 +46,10 @@ object ApiSpecModels {
description = None description = None
) )
val sha1 = "6dcb09b5b57875f334f61aebed695e2e4193db5e" val sha1: String = "6dcb09b5b57875f334f61aebed695e2e4193db5e"
val repo1Name = RepositoryName("octocat/Hello-World") val repo1Name: RepositoryName = RepositoryName("octocat/Hello-World")
val repository = Repository( val repository: Repository = Repository(
userName = repo1Name.owner, userName = repo1Name.owner,
repositoryName = repo1Name.name, repositoryName = repo1Name.name,
isPrivate = false, isPrivate = false,
@@ -73,7 +74,7 @@ object ApiSpecModels {
) )
) )
val repositoryInfo = RepositoryInfo( val repositoryInfo: RepositoryInfo = RepositoryInfo(
owner = repo1Name.owner, owner = repo1Name.owner,
name = repo1Name.name, name = repo1Name.name,
repository = repository, repository = repository,
@@ -101,7 +102,7 @@ object ApiSpecModels {
managers = Seq("myboss") managers = Seq("myboss")
) )
val label = Label( val label: Label = Label(
userName = repo1Name.owner, userName = repo1Name.owner,
repositoryName = repo1Name.name, repositoryName = repo1Name.name,
labelId = 10, labelId = 10,
@@ -109,7 +110,7 @@ object ApiSpecModels {
color = "f29513" color = "f29513"
) )
val issue = Issue( val issue: Issue = Issue(
userName = repo1Name.owner, userName = repo1Name.owner,
repositoryName = repo1Name.name, repositoryName = repo1Name.name,
issueId = 1347, issueId = 1347,
@@ -124,14 +125,14 @@ object ApiSpecModels {
isPullRequest = false isPullRequest = false
) )
val issuePR = issue.copy( val issuePR: Issue = issue.copy(
title = "new-feature", title = "new-feature",
content = Some("Please pull these awesome changes"), content = Some("Please pull these awesome changes"),
closed = true, closed = true,
isPullRequest = true isPullRequest = true
) )
val issueComment = IssueComment( val issueComment: IssueComment = IssueComment(
userName = repo1Name.owner, userName = repo1Name.owner,
repositoryName = repo1Name.name, repositoryName = repo1Name.name,
issueId = issue.issueId, issueId = issue.issueId,
@@ -143,7 +144,7 @@ object ApiSpecModels {
updatedDate = date1 updatedDate = date1
) )
val pullRequest = PullRequest( val pullRequest: PullRequest = PullRequest(
userName = repo1Name.owner, userName = repo1Name.owner,
repositoryName = repo1Name.name, repositoryName = repo1Name.name,
issueId = issuePR.issueId, issueId = issuePR.issueId,
@@ -156,7 +157,7 @@ object ApiSpecModels {
isDraft = true isDraft = true
) )
val commitComment = CommitComment( val commitComment: CommitComment = CommitComment(
userName = repo1Name.owner, userName = repo1Name.owner,
repositoryName = repo1Name.name, repositoryName = repo1Name.name,
commitId = sha1, commitId = sha1,
@@ -174,7 +175,7 @@ object ApiSpecModels {
originalNewLine = None originalNewLine = None
) )
val commitStatus = CommitStatus( val commitStatus: CommitStatus = CommitStatus(
commitStatusId = 1, commitStatusId = 1,
userName = repo1Name.owner, userName = repo1Name.owner,
repositoryName = repo1Name.name, repositoryName = repo1Name.name,
@@ -188,7 +189,7 @@ object ApiSpecModels {
updatedDate = date1 updatedDate = date1
) )
val milestone = Milestone( val milestone: Milestone = Milestone(
userName = repo1Name.owner, userName = repo1Name.owner,
repositoryName = repo1Name.name, repositoryName = repo1Name.name,
milestoneId = 1, milestoneId = 1,
@@ -200,28 +201,28 @@ object ApiSpecModels {
// APIs // APIs
val apiUser = ApiUser(account) val apiUser: ApiUser = ApiUser(account)
val apiRepository = ApiRepository( val apiRepository: ApiRepository = ApiRepository(
repository = repository, repository = repository,
owner = apiUser, owner = apiUser,
forkedCount = repositoryInfo.forkedCount, forkedCount = repositoryInfo.forkedCount,
watchers = 0 watchers = 0
) )
val apiLabel = ApiLabel( val apiLabel: ApiLabel = ApiLabel(
label = label, label = label,
repositoryName = repo1Name repositoryName = repo1Name
) )
val apiMilestone = ApiMilestone( val apiMilestone: ApiMilestone = ApiMilestone(
repository = repository, repository = repository,
milestone = milestone, milestone = milestone,
open_issue_count = 1, open_issue_count = 1,
closed_issue_count = 1 closed_issue_count = 1
) )
val apiIssue = ApiIssue( val apiIssue: ApiIssue = ApiIssue(
issue = issue, issue = issue,
repositoryName = repo1Name, repositoryName = repo1Name,
user = apiUser, user = apiUser,
@@ -230,7 +231,7 @@ object ApiSpecModels {
milestone = Some(apiMilestone) milestone = Some(apiMilestone)
) )
val apiNotAssignedIssue = ApiIssue( val apiNotAssignedIssue: ApiIssue = ApiIssue(
issue = issue, issue = issue,
repositoryName = repo1Name, repositoryName = repo1Name,
user = apiUser, user = apiUser,
@@ -239,7 +240,7 @@ object ApiSpecModels {
milestone = Some(apiMilestone) milestone = Some(apiMilestone)
) )
val apiIssuePR = ApiIssue( val apiIssuePR: ApiIssue = ApiIssue(
issue = issuePR, issue = issuePR,
repositoryName = repo1Name, repositoryName = repo1Name,
user = apiUser, user = apiUser,
@@ -248,7 +249,7 @@ object ApiSpecModels {
milestone = Some(apiMilestone) milestone = Some(apiMilestone)
) )
val apiComment = ApiComment( val apiComment: ApiComment = ApiComment(
comment = issueComment, comment = issueComment,
repositoryName = repo1Name, repositoryName = repo1Name,
issueId = issueComment.issueId, issueId = issueComment.issueId,
@@ -256,7 +257,7 @@ object ApiSpecModels {
isPullRequest = false isPullRequest = false
) )
val apiCommentPR = ApiComment( val apiCommentPR: ApiComment = ApiComment(
comment = issueComment, comment = issueComment,
repositoryName = repo1Name, repositoryName = repo1Name,
issueId = issueComment.issueId, issueId = issueComment.issueId,
@@ -264,7 +265,7 @@ object ApiSpecModels {
isPullRequest = true isPullRequest = true
) )
val apiPullRequest = ApiPullRequest( val apiPullRequest: ApiPullRequest = ApiPullRequest(
issue = issuePR, issue = issuePR,
pullRequest = pullRequest, pullRequest = pullRequest,
headRepo = apiRepository, headRepo = apiRepository,
@@ -275,15 +276,14 @@ object ApiSpecModels {
mergedComment = Some((issueComment, account)) mergedComment = Some((issueComment, account))
) )
// https://developer.github.com/v3/activity/events/types/#pullrequestreviewcommentevent val apiPullRequestReviewComment: ApiPullRequestReviewComment = ApiPullRequestReviewComment(
val apiPullRequestReviewComment = ApiPullRequestReviewComment(
comment = commitComment, comment = commitComment,
commentedUser = apiUser, commentedUser = apiUser,
repositoryName = repo1Name, repositoryName = repo1Name,
issueId = commitComment.issueId.get issueId = commitComment.issueId.get
) )
val commitInfo = (id: String) => val commitInfo: String => CommitInfo = (id: String) =>
CommitInfo( CommitInfo(
id = id, id = id,
shortMessage = "short message", shortMessage = "short message",
@@ -299,12 +299,12 @@ object ApiSpecModels {
None None
) )
val apiCommitListItem = ApiCommitListItem( val apiCommitListItem: ApiCommitListItem = ApiCommitListItem(
commit = commitInfo(sha1), commit = commitInfo(sha1),
repositoryName = repo1Name repositoryName = repo1Name
) )
val apiCommit = { val apiCommit: ApiCommit = {
val commit = commitInfo(sha1) val commit = commitInfo(sha1)
ApiCommit( ApiCommit(
id = commit.id, id = commit.id,
@@ -318,7 +318,7 @@ object ApiSpecModels {
)(repo1Name) )(repo1Name)
} }
val apiCommits = ApiCommits( val apiCommits: ApiCommits = ApiCommits(
repositoryName = repo1Name, repositoryName = repo1Name,
commitInfo = commitInfo(sha1), commitInfo = commitInfo(sha1),
diffs = Seq( diffs = Seq(
@@ -348,42 +348,45 @@ object ApiSpecModels {
commentCount = 2 commentCount = 2
) )
val apiCommitStatus = ApiCommitStatus( val apiCommitStatus: ApiCommitStatus = ApiCommitStatus(
status = commitStatus, status = commitStatus,
creator = apiUser creator = apiUser
) )
val apiCombinedCommitStatus = ApiCombinedCommitStatus( val apiCombinedCommitStatus: ApiCombinedCommitStatus = ApiCombinedCommitStatus(
sha = sha1, sha = sha1,
statuses = Iterable((commitStatus, account)), statuses = Iterable((commitStatus, account)),
repository = apiRepository repository = apiRepository
) )
val apiBranchProtectionOutput = ApiBranchProtection( val apiBranchProtectionOutput: ApiBranchProtectionResponse = ApiBranchProtectionResponse(
info = ProtectedBranchInfo( info = ProtectedBranchInfo(
owner = repo1Name.owner, owner = repo1Name.owner,
repository = repo1Name.name, repository = repo1Name.name,
branch = "main", branch = "main",
enabled = true, enabled = true,
contexts = Seq("continuous-integration/travis-ci"), contexts = Some(Seq("continuous-integration/travis-ci")),
includeAdministrators = true enforceAdmins = true,
restrictionsUsers = Some(Seq("admin"))
) )
) )
val apiBranchProtectionInput = new ApiBranchProtection( val apiBranchProtectionInput: ApiBranchProtectionResponse = new ApiBranchProtectionResponse(
url = None, url = None,
enabled = true, enabled = true,
required_status_checks = Some( required_status_checks = Some(
ApiBranchProtection.Status( ApiBranchProtectionResponse.Status(
url = None, url = None,
enforcement_level = ApiBranchProtection.Everyone, enforcement_level = ApiBranchProtectionResponse.Everyone,
contexts = Seq("continuous-integration/travis-ci"), contexts = Seq("continuous-integration/travis-ci"),
contexts_url = None contexts_url = None
) )
) ),
restrictions = Some(Restrictions(users = Seq("admin"))),
enforce_admins = None
) )
val apiBranch = ApiBranch( val apiBranch: ApiBranch = ApiBranch(
name = "main", name = "main",
commit = ApiBranchCommit(sha1), commit = ApiBranchCommit(sha1),
protection = apiBranchProtectionOutput protection = apiBranchProtectionOutput
@@ -391,12 +394,12 @@ object ApiSpecModels {
repositoryName = repo1Name repositoryName = repo1Name
) )
val apiBranchForList = ApiBranchForList( val apiBranchForList: ApiBranchForList = ApiBranchForList(
name = "main", name = "main",
commit = ApiBranchCommit(sha1) commit = ApiBranchCommit(sha1)
) )
val apiContents = ApiContents( val apiContents: ApiContents = ApiContents(
fileInfo = FileInfo( fileInfo = FileInfo(
id = ObjectId.fromString(sha1), id = ObjectId.fromString(sha1),
isDirectory = false, isDirectory = false,
@@ -413,14 +416,14 @@ object ApiSpecModels {
content = Some("README".getBytes("UTF-8")) content = Some("README".getBytes("UTF-8"))
) )
val apiEndPoint = ApiEndPoint() val apiEndPoint: ApiEndPoint = ApiEndPoint()
val apiError = ApiError( val apiError: ApiError = ApiError(
message = "A repository with this name already exists on this account", message = "A repository with this name already exists on this account",
documentation_url = Some("https://developer.github.com/v3/repos/#create") documentation_url = Some("https://developer.github.com/v3/repos/#create")
) )
val apiGroup = ApiGroup( val apiGroup: ApiGroup = ApiGroup(
account.copy( account.copy(
isAdmin = true, isAdmin = true,
isGroupAccount = true, isGroupAccount = true,
@@ -428,7 +431,7 @@ object ApiSpecModels {
) )
) )
val apiPlugin = ApiPlugin( val apiPlugin: ApiPlugin = ApiPlugin(
plugin = PluginInfo( plugin = PluginInfo(
pluginId = "gist", pluginId = "gist",
pluginName = "Gist Plugin", pluginName = "Gist Plugin",
@@ -441,12 +444,12 @@ object ApiSpecModels {
) )
) )
val apiPusher = ApiPusher(account) val apiPusher: ApiPusher = ApiPusher(account)
// have both urls as https, as the expected samples are using https // have both urls as https, as the expected samples are using https
val gitHubContext = JsonFormat.Context("https://api.github.com", Some("https://api.github.com")) val gitHubContext: JsonFormat.Context = JsonFormat.Context("https://api.github.com", Some("https://api.github.com"))
val apiRefHeadsMaster = ApiRef( val apiRefHeadsMaster: ApiRef = ApiRef(
ref = "refs/heads/main", ref = "refs/heads/main",
url = ApiPath("/repos/gitbucket/gitbucket/git/refs/heads/main"), url = ApiPath("/repos/gitbucket/gitbucket/git/refs/heads/main"),
node_id = "MDM6UmVmOTM1MDc0NjpyZWZzL2hlYWRzL21hc3Rlcg==", node_id = "MDM6UmVmOTM1MDc0NjpyZWZzL2hlYWRzL21hc3Rlcg==",
@@ -457,7 +460,7 @@ object ApiSpecModels {
) )
) )
val apiRefTag = ApiRef( val apiRefTag: ApiRef = ApiRef(
ref = "refs/tags/1.0", ref = "refs/tags/1.0",
url = ApiPath("/repos/gitbucket/gitbucket/git/refs/tags/1.0"), url = ApiPath("/repos/gitbucket/gitbucket/git/refs/tags/1.0"),
node_id = "MDM6UmVmOTM1MDc0NjpyZWZzL3RhZ3MvMS4w", node_id = "MDM6UmVmOTM1MDc0NjpyZWZzL3RhZ3MvMS4w",
@@ -468,9 +471,9 @@ object ApiSpecModels {
) )
) )
val assetFileName = "010203040a0b0c0d" val assetFileName: String = "010203040a0b0c0d"
val apiReleaseAsset = ApiReleaseAsset( val apiReleaseAsset: ApiReleaseAsset = ApiReleaseAsset(
name = "release.zip", name = "release.zip",
size = 100 size = 100
)( )(
@@ -479,7 +482,7 @@ object ApiSpecModels {
repositoryName = repo1Name repositoryName = repo1Name
) )
val apiRelease = ApiRelease( val apiRelease: ApiRelease = ApiRelease(
name = "release1", name = "release1",
tag_name = "tag1", tag_name = "tag1",
body = Some("content"), body = Some("content"),
@@ -489,7 +492,7 @@ object ApiSpecModels {
// JSON String for APIs // JSON String for APIs
val jsonUser = """{ val jsonUser: String = """{
|"login":"octocat", |"login":"octocat",
|"email":"octocat@example.com", |"email":"octocat@example.com",
|"type":"User", |"type":"User",
@@ -501,7 +504,7 @@ object ApiSpecModels {
|"avatar_url":"http://gitbucket.exmple.com/octocat/_avatar" |"avatar_url":"http://gitbucket.exmple.com/octocat/_avatar"
|}""".stripMargin |}""".stripMargin
val jsonRepository = s"""{ val jsonRepository: String = s"""{
|"name":"Hello-World", |"name":"Hello-World",
|"full_name":"octocat/Hello-World", |"full_name":"octocat/Hello-World",
|"description":"This your first repo!", |"description":"This your first repo!",
@@ -519,10 +522,10 @@ object ApiSpecModels {
|"html_url":"http://gitbucket.exmple.com/octocat/Hello-World" |"html_url":"http://gitbucket.exmple.com/octocat/Hello-World"
|}""".stripMargin |}""".stripMargin
val jsonLabel = val jsonLabel: String =
"""{"name":"bug","color":"f29513","url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/labels/bug"}""" """{"name":"bug","color":"f29513","url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/labels/bug"}"""
val jsonMilestone = """{ val jsonMilestone: String = """{
|"url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/milestones/1", |"url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/milestones/1",
|"html_url":"http://gitbucket.exmple.com/octocat/Hello-World/milestone/1", |"html_url":"http://gitbucket.exmple.com/octocat/Hello-World/milestone/1",
|"id":1, |"id":1,
@@ -535,7 +538,7 @@ object ApiSpecModels {
|"due_on":"2011-04-14T16:00:49Z" |"due_on":"2011-04-14T16:00:49Z"
|}""".stripMargin |}""".stripMargin
val jsonIssue = s"""{ val jsonIssue: String = s"""{
|"number":1347, |"number":1347,
|"title":"Found a bug", |"title":"Found a bug",
|"user":$jsonUser, |"user":$jsonUser,
@@ -552,7 +555,7 @@ object ApiSpecModels {
|"html_url":"http://gitbucket.exmple.com/octocat/Hello-World/issues/1347" |"html_url":"http://gitbucket.exmple.com/octocat/Hello-World/issues/1347"
|}""".stripMargin |}""".stripMargin
val jsonNotAssignedIssue = s"""{ val jsonNotAssignedIssue: String = s"""{
|"number":1347, |"number":1347,
|"title":"Found a bug", |"title":"Found a bug",
|"user":$jsonUser, |"user":$jsonUser,
@@ -568,7 +571,7 @@ object ApiSpecModels {
|"html_url":"http://gitbucket.exmple.com/octocat/Hello-World/issues/1347" |"html_url":"http://gitbucket.exmple.com/octocat/Hello-World/issues/1347"
|}""".stripMargin |}""".stripMargin
val jsonIssuePR = s"""{ val jsonIssuePR: String = s"""{
|"number":1347, |"number":1347,
|"title":"new-feature", |"title":"new-feature",
|"user":$jsonUser, |"user":$jsonUser,
@@ -588,7 +591,7 @@ object ApiSpecModels {
|"html_url":"http://gitbucket.exmple.com/octocat/Hello-World/pull/1347"} |"html_url":"http://gitbucket.exmple.com/octocat/Hello-World/pull/1347"}
|}""".stripMargin |}""".stripMargin
val jsonPullRequest = s"""{ val jsonPullRequest: String = s"""{
|"number":1347, |"number":1347,
|"state":"closed", |"state":"closed",
|"updated_at":"2011-04-14T16:00:49Z", |"updated_at":"2011-04-14T16:00:49Z",
@@ -615,7 +618,7 @@ object ApiSpecModels {
|"statuses_url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/statuses/6dcb09b5b57875f334f61aebed695e2e4193db5e" |"statuses_url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/statuses/6dcb09b5b57875f334f61aebed695e2e4193db5e"
|}""".stripMargin |}""".stripMargin
val jsonPullRequestReviewComment = s"""{ val jsonPullRequestReviewComment: String = s"""{
|"id":29724692, |"id":29724692,
|"path":"README.md", |"path":"README.md",
|"commit_id":"6dcb09b5b57875f334f61aebed695e2e4193db5e", |"commit_id":"6dcb09b5b57875f334f61aebed695e2e4193db5e",
@@ -632,7 +635,7 @@ object ApiSpecModels {
|"pull_request":{"href":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/pulls/1347"}} |"pull_request":{"href":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/pulls/1347"}}
|}""".stripMargin |}""".stripMargin
val jsonComment = s"""{ val jsonComment: String = s"""{
|"id":1, |"id":1,
|"user":$jsonUser, |"user":$jsonUser,
|"body":"Me too", |"body":"Me too",
@@ -641,7 +644,7 @@ object ApiSpecModels {
|"html_url":"http://gitbucket.exmple.com/octocat/Hello-World/issues/1347#comment-1" |"html_url":"http://gitbucket.exmple.com/octocat/Hello-World/issues/1347#comment-1"
|}""".stripMargin |}""".stripMargin
val jsonCommentPR = s"""{ val jsonCommentPR: String = s"""{
|"id":1, |"id":1,
|"user":$jsonUser, |"user":$jsonUser,
|"body":"Me too", |"body":"Me too",
@@ -650,7 +653,7 @@ object ApiSpecModels {
|"html_url":"http://gitbucket.exmple.com/octocat/Hello-World/pull/1347#comment-1" |"html_url":"http://gitbucket.exmple.com/octocat/Hello-World/pull/1347#comment-1"
|}""".stripMargin |}""".stripMargin
val jsonCommitListItem = s"""{ val jsonCommitListItem: String = s"""{
|"sha":"6dcb09b5b57875f334f61aebed695e2e4193db5e", |"sha":"6dcb09b5b57875f334f61aebed695e2e4193db5e",
|"commit":{ |"commit":{
|"message":"full message", |"message":"full message",
@@ -664,7 +667,7 @@ object ApiSpecModels {
|"url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e" |"url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e"
|}""".stripMargin |}""".stripMargin
val jsonCommit = (id: String) => s"""{ val jsonCommit: String => String = (id: String) => s"""{
|"id":"$id", |"id":"$id",
|"message":"full message", |"message":"full message",
|"timestamp":"2011-04-14T16:00:49Z", |"timestamp":"2011-04-14T16:00:49Z",
@@ -677,7 +680,7 @@ object ApiSpecModels {
|"html_url":"http://gitbucket.exmple.com/octocat/Hello-World/commit/$id" |"html_url":"http://gitbucket.exmple.com/octocat/Hello-World/commit/$id"
|}""".stripMargin |}""".stripMargin
val jsonCommits = s"""{ val jsonCommits: String = s"""{
|"url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e", |"url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e",
|"sha":"6dcb09b5b57875f334f61aebed695e2e4193db5e", |"sha":"6dcb09b5b57875f334f61aebed695e2e4193db5e",
|"html_url":"http://gitbucket.exmple.com/octocat/Hello-World/commit/6dcb09b5b57875f334f61aebed695e2e4193db5e", |"html_url":"http://gitbucket.exmple.com/octocat/Hello-World/commit/6dcb09b5b57875f334f61aebed695e2e4193db5e",
@@ -707,7 +710,7 @@ object ApiSpecModels {
|"patch":"@@ -1 +1,2 @@\\n-body1\\n\\\\ No newline at end of file\\n+body1\\n+body2\\n\\\\ No newline at end of file"}] |"patch":"@@ -1 +1,2 @@\\n-body1\\n\\\\ No newline at end of file\\n+body1\\n+body2\\n\\\\ No newline at end of file"}]
|}""".stripMargin |}""".stripMargin
val jsonCommitStatus = s"""{ val jsonCommitStatus: String = s"""{
|"created_at":"2011-04-14T16:00:49Z", |"created_at":"2011-04-14T16:00:49Z",
|"updated_at":"2011-04-14T16:00:49Z", |"updated_at":"2011-04-14T16:00:49Z",
|"state":"success", |"state":"success",
@@ -719,7 +722,7 @@ object ApiSpecModels {
|"url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e/statuses" |"url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e/statuses"
|}""".stripMargin |}""".stripMargin
val jsonCombinedCommitStatus = s"""{ val jsonCombinedCommitStatus: String = s"""{
|"state":"success", |"state":"success",
|"sha":"6dcb09b5b57875f334f61aebed695e2e4193db5e", |"sha":"6dcb09b5b57875f334f61aebed695e2e4193db5e",
|"total_count":1, |"total_count":1,
@@ -728,7 +731,7 @@ object ApiSpecModels {
|"url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e/status" |"url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e/status"
|}""".stripMargin |}""".stripMargin
val jsonBranchProtectionOutput = val jsonBranchProtectionOutput: String =
"""{ """{
|"url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/branches/main/protection", |"url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/branches/main/protection",
|"enabled":true, |"enabled":true,
@@ -736,15 +739,25 @@ object ApiSpecModels {
|"url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/branches/main/protection/required_status_checks", |"url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/branches/main/protection/required_status_checks",
|"enforcement_level":"everyone", |"enforcement_level":"everyone",
|"contexts":["continuous-integration/travis-ci"], |"contexts":["continuous-integration/travis-ci"],
|"contexts_url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/branches/main/protection/required_status_checks/contexts"} |"contexts_url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/branches/main/protection/required_status_checks/contexts"
|},
|"restrictions":{
|"users":["admin"]
|},
|"enforce_admins":{
|"enabled":true
|}
|}""".stripMargin |}""".stripMargin
val jsonBranchProtectionInput = val jsonBranchProtectionInput: String =
"""{ """{
|"enabled":true, |"enabled":true,
|"required_status_checks":{ |"required_status_checks":{
|"enforcement_level":"everyone", |"enforcement_level":"everyone",
|"contexts":["continuous-integration/travis-ci"] |"contexts":["continuous-integration/travis-ci"]
|},
|"restrictions":{
|"users":["admin"]
|} |}
|}""".stripMargin |}""".stripMargin
@@ -757,9 +770,9 @@ object ApiSpecModels {
|"html":"http://gitbucket.exmple.com/octocat/Hello-World/tree/main"} |"html":"http://gitbucket.exmple.com/octocat/Hello-World/tree/main"}
|}""".stripMargin |}""".stripMargin
val jsonBranchForList = """{"name":"main","commit":{"sha":"6dcb09b5b57875f334f61aebed695e2e4193db5e"}}""" val jsonBranchForList: String = """{"name":"main","commit":{"sha":"6dcb09b5b57875f334f61aebed695e2e4193db5e"}}"""
val jsonContents = val jsonContents: String =
"""{ """{
|"type":"file", |"type":"file",
|"name":"README.md", |"name":"README.md",
@@ -770,14 +783,14 @@ object ApiSpecModels {
|"download_url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/raw/6dcb09b5b57875f334f61aebed695e2e4193db5e/doc/README.md" |"download_url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/raw/6dcb09b5b57875f334f61aebed695e2e4193db5e/doc/README.md"
|}""".stripMargin |}""".stripMargin
val jsonEndPoint = """{"rate_limit_url":"http://gitbucket.exmple.com/api/v3/rate_limit"}""" val jsonEndPoint: String = """{"rate_limit_url":"http://gitbucket.exmple.com/api/v3/rate_limit"}"""
val jsonError = """{ val jsonError: String = """{
|"message":"A repository with this name already exists on this account", |"message":"A repository with this name already exists on this account",
|"documentation_url":"https://developer.github.com/v3/repos/#create" |"documentation_url":"https://developer.github.com/v3/repos/#create"
|}""".stripMargin |}""".stripMargin
val jsonGroup = """{ val jsonGroup: String = """{
|"login":"octocat", |"login":"octocat",
|"description":"Admin group", |"description":"Admin group",
|"created_at":"2011-04-14T16:00:49Z", |"created_at":"2011-04-14T16:00:49Z",
@@ -787,7 +800,7 @@ object ApiSpecModels {
|"avatar_url":"http://gitbucket.exmple.com/octocat/_avatar" |"avatar_url":"http://gitbucket.exmple.com/octocat/_avatar"
|}""".stripMargin |}""".stripMargin
val jsonPlugin = """{ val jsonPlugin: String = """{
|"id":"gist", |"id":"gist",
|"name":"Gist Plugin", |"name":"Gist Plugin",
|"version":"4.16.0", |"version":"4.16.0",
@@ -795,12 +808,12 @@ object ApiSpecModels {
|"jarFileName":"gitbucket-gist-plugin-gitbucket_4.30.0-SNAPSHOT-4.17.0.jar" |"jarFileName":"gitbucket-gist-plugin-gitbucket_4.30.0-SNAPSHOT-4.17.0.jar"
|}""".stripMargin |}""".stripMargin
val jsonPusher = """{"name":"octocat","email":"octocat@example.com"}""" val jsonPusher: String = """{"name":"octocat","email":"octocat@example.com"}"""
// I checked all refs in gitbucket repo, and there appears to be only type "commit" and type "tag" // I checked all refs in gitbucket repo, and there appears to be only type "commit" and type "tag"
val jsonRef = """{"ref":"refs/heads/featureA","object":{"sha":"6dcb09b5b57875f334f61aebed695e2e4193db5e"}}""" val jsonRef: String = """{"ref":"refs/heads/featureA","object":{"sha":"6dcb09b5b57875f334f61aebed695e2e4193db5e"}}"""
val jsonRefHeadsMain = val jsonRefHeadsMain: String =
"""{ """{
|"ref": "refs/heads/main", |"ref": "refs/heads/main",
|"node_id": "MDM6UmVmOTM1MDc0NjpyZWZzL2hlYWRzL21hc3Rlcg==", |"node_id": "MDM6UmVmOTM1MDc0NjpyZWZzL2hlYWRzL21hc3Rlcg==",
@@ -812,7 +825,7 @@ object ApiSpecModels {
|} |}
|}""".stripMargin |}""".stripMargin
val jsonRefTag = val jsonRefTag: String =
"""{ """{
|"ref": "refs/tags/1.0", |"ref": "refs/tags/1.0",
|"node_id": "MDM6UmVmOTM1MDc0NjpyZWZzL3RhZ3MvMS4w", |"node_id": "MDM6UmVmOTM1MDc0NjpyZWZzL3RhZ3MvMS4w",
@@ -824,7 +837,7 @@ object ApiSpecModels {
|} |}
|}""".stripMargin |}""".stripMargin
val jsonReleaseAsset = val jsonReleaseAsset: String =
s"""{ s"""{
|"name":"release.zip", |"name":"release.zip",
|"size":100, |"size":100,
@@ -833,7 +846,7 @@ object ApiSpecModels {
|"browser_download_url":"http://gitbucket.exmple.com/octocat/Hello-World/releases/tag1/assets/${assetFileName}" |"browser_download_url":"http://gitbucket.exmple.com/octocat/Hello-World/releases/tag1/assets/${assetFileName}"
|}""".stripMargin |}""".stripMargin
val jsonRelease = val jsonRelease: String =
s"""{ s"""{
|"name":"release1", |"name":"release1",
|"tag_name":"tag1", |"tag_name":"tag1",

View File

@@ -1,19 +1,19 @@
package gitbucket.core.api package gitbucket.core.api
import org.json4s.Formats import org.json4s.{Formats, JValue, jvalue2extractable}
import org.json4s.jackson.JsonMethods import org.json4s.jackson.JsonMethods
import org.json4s.jvalue2extractable import org.scalatest.Assertion
import org.scalatest.funsuite.AnyFunSuite import org.scalatest.funsuite.AnyFunSuite
class JsonFormatSpec extends AnyFunSuite { class JsonFormatSpec extends AnyFunSuite {
import ApiSpecModels._ import ApiSpecModels.*
implicit val format: Formats = JsonFormat.jsonFormats implicit val format: Formats = JsonFormat.jsonFormats
private def expected(json: String) = json.replaceAll("\n", "") private def expected(json: String) = json.replaceAll("\n", "")
def normalizeJson(json: String) = { def normalizeJson(json: String): JValue = {
org.json4s.jackson.parseJson(json) org.json4s.jackson.parseJson(json)
} }
def assertEqualJson(actual: String, expected: String) = { def assertEqualJson(actual: String, expected: String): Assertion = {
assert(normalizeJson(actual) == normalizeJson(expected)) assert(normalizeJson(actual) == normalizeJson(expected))
} }
@@ -57,7 +57,9 @@ class JsonFormatSpec extends AnyFunSuite {
assert(JsonFormat(apiBranchProtectionOutput) == expected(jsonBranchProtectionOutput)) assert(JsonFormat(apiBranchProtectionOutput) == expected(jsonBranchProtectionOutput))
} }
test("deserialize apiBranchProtection") { test("deserialize apiBranchProtection") {
assert(JsonMethods.parse(jsonBranchProtectionInput).extract[ApiBranchProtection] == apiBranchProtectionInput) assert(
JsonMethods.parse(jsonBranchProtectionInput).extract[ApiBranchProtectionResponse] == apiBranchProtectionInput
)
} }
test("apiBranch") { test("apiBranch") {
assert(JsonFormat(apiBranch) == expected(jsonBranch)) assert(JsonFormat(apiBranch) == expected(jsonBranch))

View File

@@ -29,26 +29,28 @@ class ProtectedBranchServiceSpec
it("should enable and update and disable") { it("should enable and update and disable") {
withTestDB { implicit session => withTestDB { implicit session =>
generateNewUserWithDBRepository("user1", "repo1") generateNewUserWithDBRepository("user1", "repo1")
enableBranchProtection("user1", "repo1", "branch", false, Nil) enableBranchProtection("user1", "repo1", "branch", false, false, Nil, false, Nil)
assert( assert(
getProtectedBranchInfo("user1", "repo1", "branch") == ProtectedBranchInfo( getProtectedBranchInfo("user1", "repo1", "branch") == ProtectedBranchInfo(
"user1", "user1",
"repo1", "repo1",
"branch", "branch",
true, true,
Nil, None,
false false,
None
) )
) )
enableBranchProtection("user1", "repo1", "branch", true, Seq("hoge", "huge")) enableBranchProtection("user1", "repo1", "branch", true, true, Seq("hoge", "huge"), false, Nil)
assert( assert(
getProtectedBranchInfo("user1", "repo1", "branch") == ProtectedBranchInfo( getProtectedBranchInfo("user1", "repo1", "branch") == ProtectedBranchInfo(
"user1", "user1",
"repo1", "repo1",
"branch", "branch",
true, enabled = true,
Seq("hoge", "huge"), contexts = Some(Seq("hoge", "huge")),
true enforceAdmins = true,
restrictionsUsers = None
) )
) )
disableBranchProtection("user1", "repo1", "branch") disableBranchProtection("user1", "repo1", "branch")
@@ -57,21 +59,21 @@ class ProtectedBranchServiceSpec
) )
} }
} }
it("should empty contexts is no-include-administrators") { it("should empty contexts is include-administrators") {
withTestDB { implicit session => withTestDB { implicit session =>
generateNewUserWithDBRepository("user1", "repo1") generateNewUserWithDBRepository("user1", "repo1")
enableBranchProtection("user1", "repo1", "branch", false, Nil) enableBranchProtection("user1", "repo1", "branch", false, false, Nil, false, Nil)
assert(getProtectedBranchInfo("user1", "repo1", "branch").includeAdministrators == false) assert(!getProtectedBranchInfo("user1", "repo1", "branch").enforceAdmins)
enableBranchProtection("user1", "repo1", "branch", true, Nil) enableBranchProtection("user1", "repo1", "branch", true, false, Nil, false, Nil)
assert(getProtectedBranchInfo("user1", "repo1", "branch").includeAdministrators == false) assert(getProtectedBranchInfo("user1", "repo1", "branch").enforceAdmins)
} }
} }
it("getProtectedBranchList") { it("getProtectedBranchList") {
withTestDB { implicit session => withTestDB { implicit session =>
generateNewUserWithDBRepository("user1", "repo1") generateNewUserWithDBRepository("user1", "repo1")
enableBranchProtection("user1", "repo1", "branch", false, Nil) enableBranchProtection("user1", "repo1", "branch", false, false, Nil, false, Nil)
enableBranchProtection("user1", "repo1", "branch2", false, Seq("fuga")) enableBranchProtection("user1", "repo1", "branch2", false, false, Seq("fuga"), false, Nil)
enableBranchProtection("user1", "repo1", "branch3", true, Seq("hoge")) enableBranchProtection("user1", "repo1", "branch3", true, false, Seq("hoge"), false, Nil)
assert(getProtectedBranchList("user1", "repo1").toSet == Set("branch", "branch2", "branch3")) assert(getProtectedBranchList("user1", "repo1").toSet == Set("branch", "branch2", "branch3"))
} }
} }
@@ -87,12 +89,12 @@ class ProtectedBranchServiceSpec
ReceiveCommand.Type.UPDATE_NONFASTFORWARD ReceiveCommand.Type.UPDATE_NONFASTFORWARD
) )
generateNewUserWithDBRepository("user1", "repo1") generateNewUserWithDBRepository("user1", "repo1")
assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user1", false) == None) assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user1", false).isEmpty)
enableBranchProtection("user1", "repo1", "branch", false, Nil) enableBranchProtection("user1", "repo1", "branch", false, false, Nil, false, Nil)
assert( assert(
receiveHook.preReceive("user1", "repo1", rp, rc, "user1", false) == Some( receiveHook
"Cannot force-push to a protected branch" .preReceive("user1", "repo1", rp, rc, "user1", false)
) .contains("Cannot force-push to a protected branch")
) )
} }
} }
@@ -109,12 +111,12 @@ class ProtectedBranchServiceSpec
ReceiveCommand.Type.UPDATE_NONFASTFORWARD ReceiveCommand.Type.UPDATE_NONFASTFORWARD
) )
generateNewUserWithDBRepository("user1", "repo1") generateNewUserWithDBRepository("user1", "repo1")
assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user2", false) == None) assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user2", false).isEmpty)
enableBranchProtection("user1", "repo1", "branch", false, Nil) enableBranchProtection("user1", "repo1", "branch", false, false, Nil, false, Nil)
assert( assert(
receiveHook.preReceive("user1", "repo1", rp, rc, "user2", false) == Some( receiveHook
"Cannot force-push to a protected branch" .preReceive("user1", "repo1", rp, rc, "user2", false)
) .contains("Cannot force-push to a protected branch")
) )
} }
} }
@@ -131,33 +133,33 @@ class ProtectedBranchServiceSpec
ReceiveCommand.Type.UPDATE ReceiveCommand.Type.UPDATE
) )
val user1 = generateNewUserWithDBRepository("user1", "repo1") val user1 = generateNewUserWithDBRepository("user1", "repo1")
assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user2", false) == None) assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user2", false).isEmpty)
enableBranchProtection("user1", "repo1", "branch", false, Seq("must")) enableBranchProtection("user1", "repo1", "branch", false, true, Seq("must"), false, Nil)
assert( assert(
receiveHook.preReceive("user1", "repo1", rp, rc, "user2", false) == Some( receiveHook
"Required status check \"must\" is expected" .preReceive("user1", "repo1", rp, rc, "user2", false)
.contains("Required status check \"must\" is expected")
) )
) enableBranchProtection("user1", "repo1", "branch", false, true, Seq("must", "must2"), false, Nil)
enableBranchProtection("user1", "repo1", "branch", false, Seq("must", "must2"))
assert( assert(
receiveHook.preReceive("user1", "repo1", rp, rc, "user2", false) == Some( receiveHook
"2 of 2 required status checks are expected" .preReceive("user1", "repo1", rp, rc, "user2", false)
) .contains("2 of 2 required status checks are expected")
) )
createCommitStatus("user1", "repo1", sha2, "context", CommitState.SUCCESS, None, None, now, user1) createCommitStatus("user1", "repo1", sha2, "context", CommitState.SUCCESS, None, None, now, user1)
assert( assert(
receiveHook.preReceive("user1", "repo1", rp, rc, "user2", false) == Some( receiveHook
"2 of 2 required status checks are expected" .preReceive("user1", "repo1", rp, rc, "user2", false)
) .contains("2 of 2 required status checks are expected")
) )
createCommitStatus("user1", "repo1", sha2, "must", CommitState.SUCCESS, None, None, now, user1) createCommitStatus("user1", "repo1", sha2, "must", CommitState.SUCCESS, None, None, now, user1)
assert( assert(
receiveHook.preReceive("user1", "repo1", rp, rc, "user2", false) == Some( receiveHook
"Required status check \"must2\" is expected" .preReceive("user1", "repo1", rp, rc, "user2", false)
) .contains("Required status check \"must2\" is expected")
) )
createCommitStatus("user1", "repo1", sha2, "must2", CommitState.SUCCESS, None, None, now, user1) createCommitStatus("user1", "repo1", sha2, "must2", CommitState.SUCCESS, None, None, now, user1)
assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user2", false) == None) assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user2", false).isEmpty)
} }
} }
} }
@@ -173,37 +175,60 @@ class ProtectedBranchServiceSpec
ReceiveCommand.Type.UPDATE ReceiveCommand.Type.UPDATE
) )
val user1 = generateNewUserWithDBRepository("user1", "repo1") val user1 = generateNewUserWithDBRepository("user1", "repo1")
assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user1", false) == None) assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user1", false).isEmpty)
enableBranchProtection("user1", "repo1", "branch", false, Seq("must")) enableBranchProtection("user1", "repo1", "branch", false, true, Seq("must"), false, Nil)
assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user1", false) == None) assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user1", false).isEmpty)
enableBranchProtection("user1", "repo1", "branch", true, Seq("must")) enableBranchProtection("user1", "repo1", "branch", true, true, Seq("must"), false, Nil)
assert( assert(
receiveHook.preReceive("user1", "repo1", rp, rc, "user1", false) == Some( receiveHook
"Required status check \"must\" is expected" .preReceive("user1", "repo1", rp, rc, "user1", false)
.contains("Required status check \"must\" is expected")
) )
) enableBranchProtection("user1", "repo1", "branch", false, true, Seq("must", "must2"), false, Nil)
enableBranchProtection("user1", "repo1", "branch", false, Seq("must", "must2")) assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user1", false).isEmpty)
assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user1", false) == None) enableBranchProtection("user1", "repo1", "branch", true, true, Seq("must", "must2"), false, Nil)
enableBranchProtection("user1", "repo1", "branch", true, Seq("must", "must2"))
assert( assert(
receiveHook.preReceive("user1", "repo1", rp, rc, "user1", false) == Some( receiveHook
"2 of 2 required status checks are expected" .preReceive("user1", "repo1", rp, rc, "user1", false)
) .contains("2 of 2 required status checks are expected")
) )
createCommitStatus("user1", "repo1", sha2, "context", CommitState.SUCCESS, None, None, now, user1) createCommitStatus("user1", "repo1", sha2, "context", CommitState.SUCCESS, None, None, now, user1)
assert( assert(
receiveHook.preReceive("user1", "repo1", rp, rc, "user1", false) == Some( receiveHook
"2 of 2 required status checks are expected" .preReceive("user1", "repo1", rp, rc, "user1", false)
) .contains("2 of 2 required status checks are expected")
) )
createCommitStatus("user1", "repo1", sha2, "must", CommitState.SUCCESS, None, None, now, user1) createCommitStatus("user1", "repo1", sha2, "must", CommitState.SUCCESS, None, None, now, user1)
assert( assert(
receiveHook.preReceive("user1", "repo1", rp, rc, "user1", false) == Some( receiveHook
"Required status check \"must2\" is expected" .preReceive("user1", "repo1", rp, rc, "user1", false)
) .contains("Required status check \"must2\" is expected")
) )
createCommitStatus("user1", "repo1", sha2, "must2", CommitState.SUCCESS, None, None, now, user1) createCommitStatus("user1", "repo1", sha2, "must2", CommitState.SUCCESS, None, None, now, user1)
assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user1", false) == None) assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user1", false).isEmpty)
}
}
}
it("should restrict push to allowed users only") {
withTestDB { implicit session =>
withTestRepository { git =>
val rp = new ReceivePack(git.getRepository)
rp.setAllowNonFastForwards(true)
val rc = new ReceiveCommand(
ObjectId.fromString(sha),
ObjectId.fromString(sha2),
"refs/heads/branch",
ReceiveCommand.Type.UPDATE
)
generateNewUserWithDBRepository("user1", "repo1")
generateNewAccount("user2")
enableBranchProtection("user1", "repo1", "branch", false, false, Nil, true, Seq("user2"))
assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user2", false).isEmpty)
assert(
receiveHook
.preReceive("user1", "repo1", rp, rc, "user3", false)
.contains("You do not have permission to push to this branch")
)
} }
} }
} }
@@ -212,29 +237,53 @@ class ProtectedBranchServiceSpec
it("administrator is owner") { it("administrator is owner") {
withTestDB { implicit session => withTestDB { implicit session =>
generateNewUserWithDBRepository("user1", "repo1") generateNewUserWithDBRepository("user1", "repo1")
val x = ProtectedBranchInfo("user1", "repo1", "branch", true, Nil, false) val x = ProtectedBranchInfo(
assert(x.isAdministrator("user1") == true) "user1",
assert(x.isAdministrator("user2") == false) "repo1",
"branch",
enabled = true,
contexts = Some(Nil),
enforceAdmins = false,
restrictionsUsers = None
)
assert(x.isAdministrator("user1"))
assert(!x.isAdministrator("user2"))
} }
} }
it("administrator is manager") { it("administrator is manager") {
withTestDB { implicit session => withTestDB { implicit session =>
val x = ProtectedBranchInfo("grp1", "repo1", "branch", true, Nil, false) val x = ProtectedBranchInfo(
"grp1",
"repo1",
"branch",
enabled = true,
contexts = Some(Nil),
enforceAdmins = false,
restrictionsUsers = None
)
x.createGroup("grp1", None, None) x.createGroup("grp1", None, None)
generateNewAccount("user1") generateNewAccount("user1")
generateNewAccount("user2") generateNewAccount("user2")
generateNewAccount("user3") generateNewAccount("user3")
x.updateGroupMembers("grp1", List("user1" -> true, "user2" -> false)) x.updateGroupMembers("grp1", List("user1" -> true, "user2" -> false))
assert(x.isAdministrator("user1") == true) assert(x.isAdministrator("user1"))
assert(x.isAdministrator("user2") == false) assert(!x.isAdministrator("user2"))
assert(x.isAdministrator("user3") == false) assert(!x.isAdministrator("user3"))
} }
} }
it("unSuccessedContexts") { it("unSuccessedContexts") {
withTestDB { implicit session => withTestDB { implicit session =>
val user1 = generateNewUserWithDBRepository("user1", "repo1") val user1 = generateNewUserWithDBRepository("user1", "repo1")
val x = ProtectedBranchInfo("user1", "repo1", "branch", true, List("must"), false) val x = ProtectedBranchInfo(
"user1",
"repo1",
"branch",
enabled = true,
contexts = Some(List("must")),
enforceAdmins = false,
restrictionsUsers = None
)
assert(x.unSuccessedContexts(sha) == Set("must")) assert(x.unSuccessedContexts(sha) == Set("must"))
createCommitStatus("user1", "repo1", sha, "context", CommitState.SUCCESS, None, None, now, user1) createCommitStatus("user1", "repo1", sha, "context", CommitState.SUCCESS, None, None, now, user1)
assert(x.unSuccessedContexts(sha) == Set("must")) assert(x.unSuccessedContexts(sha) == Set("must"))
@@ -251,7 +300,15 @@ class ProtectedBranchServiceSpec
it("unSuccessedContexts when empty") { it("unSuccessedContexts when empty") {
withTestDB { implicit session => withTestDB { implicit session =>
val user1 = generateNewUserWithDBRepository("user1", "repo1") val user1 = generateNewUserWithDBRepository("user1", "repo1")
val x = ProtectedBranchInfo("user1", "repo1", "branch", true, Nil, false) val x = ProtectedBranchInfo(
"user1",
"repo1",
"branch",
enabled = true,
contexts = Some(Nil),
enforceAdmins = false,
restrictionsUsers = None
)
val sha = "0c77148632618b59b6f70004e3084002be2b8804" val sha = "0c77148632618b59b6f70004e3084002be2b8804"
assert(x.unSuccessedContexts(sha) == Set()) assert(x.unSuccessedContexts(sha) == Set())
createCommitStatus("user1", "repo1", sha, "context", CommitState.SUCCESS, None, None, now, user1) createCommitStatus("user1", "repo1", sha, "context", CommitState.SUCCESS, None, None, now, user1)
@@ -261,23 +318,63 @@ class ProtectedBranchServiceSpec
it("if disabled, needStatusCheck is false") { it("if disabled, needStatusCheck is false") {
withTestDB { implicit session => withTestDB { implicit session =>
assert( assert(
ProtectedBranchInfo("user1", "repo1", "branch", false, Seq("must"), true).needStatusCheck("user1") == false !ProtectedBranchInfo(
"user1",
"repo1",
"branch",
enabled = false,
contexts = Some(Seq("must")),
enforceAdmins = true,
restrictionsUsers = None
).needStatusCheck("user1")
) )
} }
} }
it("needStatusCheck includeAdministrators") { it("needStatusCheck includeAdministrators") {
withTestDB { implicit session => withTestDB { implicit session =>
assert( assert(
ProtectedBranchInfo("user1", "repo1", "branch", true, Seq("must"), false).needStatusCheck("user2") == true ProtectedBranchInfo(
"user1",
"repo1",
"branch",
enabled = true,
contexts = Some(Seq("must")),
enforceAdmins = false,
restrictionsUsers = None
).needStatusCheck("user2")
) )
assert( assert(
ProtectedBranchInfo("user1", "repo1", "branch", true, Seq("must"), false).needStatusCheck("user1") == false !ProtectedBranchInfo(
"user1",
"repo1",
"branch",
enabled = true,
contexts = Some(Seq("must")),
enforceAdmins = false,
restrictionsUsers = None
).needStatusCheck("user1")
) )
assert( assert(
ProtectedBranchInfo("user1", "repo1", "branch", true, Seq("must"), true).needStatusCheck("user2") == true ProtectedBranchInfo(
"user1",
"repo1",
"branch",
enabled = true,
contexts = Some(Seq("must")),
enforceAdmins = true,
restrictionsUsers = None
).needStatusCheck("user2")
) )
assert( assert(
ProtectedBranchInfo("user1", "repo1", "branch", true, Seq("must"), true).needStatusCheck("user1") == true ProtectedBranchInfo(
"user1",
"repo1",
"branch",
enabled = true,
contexts = Some(Seq("must")),
enforceAdmins = true,
restrictionsUsers = None
).needStatusCheck("user1")
) )
} }
} }

View File

@@ -21,7 +21,7 @@ class RepositoryServiceSpec extends AnyFunSuite with ServiceSpecBase with Reposi
now = new java.util.Date now = new java.util.Date
) )
service.enableBranchProtection("root", "repo", "branch", true, Seq("must1", "must2")) service.enableBranchProtection("root", "repo", "branch", true, true, Seq("must1", "must2"), false, Nil)
val orgPbi = service.getProtectedBranchInfo("root", "repo", "branch") val orgPbi = service.getProtectedBranchInfo("root", "repo", "branch")
val org = service.getCommitStatus("root", "repo", id).get val org = service.getCommitStatus("root", "repo", id).get