Repository operation restrictions (#2469)

This commit is contained in:
Naoki Takezoe
2020-07-04 22:03:10 +09:00
committed by GitHub
parent 501cbf54ab
commit 696d354f3c
12 changed files with 238 additions and 75 deletions

View File

@@ -1,4 +1,4 @@
version = "2.6.1"
version = "1.5.1"
project.git = true
maxColumn = 120

View File

@@ -16,6 +16,7 @@ import gitbucket.core.util._
import org.scalatra.i18n.Messages
import org.scalatra.BadRequest
import org.scalatra.forms._
import org.scalatra.Forbidden
class AccountController
extends AccountControllerBase
@@ -691,6 +692,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
* Create new repository.
*/
post("/new", newRepositoryForm)(usersOnly { form =>
if (context.settings.repositoryOperation.create || context.loginAccount.get.isAdmin) {
LockUtil.lock(s"${form.owner}/${form.name}") {
if (getRepository(form.owner, form.name).isEmpty) {
createRepository(
@@ -704,14 +706,14 @@ trait AccountControllerBase extends AccountManagementControllerBase {
)
}
}
// redirect to the repository
redirect(s"/${form.owner}/${form.name}")
} else Forbidden()
})
get("/:owner/:repository/fork")(readableUsersOnly { repository =>
if (repository.repository.options.allowFork) {
val loginAccount = context.loginAccount.get
if (repository.repository.options.allowFork && (context.settings.repositoryOperation.fork || loginAccount.isAdmin)) {
val loginUserName = loginAccount.userName
val groups = getGroupsByUserName(loginUserName)
groups match {
@@ -735,8 +737,8 @@ trait AccountControllerBase extends AccountManagementControllerBase {
})
post("/:owner/:repository/fork", accountForm)(readableUsersOnly { (form, repository) =>
if (repository.repository.options.allowFork) {
val loginAccount = context.loginAccount.get
if (repository.repository.options.allowFork && (context.settings.repositoryOperation.fork || loginAccount.isAdmin)) {
val loginUserName = loginAccount.userName
val accountName = form.accountName
@@ -750,7 +752,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
// redirect to the repository
redirect(s"/${accountName}/${repository.name}")
}
} else BadRequest()
} else Forbidden()
})
private def existsAccount: Constraint = new Constraint() {

View File

@@ -20,6 +20,7 @@ import org.eclipse.jgit.lib.Constants
import org.eclipse.jgit.lib.ObjectId
import scala.util.Using
import org.scalatra.Forbidden
class RepositorySettingsController
extends RepositorySettingsControllerBase
@@ -44,7 +45,6 @@ trait RepositorySettingsControllerBase extends ControllerBase {
// for repository options
case class OptionsForm(
repositoryName: String,
description: Option[String],
isPrivate: Boolean,
issuesOption: String,
@@ -57,9 +57,6 @@ trait RepositorySettingsControllerBase extends ControllerBase {
)
val optionsForm = mapping(
"repositoryName" -> trim(
label("Repository Name", text(required, maxlength(100), repository, renameRepositoryName))
),
"description" -> trim(label("Description", optional(text()))),
"isPrivate" -> trim(label("Repository Type", boolean())),
"issuesOption" -> trim(label("Issues Option", text(required, featureOption))),
@@ -104,6 +101,15 @@ trait RepositorySettingsControllerBase extends ControllerBase {
(url, events, ctype, token) => WebHookForm(url, events, WebHookContentType.valueOf(ctype), token)
)
// for rename repository
case class RenameRepositoryForm(repositoryName: String)
val renameForm = mapping(
"repositoryName" -> trim(
label("New repository name", text(required, maxlength(100), repository, renameRepositoryName))
)
)(RenameRepositoryForm.apply)
// for transfer ownership
case class TransferOwnerShipForm(newOwner: String)
@@ -144,13 +150,8 @@ trait RepositorySettingsControllerBase extends ControllerBase {
form.mergeOptions,
form.defaultMergeOption
)
// Change repository name
if (repository.name != form.repositoryName) {
// Update database
renameRepository(repository.owner, repository.name, repository.owner, form.repositoryName)
}
flash.update("info", "Repository settings has been updated.")
redirect(s"/${repository.owner}/${form.repositoryName}/settings/options")
redirect(s"/${repository.owner}/${repository.name}/settings/options")
})
/** branch settings */
@@ -364,24 +365,40 @@ trait RepositorySettingsControllerBase extends ControllerBase {
html.danger(_, flash.get("info"))
})
/**
* Rename repository.
*/
post("/:owner/:repository/settings/rename", renameForm)(ownerOnly { (form, repository) =>
if (context.settings.repositoryOperation.rename || context.loginAccount.get.isAdmin) {
if (repository.name != form.repositoryName) {
renameRepository(repository.owner, repository.name, repository.owner, form.repositoryName)
}
redirect(s"/${repository.owner}/${form.repositoryName}")
} else Forbidden()
})
/**
* Transfer repository ownership.
*/
post("/:owner/:repository/settings/transfer", transferForm)(ownerOnly { (form, repository) =>
if (context.settings.repositoryOperation.transfer || context.loginAccount.get.isAdmin) {
// Change repository owner
if (repository.owner != form.newOwner) {
renameRepository(repository.owner, repository.name, form.newOwner, repository.name)
}
redirect(s"/${form.newOwner}/${repository.name}")
} else Forbidden()
})
/**
* Delete the repository.
*/
post("/:owner/:repository/settings/delete")(ownerOnly { repository =>
if (context.settings.repositoryOperation.delete || context.loginAccount.get.isAdmin) {
// Delete the repository and related files
deleteRepository(repository.repository)
redirect(s"/${repository.owner}")
} else Forbidden()
})
/**

View File

@@ -38,7 +38,14 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
"information" -> trim(label("Information", optional(text()))),
"allowAccountRegistration" -> trim(label("Account registration", boolean())),
"allowAnonymousAccess" -> trim(label("Anonymous access", boolean())),
"isCreateRepoOptionPublic" -> trim(label("Default option to create a new repository", boolean())),
"isCreateRepoOptionPublic" -> trim(label("Default visibility of new repository", boolean())),
"repositoryOperation" -> mapping(
"create" -> trim(label("Allow all users to create repository", boolean())),
"delete" -> trim(label("Allow all users to delete repository", boolean())),
"rename" -> trim(label("Allow all users to rename repository", boolean())),
"transfer" -> trim(label("Allow all users to transfer repository", boolean())),
"fork" -> trim(label("Allow all users to fork repository", boolean()))
)(RepositoryOperation.apply),
"gravatar" -> trim(label("Gravatar", boolean())),
"notification" -> trim(label("Notification", boolean())),
"activityLogLimit" -> trim(label("Limit of activity logs", optional(number()))),

View File

@@ -21,6 +21,11 @@ trait SystemSettingsService {
props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString)
props.setProperty(AllowAnonymousAccess, settings.allowAnonymousAccess.toString)
props.setProperty(IsCreateRepoOptionPublic, settings.isCreateRepoOptionPublic.toString)
props.setProperty(RepositoryOperationCreate, settings.repositoryOperation.create.toString)
props.setProperty(RepositoryOperationDelete, settings.repositoryOperation.delete.toString)
props.setProperty(RepositoryOperationRename, settings.repositoryOperation.rename.toString)
props.setProperty(RepositoryOperationTransfer, settings.repositoryOperation.transfer.toString)
props.setProperty(RepositoryOperationFork, settings.repositoryOperation.fork.toString)
props.setProperty(Gravatar, settings.gravatar.toString)
props.setProperty(Notification, settings.notification.toString)
settings.activityLogLimit.foreach(x => props.setProperty(ActivityLogLimit, x.toString))
@@ -98,6 +103,13 @@ trait SystemSettingsService {
getValue(props, AllowAccountRegistration, false),
getValue(props, AllowAnonymousAccess, true),
getValue(props, IsCreateRepoOptionPublic, true),
RepositoryOperation(
create = getValue(props, RepositoryOperationCreate, true),
delete = getValue(props, RepositoryOperationDelete, true),
rename = getValue(props, RepositoryOperationRename, true),
transfer = getValue(props, RepositoryOperationTransfer, true),
fork = getValue(props, RepositoryOperationFork, true)
),
getValue(props, Gravatar, false),
getValue(props, Notification, false),
getOptionValue[Int](props, ActivityLogLimit, None),
@@ -185,6 +197,7 @@ object SystemSettingsService {
allowAccountRegistration: Boolean,
allowAnonymousAccess: Boolean,
isCreateRepoOptionPublic: Boolean,
repositoryOperation: RepositoryOperation,
gravatar: Boolean,
notification: Boolean,
activityLogLimit: Option[Int],
@@ -226,6 +239,14 @@ object SystemSettingsService {
}
}
case class RepositoryOperation(
create: Boolean,
delete: Boolean,
rename: Boolean,
transfer: Boolean,
fork: Boolean
)
case class Ssh(
enabled: Boolean,
sshHost: Option[String],
@@ -291,6 +312,11 @@ object SystemSettingsService {
private val AllowAccountRegistration = "allow_account_registration"
private val AllowAnonymousAccess = "allow_anonymous_access"
private val IsCreateRepoOptionPublic = "is_create_repository_option_public"
private val RepositoryOperationCreate = "repository_operation_create"
private val RepositoryOperationDelete = "repository_operation_delete"
private val RepositoryOperationRename = "repository_operation_rename"
private val RepositoryOperationTransfer = "repository_operation_transfer"
private val RepositoryOperationFork = "repository_operation_fork"
private val Gravatar = "gravatar"
private val Notification = "notification"
private val ActivityLogLimit = "activity_log_limit"

View File

@@ -116,8 +116,80 @@
<span class="strong">Deny</span> <span class="normal">- Only administrators can create accounts.</span>
</label>
</fieldset>
<!--====================================================================-->
<!-- Repository operations -->
<!--====================================================================-->
<hr>
<label class="strong">Default permissions when creating a new repository</label>
<label class="strong">Repository operation</label>
<fieldset>
<div class="form-group">
<label class="control-label col-md-2">Create</label>
<div class="col-md-10">
<label class="radio">
<input type="radio" name="repositoryOperation.create" value="true"@if(context.settings.repositoryOperation.create){ checked}>
<span class="strong">All users</span> <span class="normal">- All users can create repository.</span>
</label>
<label class="radio">
<input type="radio" name="repositoryOperation.create" value="false"@if(!context.settings.repositoryOperation.create){ checked}>
<span class="strong">Admin only</span> <span class="normal">- Only administrators can create repository.</span>
</label>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-2">Delete</label>
<div class="col-md-10">
<label class="radio">
<input type="radio" name="repositoryOperation.delete" value="true"@if(context.settings.repositoryOperation.delete){ checked}>
<span class="strong">All users</span> <span class="normal">- All users can delete repository.</span>
</label>
<label class="radio">
<input type="radio" name="repositoryOperation.delete" value="false"@if(!context.settings.repositoryOperation.delete){ checked}>
<span class="strong">Admin only</span> <span class="normal">- Only administrators can delete repository.</span>
</label>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-2">Rename</label>
<div class="col-md-10">
<label class="radio">
<input type="radio" name="repositoryOperation.rename" value="true"@if(context.settings.repositoryOperation.rename){ checked}>
<span class="strong">All users</span> <span class="normal">- All users can rename repository.</span>
</label>
<label class="radio">
<input type="radio" name="repositoryOperation.rename" value="false"@if(!context.settings.repositoryOperation.rename){ checked}>
<span class="strong">Admin only</span> <span class="normal">- Only administrators can rename repository.</span>
</label>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-2">Transfer</label>
<div class="col-md-10">
<label class="radio">
<input type="radio" name="repositoryOperation.transfer" value="true"@if(context.settings.repositoryOperation.transfer){ checked}>
<span class="strong">All users</span> <span class="normal">- All users can transfer repository.</span>
</label>
<label class="radio">
<input type="radio" name="repositoryOperation.transfer" value="false"@if(!context.settings.repositoryOperation.transfer){ checked}>
<span class="strong">Admin only</span> <span class="normal">- Only administrators can transfer repository.</span>
</label>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-2">Fork</label>
<div class="col-md-10">
<label class="radio">
<input type="radio" name="repositoryOperation.fork" value="true"@if(context.settings.repositoryOperation.fork){ checked}>
<span class="strong">All users</span> <span class="normal">- All users can fork repository.</span>
</label>
<label class="radio">
<input type="radio" name="repositoryOperation.fork" value="false"@if(!context.settings.repositoryOperation.fork){ checked}>
<span class="strong">Admin only</span> <span class="normal">- Only administrators can fork repository.</span>
</label>
</div>
</div>
</fieldset>
<hr>
<label class="strong">Default visibility when creating a new repository</label>
<fieldset>
<label class="radio">
<input type="radio" name="isCreateRepoOptionPublic" value="true"@if(context.settings.isCreateRepoOptionPublic){ checked}>

View File

@@ -102,7 +102,9 @@
<ul class="dropdown-menu pull-right" style="width: auto;">
<li>
<ul class="menu">
@if(context.settings.repositoryOperation.create || context.loginAccount.get.isAdmin){
<li><a href="@context.path/new">New repository</a></li>
}
<li><a href="@context.path/groups/new">New group</a></li>
</ul>
</li>

View File

@@ -75,7 +75,7 @@
@gitbucket.core.plugin.PluginRegistry().getRepositoryHeaders.map { repositoryHeaderComponent =>
@repositoryHeaderComponent(repository, context)
}
@if(repository.repository.options.allowFork) {
@if(repository.repository.options.allowFork && (context.settings.repositoryOperation.fork || context.loginAccount.map(_.isAdmin).getOrElse(false))) {
@if(context.loginAccount.isEmpty){
<a class="btn btn-default btn-sm" href="@context.path/signin?redirect=@helpers.urlEncode(s"${context.path}/${repository.owner}/${repository.name}")">
<span class="strong"><i class="octicon octicon-repo-forked"></i>Fork</span><span class="muted">: @repository.forkedCount</span>

View File

@@ -7,8 +7,35 @@
<div class="panel panel-default">
<div class="panel-heading strong">Danger Zone</div>
<div class="panel-body">
<form id="transfer-form" method="post" action="@helpers.url(repository)/settings/transfer" validate="true" autocomplete="off">
<form id="gc-form" method="post" action="@helpers.url(repository)/settings/gc">
<fieldset class="form-group">
<label class="strong">Garbage collection</label>
<div>
Run garbage collection for this git repository immediately.
<input type="submit" class="btn btn-danger pull-right" value="Garbage collection"/>
</div>
</fieldset>
</form>
@if(context.settings.repositoryOperation.rename || context.loginAccount.get.isAdmin){
<form id="rename-form" method="post" action="@helpers.url(repository)/settings/rename" validate="true" autocomplete="off">
<fieldset class="border-top form-group">
<label class="strong">Rename repository</label>
<div>
Rename this repository. The current URL will be unavailable.
<div class="pull-right">
<input type="text" name="repositoryName" id="repositoryName" class="form-control" style="width: 200px; display: inline; vertical-align: middle;" value="@repository.name"/>
<input type="submit" class="btn btn-danger" value="Rename"/>
<div>
<span id="error-repositoryName" class="error"></span>
</div>
</div>
</div>
</fieldset>
</form>
}
@if(context.settings.repositoryOperation.transfer || context.loginAccount.get.isAdmin){
<form id="transfer-form" method="post" action="@helpers.url(repository)/settings/transfer" validate="true" autocomplete="off">
<fieldset class="border-top form-group">
<label class="strong">Transfer Ownership</label>
<div>
Transfer this repo to another user or to group.
@@ -22,6 +49,8 @@
</div>
</fieldset>
</form>
}
@if(context.settings.repositoryOperation.delete || context.loginAccount.get.isAdmin){
<form id="delete-form" method="post" action="@helpers.url(repository)/settings/delete">
<fieldset class="border-top form-group">
<label class="strong">Delete repository</label>
@@ -31,15 +60,7 @@
</div>
</fieldset>
</form>
<form id="gc-form" method="post" action="@helpers.url(repository)/settings/gc">
<fieldset class="border-top form-group">
<label class="strong">Garbage collection</label>
<div>
Run garbage collection for this git repository immediately.
<input type="submit" class="btn btn-danger pull-right" value="Garbage collection"/>
</div>
</fieldset>
</form>
}
</div>
</div>
}
@@ -50,6 +71,13 @@ $(function(){
$('#delete-form').submit(function(){
return confirm('Once you delete a repository, there is no going back.\nAre you sure?');
});
$('#rename-form').submit(function(){
if($('#rename-form').data('validated') === true){
return confirm('Rename this repository as you entered. The current URL will be unavailable.\nAre you sure?');
} else {
return true;
}
});
$('#transfer-form').submit(function(){
if($('#transfer-form').data('validated') === true){
return confirm('Transfer to the repository owner you entered.\nAre you sure?');

View File

@@ -9,11 +9,6 @@
<div class="panel-heading strong">Settings</div>
<div class="panel-body">
<fieldset class="form-group">
<label for="repositoryName" class="strong">Repository Name:</label>
<input type="text" name="repositoryName" id="repositoryName" class="form-control" value="@repository.name"/>
<span id="error-repositoryName" class="error"></span>
</fieldset>
<fieldset class="border-top form-group">
<label for="description" class="strong">Description:</label>
<input type="text" name="description" id="description" class="form-control" value="@repository.repository.description"/>
</fieldset>

View File

@@ -12,7 +12,7 @@ import java.sql.DriverManager
import java.io.File
import gitbucket.core.controller.Context
import gitbucket.core.service.SystemSettingsService.{Ssh, SystemSettings}
import gitbucket.core.service.SystemSettingsService.{RepositoryOperation, Ssh, SystemSettings}
import javax.servlet.http.{HttpServletRequest, HttpSession}
import org.scalatestplus.mockito.MockitoSugar
import org.mockito.Mockito._
@@ -36,6 +36,13 @@ trait ServiceSpecBase extends MockitoSugar {
allowAccountRegistration = false,
allowAnonymousAccess = true,
isCreateRepoOptionPublic = true,
repositoryOperation = RepositoryOperation(
create = true,
delete = true,
rename = true,
transfer = true,
fork = true
),
gravatar = false,
notification = false,
activityLogLimit = None,

View File

@@ -7,7 +7,7 @@ import javax.servlet.http.{HttpServletRequest, HttpSession}
import gitbucket.core.controller.Context
import gitbucket.core.model.Account
import gitbucket.core.service.RequestCache
import gitbucket.core.service.SystemSettingsService.{Ssh, SystemSettings, WebHook, Upload}
import gitbucket.core.service.SystemSettingsService.{RepositoryOperation, Ssh, SystemSettings, Upload, WebHook}
import org.mockito.Mockito._
import org.scalatest.FunSpec
import org.scalatestplus.mockito.MockitoSugar
@@ -122,6 +122,13 @@ class AvatarImageProviderSpec extends FunSpec with MockitoSugar {
allowAccountRegistration = false,
allowAnonymousAccess = true,
isCreateRepoOptionPublic = true,
repositoryOperation = RepositoryOperation(
create = true,
delete = true,
rename = true,
transfer = true,
fork = true
),
gravatar = useGravatar,
notification = false,
activityLogLimit = None,