Merge pull request #591 from marklacroix/anon-access

(refs #274) Add option to deny anonymous (i.e. unauthorized) access
This commit is contained in:
Naoki Takezoe
2015-01-27 10:28:11 +09:00
7 changed files with 68 additions and 27 deletions

View File

@@ -14,6 +14,7 @@ class ScalatraBootstrap extends LifeCycle {
context.getFilterRegistration("basicAuthenticationFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/git/*") context.getFilterRegistration("basicAuthenticationFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/git/*")
// Register controllers // Register controllers
context.mount(new AnonymousAccessController, "/*")
context.mount(new IndexController, "/") context.mount(new IndexController, "/")
context.mount(new SearchController, "/") context.mount(new SearchController, "/")
context.mount(new FileUploadController, "/upload") context.mount(new FileUploadController, "/upload")

View File

@@ -0,0 +1,14 @@
package app
class AnonymousAccessController extends AnonymousAccessControllerBase
trait AnonymousAccessControllerBase extends ControllerBase {
get(!context.settings.allowAnonymousAccess, context.loginAccount.isEmpty) {
if(!context.currentPath.startsWith("/assets") && !context.currentPath.startsWith("/signin") &&
!context.currentPath.startsWith("/register")) {
Unauthorized()
} else {
pass()
}
}
}

View File

@@ -16,6 +16,7 @@ trait SystemSettingsControllerBase extends ControllerBase {
"baseUrl" -> trim(label("Base URL", optional(text()))), "baseUrl" -> trim(label("Base URL", optional(text()))),
"information" -> trim(label("Information", optional(text()))), "information" -> trim(label("Information", optional(text()))),
"allowAccountRegistration" -> trim(label("Account registration", boolean())), "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 option to create a new repository", boolean())),
"gravatar" -> trim(label("Gravatar", boolean())), "gravatar" -> trim(label("Gravatar", boolean())),
"notification" -> trim(label("Notification", boolean())), "notification" -> trim(label("Notification", boolean())),

View File

@@ -14,6 +14,7 @@ trait SystemSettingsService {
settings.baseUrl.foreach(x => props.setProperty(BaseURL, x.replaceFirst("/\\Z", ""))) settings.baseUrl.foreach(x => props.setProperty(BaseURL, x.replaceFirst("/\\Z", "")))
settings.information.foreach(x => props.setProperty(Information, x)) settings.information.foreach(x => props.setProperty(Information, x))
props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString) props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString)
props.setProperty(AllowAnonymousAccess, settings.allowAnonymousAccess.toString)
props.setProperty(IsCreateRepoOptionPublic, settings.isCreateRepoOptionPublic.toString) props.setProperty(IsCreateRepoOptionPublic, settings.isCreateRepoOptionPublic.toString)
props.setProperty(Gravatar, settings.gravatar.toString) props.setProperty(Gravatar, settings.gravatar.toString)
props.setProperty(Notification, settings.notification.toString) props.setProperty(Notification, settings.notification.toString)
@@ -65,6 +66,7 @@ trait SystemSettingsService {
getOptionValue[String](props, BaseURL, None).map(x => x.replaceFirst("/\\Z", "")), getOptionValue[String](props, BaseURL, None).map(x => x.replaceFirst("/\\Z", "")),
getOptionValue[String](props, Information, None), getOptionValue[String](props, Information, None),
getValue(props, AllowAccountRegistration, false), getValue(props, AllowAccountRegistration, false),
getValue(props, AllowAnonymousAccess, true),
getValue(props, IsCreateRepoOptionPublic, true), getValue(props, IsCreateRepoOptionPublic, true),
getValue(props, Gravatar, true), getValue(props, Gravatar, true),
getValue(props, Notification, false), getValue(props, Notification, false),
@@ -113,6 +115,7 @@ object SystemSettingsService {
baseUrl: Option[String], baseUrl: Option[String],
information: Option[String], information: Option[String],
allowAccountRegistration: Boolean, allowAccountRegistration: Boolean,
allowAnonymousAccess: Boolean,
isCreateRepoOptionPublic: Boolean, isCreateRepoOptionPublic: Boolean,
gravatar: Boolean, gravatar: Boolean,
notification: Boolean, notification: Boolean,
@@ -158,6 +161,7 @@ object SystemSettingsService {
private val BaseURL = "base_url" private val BaseURL = "base_url"
private val Information = "information" private val Information = "information"
private val AllowAccountRegistration = "allow_account_registration" private val AllowAccountRegistration = "allow_account_registration"
private val AllowAnonymousAccess = "allow_anonymous_access"
private val IsCreateRepoOptionPublic = "is_create_repository_option_public" private val IsCreateRepoOptionPublic = "is_create_repository_option_public"
private val Gravatar = "gravatar" private val Gravatar = "gravatar"
private val Notification = "notification" private val Notification = "notification"

View File

@@ -28,33 +28,45 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
override def setCharacterEncoding(encoding: String) = {} override def setCharacterEncoding(encoding: String) = {}
} }
val isUpdating = request.getRequestURI.endsWith("/git-receive-pack") || "service=git-receive-pack".equals(request.getQueryString)
val settings = loadSystemSettings()
try { try {
defining(request.paths){ case Array(_, repositoryOwner, repositoryName, _*) => defining(request.paths){
getRepository(repositoryOwner, repositoryName.replaceFirst("\\.wiki\\.git$|\\.git$", ""), "") match { case Array(_, repositoryOwner, repositoryName, _*) =>
case Some(repository) => { getRepository(repositoryOwner, repositoryName.replaceFirst("\\.wiki\\.git$|\\.git$", ""), "") match {
if(!request.getRequestURI.endsWith("/git-receive-pack") && case Some(repository) => {
!"service=git-receive-pack".equals(request.getQueryString) && !repository.repository.isPrivate){ if(!isUpdating && !repository.repository.isPrivate && settings.allowAnonymousAccess){
chain.doFilter(req, wrappedResponse) chain.doFilter(req, wrappedResponse)
} else { } else {
request.getHeader("Authorization") match { request.getHeader("Authorization") match {
case null => requireAuth(response) case null => requireAuth(response)
case auth => decodeAuthHeader(auth).split(":") match { case auth => decodeAuthHeader(auth).split(":") match {
case Array(username, password) => getWritableUser(username, password, repository) match { case Array(username, password) => {
case Some(account) => { authenticate(settings, username, password) match {
request.setAttribute(Keys.Request.UserName, account.userName) case Some(account) => {
chain.doFilter(req, wrappedResponse) if(isUpdating && hasWritePermission(repository.owner, repository.name, Some(account))){
request.setAttribute(Keys.Request.UserName, account.userName)
}
chain.doFilter(req, wrappedResponse)
}
case None => requireAuth(response)
}
} }
case None => requireAuth(response) case _ => requireAuth(response)
} }
case _ => requireAuth(response)
} }
} }
} }
case None => {
logger.debug(s"Repository ${repositoryOwner}/${repositoryName} is not found.")
response.sendError(HttpServletResponse.SC_NOT_FOUND)
}
} }
case None => { case _ => {
logger.debug(s"Repository ${repositoryOwner}/${repositoryName} is not found.") logger.debug(s"Not enough path arguments: ${request.paths}")
response.sendError(HttpServletResponse.SC_NOT_FOUND) response.sendError(HttpServletResponse.SC_NOT_FOUND)
}
} }
} }
} catch { } catch {
@@ -65,13 +77,6 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
} }
} }
private def getWritableUser(username: String, password: String, repository: RepositoryService.RepositoryInfo)
(implicit session: Session): Option[Account] =
authenticate(loadSystemSettings(), username, password) match {
case x @ Some(account) if(hasWritePermission(repository.owner, repository.name, x)) => x
case _ => None
}
private def requireAuth(response: HttpServletResponse): Unit = { private def requireAuth(response: HttpServletResponse): Unit = {
response.setHeader("WWW-Authenticate", "BASIC realm=\"GitBucket\"") response.setHeader("WWW-Authenticate", "BASIC realm=\"GitBucket\"")
response.sendError(HttpServletResponse.SC_UNAUTHORIZED) response.sendError(HttpServletResponse.SC_UNAUTHORIZED)

View File

@@ -66,6 +66,21 @@
</label> </label>
</fieldset> </fieldset>
<!--====================================================================--> <!--====================================================================-->
<!-- Anonymous access -->
<!--====================================================================-->
<hr>
<label class="strong">Anonymous access</label>
<fieldset>
<label class="radio">
<input type="radio" name="allowAnonymousAccess" value="true"@if(settings.allowAnonymousAccess){ checked}>
<span class="strong">Allow</span> - Anyone can view public repositories, user/group profiles.
</label>
<label class="radio">
<input type="radio" name="allowAnonymousAccess" value="false"@if(!settings.allowAnonymousAccess){ checked}>
<span class="strong">Deny</span> - Users must authenticate before viewing any information
</label>
</fieldset>
<!--====================================================================-->
<!-- Services --> <!-- Services -->
<!--====================================================================--> <!--====================================================================-->
<hr> <hr>

View File

@@ -95,6 +95,7 @@ class AvatarImageProviderSpec extends Specification with Mockito {
baseUrl = None, baseUrl = None,
information = None, information = None,
allowAccountRegistration = false, allowAccountRegistration = false,
allowAnonymousAccess = true,
isCreateRepoOptionPublic = true, isCreateRepoOptionPublic = true,
gravatar = useGravatar, gravatar = useGravatar,
notification = false, notification = false,