From 27ae9095639bb228a1b7ff86a3ebe4264abf05be Mon Sep 17 00:00:00 2001 From: mschaefers Date: Thu, 29 Nov 2012 18:33:09 +0100 Subject: feature: when using LdapUserService one can configure Gitblit to fetch all users from ldap that can possibly login. This allows to see newly generated LDAP users instantly in Gitblit. By now an LDAP user had to log in once to appear in GitBlit. feature: when LDAP user synchronization is enabled, one can configure GitBlit to delete all LDAP users from the backing user service that are no longer existent in LDAP. By now LDAP users that were deleted still appeared in the GitBlit user list. --- distrib/gitblit.properties | 2358 ++++++++++++++++++++++---------------------- 1 file changed, 1189 insertions(+), 1169 deletions(-) (limited to 'distrib/gitblit.properties') diff --git a/distrib/gitblit.properties b/distrib/gitblit.properties index ee050a48..3419e005 100644 --- a/distrib/gitblit.properties +++ b/distrib/gitblit.properties @@ -1,1169 +1,1189 @@ -# -# Git Servlet Settings -# - -# Base folder for repositories. -# This folder may contain bare and non-bare repositories but Gitblit will only -# allow you to push to bare repositories. -# Use forward slashes even on Windows!! -# e.g. c:/gitrepos -# -# SINCE 0.5.0 -# RESTART REQUIRED -git.repositoriesFolder = git - -# Build the available repository list at startup and cache this list for reuse. -# This reduces disk io when presenting the repositories page, responding to rpcs, -# etc, but it means that Gitblit will not automatically identify repositories -# added or deleted by external tools. -# -# For this case you can use curl, wget, etc to issue an rpc request to clear the -# cache (e.g. https://localhost/rpc?req=CLEAR_REPOSITORY_CACHE) -# -# SINCE 1.1.0 -git.cacheRepositoryList = true - -# Search the repositories folder subfolders for other repositories. -# Repositories MAY NOT be nested (i.e. one repository within another) -# but they may be grouped together in subfolders. -# e.g. c:/gitrepos/libraries/mylibrary.git -# c:/gitrepos/libraries/myotherlibrary.git -# -# SINCE 0.5.0 -git.searchRepositoriesSubfolders = true - -# Maximum number of folders to recurse into when searching for repositories. -# The default value, -1, disables depth limits. -# -# SINCE 1.1.0 -git.searchRecursionDepth = -1 - -# List of regex exclusion patterns to match against folders found in -# *git.repositoriesFolder*. -# Use forward slashes even on Windows!! -# e.g. test/jgit\.git -# -# SPACE-DELIMITED -# CASE-SENSITIVE -# SINCE 1.1.0 -git.searchExclusions = - -# List of regex url patterns for extracting a repository name when locating -# submodules. -# e.g. git.submoduleUrlPatterns = .*?://github.com/(.*) will extract -# *gitblit/gitblit.git* from *git://github.com/gitblit/gitblit.git* -# If no matches are found then the submodule repository name is assumed to be -# whatever trails the last / character. (e.g. gitblit.git). -# -# SPACE-DELIMITED -# CASE-SENSITIVE -# SINCE 1.1.0 -git.submoduleUrlPatterns = .*?://github.com/(.*) - -# Allow push/pull over http/https with JGit servlet. -# If you do NOT want to allow Git clients to clone/push to Gitblit set this -# to false. You might want to do this if you are only using ssh:// or git://. -# If you set this false, consider changing the *web.otherUrls* setting to -# indicate your clone/push urls. -# -# SINCE 0.5.0 -git.enableGitServlet = true - -# If you want to restrict all git servlet access to those with valid X509 client -# certificates then set this value to true. -# -# SINCE 1.2.0 -git.requiresClientCertificate = false - -# Enforce date checks on client certificates to ensure that they are not being -# used prematurely and that they have not expired. -# -# SINCE 1.2.0 -git.enforceCertificateValidity = true - -# List of OIDs to extract from a client certificate DN to map a certificate to -# an account username. -# -# e.g. git.certificateUsernameOIDs = CN -# e.g. git.certificateUsernameOIDs = FirstName LastName -# -# SPACE-DELIMITED -# SINCE 1.2.0 -git.certificateUsernameOIDs = CN - -# Only serve/display bare repositories. -# If there are non-bare repositories in git.repositoriesFolder and this setting -# is true, they will be excluded from the ui. -# -# SINCE 0.9.0 -git.onlyAccessBareRepositories = false - -# Allow an authenticated user to create a destination repository on a push if -# the repository does not already exist. -# -# Administrator accounts can create a repository in any project. -# These repositories are created with the default access restriction and authorization -# control values. The pushing account is set as the owner. -# -# Non-administrator accounts with the CREATE role may create personal repositories. -# These repositories are created as VIEW restricted for NAMED users. -# The pushing account is set as the owner. -# -# SINCE 1.2.0 -git.allowCreateOnPush = true - -# The default access restriction for new repositories. -# Valid values are NONE, PUSH, CLONE, VIEW -# NONE = anonymous view, clone, & push -# PUSH = anonymous view & clone and authenticated push -# CLONE = anonymous view, authenticated clone & push -# VIEW = authenticated view, clone, & push -# -# SINCE 1.0.0 -git.defaultAccessRestriction = NONE - -# The default authorization control for new repositories. -# Valid values are AUTHENTICATED and NAMED -# AUTHENTICATED = any authenticated user is granted restricted access -# NAMED = only named users/teams are granted restricted access -# -# SINCE 1.1.0 -git.defaultAuthorizationControl = NAMED - -# Enable JGit-based garbage collection. (!!EXPERIMENTAL!!) -# -# USE AT YOUR OWN RISK! -# -# If enabled, the garbage collection executor scans all repositories once a day -# at the hour of your choosing. The GC executor will take each repository "offline", -# one-at-a-time, to check if the repository satisfies it's GC trigger requirements. -# -# While the repository is offline it will be inaccessible from the web UI or from -# any of the other services (git, rpc, rss, etc). -# -# Gitblit's GC Executor MAY NOT PLAY NICE with the other Git kids on the block, -# especially on Windows systems, so if you are using other tools please coordinate -# their usage with your GC Executor schedule or do not use this feature. -# -# The GC algorithm complex and the JGit team advises caution when using their -# young implementation of GC. -# -# http://wiki.eclipse.org/EGit/New_and_Noteworthy/2.1#Garbage_Collector_and_Repository_Storage_Statistics -# -# EXPERIMENTAL -# SINCE 1.2.0 -# RESTART REQUIRED -git.enableGarbageCollection = false - -# Hour of the day for the GC Executor to scan repositories. -# This value is in 24-hour time. -# -# SINCE 1.2.0 -git.garbageCollectionHour = 0 - -# The default minimum total filesize of loose objects to trigger early garbage -# collection. -# -# You may specify a custom threshold for a repository in the repository's settings. -# Common unit suffixes of k, m, or g are supported. -# -# SINCE 1.2.0 -git.defaultGarbageCollectionThreshold = 500k - -# The default period, in days, between GCs for a repository. If the total filesize -# of the loose object exceeds *git.garbageCollectionThreshold* or the repository's -# custom threshold, this period will be short-circuited. -# -# e.g. if a repository collects 100KB of loose objects every day with a 500KB -# threshold and a period of 7 days, it will take 5 days for the loose objects to -# be collected, packed, and pruned. -# -# OR -# -# if a repository collects 10KB of loose objects every day with a 500KB threshold -# and a period of 7 days, it will take the full 7 days for the loose objects to be -# collected, packed, and pruned. -# -# You may specify a custom period for a repository in the repository's settings. -# -# The minimum value is 1 day since the GC Executor only runs once a day. -# -# SINCE 1.2.0 -git.defaultGarbageCollectionPeriod = 7 - -# Number of bytes of a pack file to load into memory in a single read operation. -# This is the "page size" of the JGit buffer cache, used for all pack access -# operations. All disk IO occurs as single window reads. Setting this too large -# may cause the process to load more data than is required; setting this too small -# may increase the frequency of read() system calls. -# -# Default on JGit is 8 KiB on all platforms. -# -# Common unit suffixes of k, m, or g are supported. -# Documentation courtesy of the Gerrit project. -# -# SINCE 1.0.0 -# RESTART REQUIRED -git.packedGitWindowSize = 8k - -# Maximum number of bytes to load and cache in memory from pack files. If JGit -# needs to access more than this many bytes it will unload less frequently used -# windows to reclaim memory space within the process. As this buffer must be shared -# with the rest of the JVM heap, it should be a fraction of the total memory available. -# -# The JGit team recommends setting this value larger than the size of your biggest -# repository. This ensures you can serve most requests from memory. -# -# Default on JGit is 10 MiB on all platforms. -# -# Common unit suffixes of k, m, or g are supported. -# Documentation courtesy of the Gerrit project. -# -# SINCE 1.0.0 -# RESTART REQUIRED -git.packedGitLimit = 10m - -# Maximum number of bytes to reserve for caching base objects that multiple deltafied -# objects reference. By storing the entire decompressed base object in a cache Git -# is able to avoid unpacking and decompressing frequently used base objects multiple times. -# -# Default on JGit is 10 MiB on all platforms. You probably do not need to adjust -# this value. -# -# Common unit suffixes of k, m, or g are supported. -# Documentation courtesy of the Gerrit project. -# -# SINCE 1.0.0 -# RESTART REQUIRED -git.deltaBaseCacheLimit = 10m - -# Maximum number of pack files to have open at once. A pack file must be opened -# in order for any of its data to be available in a cached window. -# -# If you increase this to a larger setting you may need to also adjust the ulimit -# on file descriptors for the host JVM, as Gitblit needs additional file descriptors -# available for network sockets and other repository data manipulation. -# -# Default on JGit is 128 file descriptors on all platforms. -# Documentation courtesy of the Gerrit project. -# -# SINCE 1.0.0 -# RESTART REQUIRED -git.packedGitOpenFiles = 128 - -# Largest object size, in bytes, that JGit will allocate as a contiguous byte -# array. Any file revision larger than this threshold will have to be streamed, -# typically requiring the use of temporary files under $GIT_DIR/objects to implement -# psuedo-random access during delta decompression. -# -# Servers with very high traffic should set this to be larger than the size of -# their common big files. For example a server managing the Android platform -# typically has to deal with ~10-12 MiB XML files, so 15 m would be a reasonable -# setting in that environment. Setting this too high may cause the JVM to run out -# of heap space when handling very big binary files, such as device firmware or -# CD-ROM ISO images. Make sure to adjust your JVM heap accordingly. -# -# Default is 50 MiB on all platforms. -# -# Common unit suffixes of k, m, or g are supported. -# Documentation courtesy of the Gerrit project. -# -# SINCE 1.0.0 -# RESTART REQUIRED -git.streamFileThreshold = 50m - -# When true, JGit will use mmap() rather than malloc()+read() to load data from -# pack files. The use of mmap can be problematic on some JVMs as the garbage -# collector must deduce that a memory mapped segment is no longer in use before -# a call to munmap() can be made by the JVM native code. -# -# In server applications (such as Gitblit) that need to access many pack files, -# setting this to true risks artificially running out of virtual address space, -# as the garbage collector cannot reclaim unused mapped spaces fast enough. -# -# Default on JGit is false. Although potentially slower, it yields much more -# predictable behavior. -# Documentation courtesy of the Gerrit project. -# -# SINCE 1.0.0 -# RESTART REQUIRED -git.packedGitMmap = false - -# -# Groovy Integration -# - -# Location of Groovy scripts to use for Pre and Post receive hooks. -# Use forward slashes even on Windows!! -# e.g. c:/groovy -# -# RESTART REQUIRED -# SINCE 0.8.0 -groovy.scriptsFolder = groovy - -# Specify the directory Grape uses for downloading libraries. -# http://groovy.codehaus.org/Grape -# -# RESTART REQUIRED -# SINCE 1.0.0 -groovy.grapeFolder = groovy/grape - -# Scripts to execute on Pre-Receive. -# -# These scripts execute after an incoming push has been parsed and validated -# but BEFORE the changes are applied to the repository. You might reject a -# push in this script based on the repository and branch the push is attempting -# to change. -# -# Script names are case-sensitive on case-sensitive file systems. You may omit -# the traditional ".groovy" from this list if your file extension is ".groovy" -# -# NOTE: -# These scripts are only executed when pushing to *Gitblit*, not to other Git -# tooling you may be using. Also note that these scripts are shared between -# repositories. These are NOT repository-specific scripts! Within the script -# you may customize the control-flow for a specific repository by checking the -# *repository* variable. -# -# SPACE-DELIMITED -# CASE-SENSITIVE -# SINCE 0.8.0 -groovy.preReceiveScripts = - -# Scripts to execute on Post-Receive. -# -# These scripts execute AFTER an incoming push has been applied to a repository. -# You might trigger a continuous-integration build here or send a notification. -# -# Script names are case-sensitive on case-sensitive file systems. You may omit -# the traditional ".groovy" from this list if your file extension is ".groovy" -# -# NOTE: -# These scripts are only executed when pushing to *Gitblit*, not to other Git -# tooling you may be using. Also note that these scripts are shared between -# repositories. These are NOT repository-specific scripts! Within the script -# you may customize the control-flow for a specific repository by checking the -# *repository* variable. -# -# SPACE-DELIMITED -# CASE-SENSITIVE -# SINCE 0.8.0 -groovy.postReceiveScripts = - -# Repository custom fields for Groovy Hook mechanism -# -# List of key=label pairs of custom fields to prompt for in the Edit Repository -# page. These keys are stored in the repository's git config file in the -# section [gitblit "customFields"]. Key names are alphanumeric only. These -# fields are intended to be used for the Groovy hook mechanism where a script -# can adjust it's execution based on the custom fields stored in the repository -# config. -# -# e.g. "commitMsgRegex=Commit Message Regular Expression" anotherProperty=Another -# -# SPACE-DELIMITED -# SINCE 1.0.0 -groovy.customFields = - -# -# Authentication Settings -# - -# Require authentication to see everything but the admin pages -# -# SINCE 0.5.0 -# RESTART REQUIRED -web.authenticateViewPages = false - -# Require admin authentication for the admin functions and pages -# -# SINCE 0.5.0 -# RESTART REQUIRED -web.authenticateAdminPages = true - -# Allow Gitblit to store a cookie in the user's browser for automatic -# authentication. The cookie is generated by the user service. -# -# SINCE 0.5.0 -web.allowCookieAuthentication = true - -# Config file for storing project metadata -# -# SINCE 1.2.0 -web.projectsFile = projects.conf - -# Either the full path to a user config file (users.conf) -# OR the full path to a simple user properties file (users.properties) -# OR a fully qualified class name that implements the IUserService interface. -# -# Alternative user services: -# com.gitblit.LdapUserService -# com.gitblit.RedmineUserService -# -# Any custom user service implementation must have a public default constructor. -# -# SINCE 0.5.0 -# RESTART REQUIRED -realm.userService = users.conf - -# How to store passwords. -# Valid values are plain, md5, or combined-md5. md5 is the hash of password. -# combined-md5 is the hash of username.toLowerCase()+password. -# Default is md5. -# -# SINCE 0.5.0 -realm.passwordStorage = md5 - -# Minimum valid length for a plain text password. -# Default value is 5. Absolute minimum is 4. -# -# SINCE 0.5.0 -realm.minPasswordLength = 5 - -# -# Gitblit Web Settings -# -# If blank Gitblit is displayed. -# -# SINCE 0.5.0 -web.siteName = - -# If *web.authenticateAdminPages*=true, users with "admin" role can create -# repositories, create users, and edit repository metadata. -# -# If *web.authenticateAdminPages*=false, any user can execute the aforementioned -# functions. -# -# SINCE 0.5.0 -web.allowAdministration = true - -# Allows rpc clients to list repositories and possibly manage or administer the -# Gitblit server, if the authenticated account has administrator permissions. -# See *web.enableRpcManagement* and *web.enableRpcAdministration*. -# -# SINCE 0.7.0 -web.enableRpcServlet = true - -# Allows rpc clients to manage repositories and users of the Gitblit instance, -# if the authenticated account has administrator permissions. -# Requires *web.enableRpcServlet=true*. -# -# SINCE 0.7.0 -web.enableRpcManagement = false - -# Allows rpc clients to control the server settings and monitor the health of this -# this Gitblit instance, if the authenticated account has administrator permissions. -# Requires *web.enableRpcServlet=true* and *web.enableRpcManagement*. -# -# SINCE 0.7.0 -web.enableRpcAdministration = false - -# Full path to a configurable robots.txt file. With this file you can control -# what parts of your Gitblit server respectable robots are allowed to traverse. -# http://googlewebmastercentral.blogspot.com/2008/06/improving-on-robots-exclusion-protocol.html -# -# SINCE 1.0.0 -web.robots.txt = - -# If true, the web ui layout will respond and adapt to the browser's dimensions. -# if false, the web ui will use a 940px fixed-width layout. -# http://twitter.github.com/bootstrap/scaffolding.html#responsive -# -# SINCE 1.0.0 -web.useResponsiveLayout = true - -# Allow Gravatar images to be displayed in Gitblit pages. -# -# SINCE 0.8.0 -web.allowGravatar = true - -# Allow dynamic zip downloads. -# -# SINCE 0.5.0 -web.allowZipDownloads = true - -# Allow optional Lucene integration. Lucene indexing is an opt-in feature. -# A repository may specify branches to index with Lucene instead of using Git -# commit traversal. There are scenarios where you may want to completely disable -# Lucene indexing despite a repository specifying indexed branches. One such -# scenario is on a resource-constrained federated Gitblit mirror. -# -# SINCE 0.9.0 -web.allowLuceneIndexing = true - -# Controls the length of shortened commit hash ids -# -# SINCE 1.2.0 -web.shortCommitIdLength = 6 - -# Use Clippy (Flash solution) to provide a copy-to-clipboard button. -# If false, a button with a more primitive JavaScript-based prompt box will -# offer a 3-step (click, ctrl+c, enter) copy-to-clipboard alternative. -# -# SINCE 0.8.0 -web.allowFlashCopyToClipboard = true - -# Default number of entries to include in RSS Syndication links -# -# SINCE 0.5.0 -web.syndicationEntries = 25 - -# Show the size of each repository on the repositories page. -# This requires recursive traversal of each repository folder. This may be -# non-performant on some operating systems and/or filesystems. -# -# SINCE 0.5.2 -web.showRepositorySizes = true - -# List of custom regex expressions that can be displayed in the Filters menu -# of the Repositories and Activity pages. Keep them very simple because you -# are likely to run into encoding issues if they are too complex. -# -# Use !!! to separate the filters -# -# SINCE 0.8.0 -web.customFilters = - -# Show federation registrations (without token) and the current pull status -# to non-administrator users. -# -# SINCE 0.6.0 -web.showFederationRegistrations = false - -# This is the message displayed when *web.authenticateViewPages=true*. -# This can point to a file with Markdown content. -# Specifying "gitblit" uses the internal login message. -# -# SINCE 0.7.0 -web.loginMessage = gitblit - -# This is the message displayed above the repositories table. -# This can point to a file with Markdown content. -# Specifying "gitblit" uses the internal welcome message. -# -# SINCE 0.5.0 -web.repositoriesMessage = gitblit - -# Ordered list of charsets/encodings to use when trying to display a blob. -# If empty, UTF-8 and ISO-8859-1 are used. The server's default charset -# is always appended to the encoding list. If all encodings fail to cleanly -# decode the blob content, UTF-8 will be used with the standard malformed -# input/unmappable character replacement strings. -# -# SPACE-DELIMITED -# SINCE 1.0.0 -web.blobEncodings = UTF-8 ISO-8859-1 - -# Manually set the default timezone to be used by Gitblit for display in the -# web ui. This value is independent of the JVM timezone. Specifying a blank -# value will default to the JVM timezone. -# e.g. America/New_York, US/Pacific, UTC, Europe/Berlin -# -# SINCE 0.9.0 -# RESTART REQUIRED -web.timezone = - -# Use the client timezone when formatting dates. -# This uses AJAX to determine the browser's timezone and may require more -# server overhead because a Wicket session is created. All Gitblit pages -# attempt to be stateless, if possible. -# -# SINCE 0.5.0 -# RESTART REQUIRED -web.useClientTimezone = false - -# Time format -# -# -# SINCE 0.8.0 -web.timeFormat = HH:mm - -# Short date format -# -# -# SINCE 0.5.0 -web.datestampShortFormat = yyyy-MM-dd - -# Long date format -# -# SINCE 0.8.0 -web.datestampLongFormat = EEEE, MMMM d, yyyy - -# Long timestamp format -# -# -# SINCE 0.5.0 -web.datetimestampLongFormat = EEEE, MMMM d, yyyy HH:mm Z - -# Mount URL parameters -# This setting controls if pretty or parameter URLs are used. -# i.e. -# if true: -# http://localhost/commit/myrepo/abcdef -# if false: -# http://localhost/commit/?r=myrepo&h=abcdef -# -# SINCE 0.5.0 -# RESTART REQUIRED -web.mountParameters = true - -# Some servlet containers (e.g. Tomcat >= 6.0.10) disallow '/' (%2F) encoding -# in URLs as a security precaution for proxies. This setting tells Gitblit -# to preemptively replace '/' with '*' or '!' for url string parameters. -# -# -# -# Add *-Dorg.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH=true* to your -# *CATALINA_OPTS* or to your JVM launch parameters -# -# SINCE 0.5.2 -web.forwardSlashCharacter = / - -# Show other URLs on the summary page for accessing your git repositories -# Use spaces to separate urls. {0} is the token for the repository name. -# e.g. -# web.otherUrls = ssh://localhost/git/{0} git://localhost/git/{0} -# -# SPACE-DELIMITED -# SINCE 0.5.0 -web.otherUrls = - -# Choose how to present the repositories list. -# grouped = group nested/subfolder repositories together (no sorting) -# flat = flat list of repositories (sorting allowed) -# -# SINCE 0.5.0 -web.repositoryListType = grouped - -# If using a grouped repository list and there are repositories at the -# root level of your repositories folder, you may specify the displayed -# group name with this setting. This value is only used for web presentation. -# -# SINCE 0.5.0 -web.repositoryRootGroupName = main - -# Display the repository swatch color next to the repository name link in the -# repositories list. -# -# SINCE 0.8.0 -web.repositoryListSwatches = true - -# Choose the diff presentation style: gitblt, gitweb, or plain -# -# SINCE 0.5.0 -web.diffStyle = gitblit - -# Control if email addresses are shown in web ui -# -# SINCE 0.5.0 -web.showEmailAddresses = true - -# Shows a combobox in the page links header with commit, committer, and author -# search selection. Default search is commit. -# -# SINCE 0.5.0 -web.showSearchTypeSelection = false - -# Generates a line graph of repository activity over time on the Summary page. -# This uses the Google Charts API. -# -# SINCE 0.5.0 -web.generateActivityGraph = true - -# The number of days to show on the activity page. -# Value must exceed 0 else default of 14 is used -# -# SINCE 0.8.0 -web.activityDuration = 14 - -# The number of commits to display on the summary page -# Value must exceed 0 else default of 20 is used -# -# SINCE 0.5.0 -web.summaryCommitCount = 16 - -# The number of tags/branches to display on the summary page. -# -1 = all tags/branches -# 0 = hide tags/branches -# N = N tags/branches -# -# SINCE 0.5.0 -web.summaryRefsCount = 5 - -# The number of items to show on a page before showing the first, prev, next -# pagination links. A default if 50 is used for any invalid value. -# -# SINCE 0.5.0 -web.itemsPerPage = 50 - -# Registered file extensions to ignore during Lucene indexing -# -# SPACE-DELIMITED -# SINCE 0.9.0 -web.luceneIgnoreExtensions = 7z arc arj bin bmp dll doc docx exe gif gz jar jpg lib lzh odg odf odt pdf ppt png so swf xcf xls xlsx zip - -# Registered extensions for google-code-prettify -# -# SPACE-DELIMITED -# SINCE 0.5.0 -web.prettyPrintExtensions = c cpp cs css frm groovy htm html java js php pl prefs properties py rb scala sh sql xml vb - -# Registered extensions for markdown transformation -# -# SPACE-DELIMITED -# CASE-SENSITIVE -# SINCE 0.5.0 -web.markdownExtensions = md mkd markdown MD MKD - -# Image extensions -# -# SPACE-DELIMITED -# SINCE 0.5.0 -web.imageExtensions = bmp jpg gif png - -# Registered extensions for binary blobs -# -# SPACE-DELIMITED -# SINCE 0.5.0 -web.binaryExtensions = jar pdf tar.gz zip - -# Aggressive heap management will run the garbage collector on every generated -# page. This slows down page generation a little but improves heap consumption. -# -# SINCE 0.5.0 -web.aggressiveHeapManagement = false - -# Run the webapp in debug mode -# -# SINCE 0.5.0 -# RESTART REQUIRED -web.debugMode = false - -# Enable/disable global regex substitutions (i.e. shared across repositories) -# -# SINCE 0.5.0 -regex.global = true - -# Example global regex substitutions -# Use !!! to separate the search pattern and the replace pattern -# searchpattern!!!replacepattern -# SINCE 0.5.0 -regex.global.bug = \\b(Bug:)(\\s*[#]?|-){0,1}(\\d+)\\b!!!Bug-Id: $3 -# SINCE 0.5.0 -regex.global.changeid = \\b(Change-Id:\\s*)([A-Za-z0-9]*)\\b!!!Change-Id: $2 - -# Example per-repository regex substitutions overrides global -# SINCE 0.5.0 -regex.myrepository.bug = \\b(Bug:)(\\s*[#]?|-){0,1}(\\d+)\\b!!!Bug-Id: $3 - -# -# Mail Settings -# SINCE 0.6.0 -# -# Mail settings are used to notify administrators of received federation proposals -# - -# ip or hostname of smtp server -# -# SINCE 0.6.0 -mail.server = - -# port to use for smtp requests -# -# SINCE 0.6.0 -mail.port = 25 - -# debug the mail executor -# -# SINCE 0.6.0 -mail.debug = false - -# if your smtp server requires authentication, supply the credentials here -# -# SINCE 0.6.0 -mail.username = -# SINCE 0.6.0 -mail.password = - -# from address for generated emails -# -# SINCE 0.6.0 -mail.fromAddress = - -# List of email addresses for the Gitblit administrators -# -# SPACE-DELIMITED -# SINCE 0.6.0 -mail.adminAddresses = - -# List of email addresses for sending push email notifications. -# -# This key currently requires use of the sendemail.groovy hook script. -# If you set sendemail.groovy in *groovy.postReceiveScripts* then email -# notifications for all repositories (regardless of access restrictions!) -# will be sent to these addresses. -# -# SPACE-DELIMITED -# SINCE 0.8.0 -mail.mailingLists = - -# -# Federation Settings -# SINCE 0.6.0 -# -# A Gitblit federation is a way to backup one Gitblit instance to another. -# -# *git.enableGitServlet* must be true to use this feature. - -# Your federation name is used for federation status acknowledgments. If it is -# unset, and you elect to send a status acknowledgment, your Gitblit instance -# will be identified by its hostname, if available, else your internal ip address. -# The source Gitblit instance will also append your external IP address to your -# identification to differentiate multiple pulling systems behind a single proxy. -# -# SINCE 0.6.0 -federation.name = - -# Specify the passphrase of this Gitblit instance. -# -# An unspecified (empty) passphrase disables processing federation requests. -# -# This value can be anything you want: an integer, a sentence, an haiku, etc. -# Keep the value simple, though, to avoid Java properties file encoding issues. -# -# Changing your passphrase will break any registrations you have established with other -# Gitblit instances. -# -# CASE-SENSITIVE -# SINCE 0.6.0 -# RESTART REQUIRED *(only to enable or disable federation)* -federation.passphrase = - -# Control whether or not this Gitblit instance can receive federation proposals -# from another Gitblit instance. Registering a federated Gitblit is a manual -# process. Proposals help to simplify that process by allowing a remote Gitblit -# instance to send your Gitblit instance the federation pull data. -# -# SINCE 0.6.0 -federation.allowProposals = false - -# The destination folder for cached federation proposals. -# Use forward slashes even on Windows!! -# -# SINCE 0.6.0 -federation.proposalsFolder = proposals - -# The default pull frequency if frequency is unspecified on a registration -# -# SINCE 0.6.0 -federation.defaultFrequency = 60 mins - -# Federation Sets are named groups of repositories. The Federation Sets are -# available for selection in the repository settings page. You can assign a -# repository to one or more sets and then distribute the token for the set. -# This allows you to grant federation pull access to a subset of your available -# repositories. Tokens for federation sets only grant repository pull access. -# -# SPACE-DELIMITED -# CASE-SENSITIVE -# SINCE 0.6.0 -federation.sets = - -# Federation pull registrations -# Registrations are read once, at startup. -# -# RESTART REQUIRED -# -# frequency: -# The shortest frequency allowed is every 5 minutes -# Decimal frequency values are cast to integers -# Frequency values may be specified in mins, hours, or days -# Values that can not be parsed or are unspecified default to *federation.defaultFrequency* -# -# folder: -# if unspecified, the folder is *git.repositoriesFolder* -# if specified, the folder is relative to *git.repositoriesFolder* -# -# bare: -# if true, each repository will be created as a *bare* repository and will not -# have a working directory. -# -# if false, each repository will be created as a normal repository suitable -# for local work. -# -# mirror: -# if true, each repository HEAD is reset to *origin/master* after each pull. -# The repository will be flagged *isFrozen* after the initial clone. -# -# if false, each repository HEAD will point to the FETCH_HEAD of the initial -# clone from the origin until pushed to or otherwise manipulated. -# -# mergeAccounts: -# if true, remote accounts and their permissions are merged into your -# users.properties file -# -# notifyOnError: -# if true and the mail configuration is properly set, administrators will be -# notified by email of pull failures -# -# include and exclude: -# Space-delimited list of repositories to include or exclude from pull -# may be * wildcard to include or exclude all -# may use fuzzy match (e.g. org.eclipse.*) - -# -# (Nearly) Perfect Mirror example -# - -#federation.example1.url = https://go.gitblit.com -#federation.example1.token = 6f3b8a24bf970f17289b234284c94f43eb42f0e4 -#federation.example1.frequency = 120 mins -#federation.example1.folder = -#federation.example1.bare = true -#federation.example1.mirror = true -#federation.example1.mergeAccounts = true - -# -# Advanced Realm Settings -# - -# URL of the LDAP server. -# To use encrypted transport, use either ldaps:// URL for SSL or ldap+tls:// to -# send StartTLS command. -# -# SINCE 1.0.0 -realm.ldap.server = ldap://localhost - -# Login username for LDAP searches. -# If this value is unspecified, anonymous LDAP login will be used. -# -# e.g. mydomain\\username -# -# SINCE 1.0.0 -realm.ldap.username = cn=Directory Manager - -# Login password for LDAP searches. -# -# SINCE 1.0.0 -realm.ldap.password = password - -# The LdapUserService must be backed by another user service for standard user -# and team management. -# default: users.conf -# -# SINCE 1.0.0 -# RESTART REQUIRED -realm.ldap.backingUserService = users.conf - -# Delegate team membership control to LDAP. -# -# If true, team user memberships will be specified by LDAP groups. This will -# disable team selection in Edit User and user selection in Edit Team. -# -# If false, LDAP will only be used for authentication and Gitblit will maintain -# team memberships with the *realm.ldap.backingUserService*. -# -# SINCE 1.0.0 -realm.ldap.maintainTeams = false - -# Root node for all LDAP users -# -# This is the root node from which subtree user searches will begin. -# If blank, Gitblit will search ALL nodes. -# -# SINCE 1.0.0 -realm.ldap.accountBase = OU=Users,OU=UserControl,OU=MyOrganization,DC=MyDomain - -# Filter criteria for LDAP users -# -# Query pattern to use when searching for a user account. This may be any valid -# LDAP query expression, including the standard (&) and (|) operators. -# -# Variables may be injected via the ${variableName} syntax. -# Recognized variables are: -# ${username} - The text entered as the user name -# -# SINCE 1.0.0 -realm.ldap.accountPattern = (&(objectClass=person)(sAMAccountName=${username})) - -# Root node for all LDAP groups to be used as Gitblit Teams -# -# This is the root node from which subtree team searches will begin. -# If blank, Gitblit will search ALL nodes. -# -# SINCE 1.0.0 -realm.ldap.groupBase = OU=Groups,OU=UserControl,OU=MyOrganization,DC=MyDomain - -# Filter criteria for LDAP groups -# -# Query pattern to use when searching for a team. This may be any valid -# LDAP query expression, including the standard (&) and (|) operators. -# -# Variables may be injected via the ${variableName} syntax. -# Recognized variables are: -# ${username} - The text entered as the user name -# ${dn} - The Distinguished Name of the user logged in -# -# All attributes from the LDAP User record are available. For example, if a user -# has an attribute "fullName" set to "John", "(fn=${fullName})" will be -# translated to "(fn=John)". -# -# SINCE 1.0.0 -realm.ldap.groupMemberPattern = (&(objectClass=group)(member=${dn})) - -# LDAP users or groups that should be given administrator privileges. -# -# Teams are specified with a leading '@' character. Groups with spaces in the -# name can be entered as "@team name". -# -# e.g. realm.ldap.admins = john @git_admins "@git admins" -# -# SPACE-DELIMITED -# SINCE 1.0.0 -realm.ldap.admins = @Git_Admins - -# Attribute(s) on the USER record that indicate their display (or full) name. -# Leave blank for no mapping available in LDAP. -# -# This may be a single attribute, or a string of multiple attributes. Examples: -# displayName - Uses the attribute 'displayName' on the user record -# ${personalTitle}. ${givenName} ${surname} - Will concatenate the 3 -# attributes together, with a '.' after personalTitle -# -# SINCE 1.0.0 -realm.ldap.displayName = displayName - -# Attribute(s) on the USER record that indicate their email address. -# Leave blank for no mapping available in LDAP. -# -# This may be a single attribute, or a string of multiple attributes. Examples: -# email - Uses the attribute 'email' on the user record -# ${givenName}.${surname}@gitblit.com -Will concatenate the 2 attributes -# together with a '.' and '@' creating something like first.last@gitblit.com -# -# SINCE 1.0.0 -realm.ldap.email = email - -# The RedmineUserService must be backed by another user service for standard user -# and team management. -# default: users.conf -# -# RESTART REQUIRED -realm.redmine.backingUserService = users.conf - -# URL of the Redmine. -realm.redmine.url = http://example.com/redmine - -# -# Server Settings -# - -# The temporary folder to decompress the embedded gitblit webapp. -# -# SINCE 0.5.0 -# RESTART REQUIRED -server.tempFolder = temp - -# Use Jetty NIO connectors. If false, Jetty Socket connectors will be used. -# -# SINCE 0.5.0 -# RESTART REQUIRED -server.useNio = true - -# Context path for the GO application. You might want to change the context -# path if running Gitblit behind a proxy layer such as mod_proxy. -# -# SINCE 0.7.0 -# RESTART REQUIRED -server.contextPath = / - -# Standard http port to serve. <= 0 disables this connector. -# On Unix/Linux systems, ports < 1024 require root permissions. -# Recommended value: 80 or 8080 -# -# SINCE 0.5.0 -# RESTART REQUIRED -server.httpPort = 0 - -# Secure/SSL https port to serve. <= 0 disables this connector. -# On Unix/Linux systems, ports < 1024 require root permissions. -# Recommended value: 443 or 8443 -# -# SINCE 0.5.0 -# RESTART REQUIRED -server.httpsPort = 8443 - -# Port for serving an Apache JServ Protocol (AJP) 1.3 connector for integrating -# Gitblit GO into an Apache HTTP server setup. <= 0 disables this connector. -# Recommended value: 8009 -# -# SINCE 0.9.0 -# RESTART REQUIRED -server.ajpPort = 0 - -# Specify the interface for Jetty to bind the standard connector. -# You may specify an ip or an empty value to bind to all interfaces. -# Specifying localhost will result in Gitblit ONLY listening to requests to -# localhost. -# -# SINCE 0.5.0 -# RESTART REQUIRED -server.httpBindInterface = localhost - -# Specify the interface for Jetty to bind the secure connector. -# You may specify an ip or an empty value to bind to all interfaces. -# Specifying localhost will result in Gitblit ONLY listening to requests to -# localhost. -# -# SINCE 0.5.0 -# RESTART REQUIRED -server.httpsBindInterface = localhost - -# Specify the interface for Jetty to bind the AJP connector. -# You may specify an ip or an empty value to bind to all interfaces. -# Specifying localhost will result in Gitblit ONLY listening to requests to -# localhost. -# -# SINCE 0.9.0 -# RESTART REQUIRED -server.ajpBindInterface = localhost - -# Password for SSL keystore. -# Keystore password and certificate password must match. -# This is provided for convenience, its probably more secure to set this value -# using the --storePassword command line parameter. -# -# If you are using the official JRE or JDK from Oracle you may not have the -# JCE Unlimited Strength Jurisdiction Policy files bundled with your JVM. Because -# of this, your store/key password can not exceed 7 characters. If you require -# longer passwords you may need to install the JCE Unlimited Strength Jurisdiction -# Policy files from Oracle. -# -# http://www.oracle.com/technetwork/java/javase/downloads/index.html -# -# Gitblit and the Gitblit Certificate Authority will both indicate if Unlimited -# Strength encryption is available. -# -# SINCE 0.5.0 -# RESTART REQUIRED -server.storePassword = gitblit - -# If serving over https (recommended) you might consider requiring clients to -# authenticate with ssl certificates. If enabled, only https clients with the -# a valid client certificate will be able to access Gitblit. -# -# If disabled, client certificate authentication is optional and will be tried -# first before falling-back to form authentication or basic authentication. -# -# Requiring client certificates to access any of Gitblit may be too extreme, -# consider this carefully. -# -# SINCE 1.2.0 -# RESTART REQUIRED -server.requireClientCertificates = false - -# Port for shutdown monitor to listen on. -# -# SINCE 0.5.0 -# RESTART REQUIRED -server.shutdownPort = 8081 \ No newline at end of file +# +# Git Servlet Settings +# + +# Base folder for repositories. +# This folder may contain bare and non-bare repositories but Gitblit will only +# allow you to push to bare repositories. +# Use forward slashes even on Windows!! +# e.g. c:/gitrepos +# +# SINCE 0.5.0 +# RESTART REQUIRED +git.repositoriesFolder = git + +# Build the available repository list at startup and cache this list for reuse. +# This reduces disk io when presenting the repositories page, responding to rpcs, +# etc, but it means that Gitblit will not automatically identify repositories +# added or deleted by external tools. +# +# For this case you can use curl, wget, etc to issue an rpc request to clear the +# cache (e.g. https://localhost/rpc?req=CLEAR_REPOSITORY_CACHE) +# +# SINCE 1.1.0 +git.cacheRepositoryList = true + +# Search the repositories folder subfolders for other repositories. +# Repositories MAY NOT be nested (i.e. one repository within another) +# but they may be grouped together in subfolders. +# e.g. c:/gitrepos/libraries/mylibrary.git +# c:/gitrepos/libraries/myotherlibrary.git +# +# SINCE 0.5.0 +git.searchRepositoriesSubfolders = true + +# Maximum number of folders to recurse into when searching for repositories. +# The default value, -1, disables depth limits. +# +# SINCE 1.1.0 +git.searchRecursionDepth = -1 + +# List of regex exclusion patterns to match against folders found in +# *git.repositoriesFolder*. +# Use forward slashes even on Windows!! +# e.g. test/jgit\.git +# +# SPACE-DELIMITED +# CASE-SENSITIVE +# SINCE 1.1.0 +git.searchExclusions = + +# List of regex url patterns for extracting a repository name when locating +# submodules. +# e.g. git.submoduleUrlPatterns = .*?://github.com/(.*) will extract +# *gitblit/gitblit.git* from *git://github.com/gitblit/gitblit.git* +# If no matches are found then the submodule repository name is assumed to be +# whatever trails the last / character. (e.g. gitblit.git). +# +# SPACE-DELIMITED +# CASE-SENSITIVE +# SINCE 1.1.0 +git.submoduleUrlPatterns = .*?://github.com/(.*) + +# Allow push/pull over http/https with JGit servlet. +# If you do NOT want to allow Git clients to clone/push to Gitblit set this +# to false. You might want to do this if you are only using ssh:// or git://. +# If you set this false, consider changing the *web.otherUrls* setting to +# indicate your clone/push urls. +# +# SINCE 0.5.0 +git.enableGitServlet = true + +# If you want to restrict all git servlet access to those with valid X509 client +# certificates then set this value to true. +# +# SINCE 1.2.0 +git.requiresClientCertificate = false + +# Enforce date checks on client certificates to ensure that they are not being +# used prematurely and that they have not expired. +# +# SINCE 1.2.0 +git.enforceCertificateValidity = true + +# List of OIDs to extract from a client certificate DN to map a certificate to +# an account username. +# +# e.g. git.certificateUsernameOIDs = CN +# e.g. git.certificateUsernameOIDs = FirstName LastName +# +# SPACE-DELIMITED +# SINCE 1.2.0 +git.certificateUsernameOIDs = CN + +# Only serve/display bare repositories. +# If there are non-bare repositories in git.repositoriesFolder and this setting +# is true, they will be excluded from the ui. +# +# SINCE 0.9.0 +git.onlyAccessBareRepositories = false + +# Allow an authenticated user to create a destination repository on a push if +# the repository does not already exist. +# +# Administrator accounts can create a repository in any project. +# These repositories are created with the default access restriction and authorization +# control values. The pushing account is set as the owner. +# +# Non-administrator accounts with the CREATE role may create personal repositories. +# These repositories are created as VIEW restricted for NAMED users. +# The pushing account is set as the owner. +# +# SINCE 1.2.0 +git.allowCreateOnPush = true + +# The default access restriction for new repositories. +# Valid values are NONE, PUSH, CLONE, VIEW +# NONE = anonymous view, clone, & push +# PUSH = anonymous view & clone and authenticated push +# CLONE = anonymous view, authenticated clone & push +# VIEW = authenticated view, clone, & push +# +# SINCE 1.0.0 +git.defaultAccessRestriction = NONE + +# The default authorization control for new repositories. +# Valid values are AUTHENTICATED and NAMED +# AUTHENTICATED = any authenticated user is granted restricted access +# NAMED = only named users/teams are granted restricted access +# +# SINCE 1.1.0 +git.defaultAuthorizationControl = NAMED + +# Enable JGit-based garbage collection. (!!EXPERIMENTAL!!) +# +# USE AT YOUR OWN RISK! +# +# If enabled, the garbage collection executor scans all repositories once a day +# at the hour of your choosing. The GC executor will take each repository "offline", +# one-at-a-time, to check if the repository satisfies it's GC trigger requirements. +# +# While the repository is offline it will be inaccessible from the web UI or from +# any of the other services (git, rpc, rss, etc). +# +# Gitblit's GC Executor MAY NOT PLAY NICE with the other Git kids on the block, +# especially on Windows systems, so if you are using other tools please coordinate +# their usage with your GC Executor schedule or do not use this feature. +# +# The GC algorithm complex and the JGit team advises caution when using their +# young implementation of GC. +# +# http://wiki.eclipse.org/EGit/New_and_Noteworthy/2.1#Garbage_Collector_and_Repository_Storage_Statistics +# +# EXPERIMENTAL +# SINCE 1.2.0 +# RESTART REQUIRED +git.enableGarbageCollection = false + +# Hour of the day for the GC Executor to scan repositories. +# This value is in 24-hour time. +# +# SINCE 1.2.0 +git.garbageCollectionHour = 0 + +# The default minimum total filesize of loose objects to trigger early garbage +# collection. +# +# You may specify a custom threshold for a repository in the repository's settings. +# Common unit suffixes of k, m, or g are supported. +# +# SINCE 1.2.0 +git.defaultGarbageCollectionThreshold = 500k + +# The default period, in days, between GCs for a repository. If the total filesize +# of the loose object exceeds *git.garbageCollectionThreshold* or the repository's +# custom threshold, this period will be short-circuited. +# +# e.g. if a repository collects 100KB of loose objects every day with a 500KB +# threshold and a period of 7 days, it will take 5 days for the loose objects to +# be collected, packed, and pruned. +# +# OR +# +# if a repository collects 10KB of loose objects every day with a 500KB threshold +# and a period of 7 days, it will take the full 7 days for the loose objects to be +# collected, packed, and pruned. +# +# You may specify a custom period for a repository in the repository's settings. +# +# The minimum value is 1 day since the GC Executor only runs once a day. +# +# SINCE 1.2.0 +git.defaultGarbageCollectionPeriod = 7 + +# Number of bytes of a pack file to load into memory in a single read operation. +# This is the "page size" of the JGit buffer cache, used for all pack access +# operations. All disk IO occurs as single window reads. Setting this too large +# may cause the process to load more data than is required; setting this too small +# may increase the frequency of read() system calls. +# +# Default on JGit is 8 KiB on all platforms. +# +# Common unit suffixes of k, m, or g are supported. +# Documentation courtesy of the Gerrit project. +# +# SINCE 1.0.0 +# RESTART REQUIRED +git.packedGitWindowSize = 8k + +# Maximum number of bytes to load and cache in memory from pack files. If JGit +# needs to access more than this many bytes it will unload less frequently used +# windows to reclaim memory space within the process. As this buffer must be shared +# with the rest of the JVM heap, it should be a fraction of the total memory available. +# +# The JGit team recommends setting this value larger than the size of your biggest +# repository. This ensures you can serve most requests from memory. +# +# Default on JGit is 10 MiB on all platforms. +# +# Common unit suffixes of k, m, or g are supported. +# Documentation courtesy of the Gerrit project. +# +# SINCE 1.0.0 +# RESTART REQUIRED +git.packedGitLimit = 10m + +# Maximum number of bytes to reserve for caching base objects that multiple deltafied +# objects reference. By storing the entire decompressed base object in a cache Git +# is able to avoid unpacking and decompressing frequently used base objects multiple times. +# +# Default on JGit is 10 MiB on all platforms. You probably do not need to adjust +# this value. +# +# Common unit suffixes of k, m, or g are supported. +# Documentation courtesy of the Gerrit project. +# +# SINCE 1.0.0 +# RESTART REQUIRED +git.deltaBaseCacheLimit = 10m + +# Maximum number of pack files to have open at once. A pack file must be opened +# in order for any of its data to be available in a cached window. +# +# If you increase this to a larger setting you may need to also adjust the ulimit +# on file descriptors for the host JVM, as Gitblit needs additional file descriptors +# available for network sockets and other repository data manipulation. +# +# Default on JGit is 128 file descriptors on all platforms. +# Documentation courtesy of the Gerrit project. +# +# SINCE 1.0.0 +# RESTART REQUIRED +git.packedGitOpenFiles = 128 + +# Largest object size, in bytes, that JGit will allocate as a contiguous byte +# array. Any file revision larger than this threshold will have to be streamed, +# typically requiring the use of temporary files under $GIT_DIR/objects to implement +# psuedo-random access during delta decompression. +# +# Servers with very high traffic should set this to be larger than the size of +# their common big files. For example a server managing the Android platform +# typically has to deal with ~10-12 MiB XML files, so 15 m would be a reasonable +# setting in that environment. Setting this too high may cause the JVM to run out +# of heap space when handling very big binary files, such as device firmware or +# CD-ROM ISO images. Make sure to adjust your JVM heap accordingly. +# +# Default is 50 MiB on all platforms. +# +# Common unit suffixes of k, m, or g are supported. +# Documentation courtesy of the Gerrit project. +# +# SINCE 1.0.0 +# RESTART REQUIRED +git.streamFileThreshold = 50m + +# When true, JGit will use mmap() rather than malloc()+read() to load data from +# pack files. The use of mmap can be problematic on some JVMs as the garbage +# collector must deduce that a memory mapped segment is no longer in use before +# a call to munmap() can be made by the JVM native code. +# +# In server applications (such as Gitblit) that need to access many pack files, +# setting this to true risks artificially running out of virtual address space, +# as the garbage collector cannot reclaim unused mapped spaces fast enough. +# +# Default on JGit is false. Although potentially slower, it yields much more +# predictable behavior. +# Documentation courtesy of the Gerrit project. +# +# SINCE 1.0.0 +# RESTART REQUIRED +git.packedGitMmap = false + +# +# Groovy Integration +# + +# Location of Groovy scripts to use for Pre and Post receive hooks. +# Use forward slashes even on Windows!! +# e.g. c:/groovy +# +# RESTART REQUIRED +# SINCE 0.8.0 +groovy.scriptsFolder = groovy + +# Specify the directory Grape uses for downloading libraries. +# http://groovy.codehaus.org/Grape +# +# RESTART REQUIRED +# SINCE 1.0.0 +groovy.grapeFolder = groovy/grape + +# Scripts to execute on Pre-Receive. +# +# These scripts execute after an incoming push has been parsed and validated +# but BEFORE the changes are applied to the repository. You might reject a +# push in this script based on the repository and branch the push is attempting +# to change. +# +# Script names are case-sensitive on case-sensitive file systems. You may omit +# the traditional ".groovy" from this list if your file extension is ".groovy" +# +# NOTE: +# These scripts are only executed when pushing to *Gitblit*, not to other Git +# tooling you may be using. Also note that these scripts are shared between +# repositories. These are NOT repository-specific scripts! Within the script +# you may customize the control-flow for a specific repository by checking the +# *repository* variable. +# +# SPACE-DELIMITED +# CASE-SENSITIVE +# SINCE 0.8.0 +groovy.preReceiveScripts = + +# Scripts to execute on Post-Receive. +# +# These scripts execute AFTER an incoming push has been applied to a repository. +# You might trigger a continuous-integration build here or send a notification. +# +# Script names are case-sensitive on case-sensitive file systems. You may omit +# the traditional ".groovy" from this list if your file extension is ".groovy" +# +# NOTE: +# These scripts are only executed when pushing to *Gitblit*, not to other Git +# tooling you may be using. Also note that these scripts are shared between +# repositories. These are NOT repository-specific scripts! Within the script +# you may customize the control-flow for a specific repository by checking the +# *repository* variable. +# +# SPACE-DELIMITED +# CASE-SENSITIVE +# SINCE 0.8.0 +groovy.postReceiveScripts = + +# Repository custom fields for Groovy Hook mechanism +# +# List of key=label pairs of custom fields to prompt for in the Edit Repository +# page. These keys are stored in the repository's git config file in the +# section [gitblit "customFields"]. Key names are alphanumeric only. These +# fields are intended to be used for the Groovy hook mechanism where a script +# can adjust it's execution based on the custom fields stored in the repository +# config. +# +# e.g. "commitMsgRegex=Commit Message Regular Expression" anotherProperty=Another +# +# SPACE-DELIMITED +# SINCE 1.0.0 +groovy.customFields = + +# +# Authentication Settings +# + +# Require authentication to see everything but the admin pages +# +# SINCE 0.5.0 +# RESTART REQUIRED +web.authenticateViewPages = false + +# Require admin authentication for the admin functions and pages +# +# SINCE 0.5.0 +# RESTART REQUIRED +web.authenticateAdminPages = true + +# Allow Gitblit to store a cookie in the user's browser for automatic +# authentication. The cookie is generated by the user service. +# +# SINCE 0.5.0 +web.allowCookieAuthentication = true + +# Config file for storing project metadata +# +# SINCE 1.2.0 +web.projectsFile = projects.conf + +# Either the full path to a user config file (users.conf) +# OR the full path to a simple user properties file (users.properties) +# OR a fully qualified class name that implements the IUserService interface. +# +# Alternative user services: +# com.gitblit.LdapUserService +# com.gitblit.RedmineUserService +# +# Any custom user service implementation must have a public default constructor. +# +# SINCE 0.5.0 +# RESTART REQUIRED +realm.userService = users.conf + +# How to store passwords. +# Valid values are plain, md5, or combined-md5. md5 is the hash of password. +# combined-md5 is the hash of username.toLowerCase()+password. +# Default is md5. +# +# SINCE 0.5.0 +realm.passwordStorage = md5 + +# Minimum valid length for a plain text password. +# Default value is 5. Absolute minimum is 4. +# +# SINCE 0.5.0 +realm.minPasswordLength = 5 + +# +# Gitblit Web Settings +# +# If blank Gitblit is displayed. +# +# SINCE 0.5.0 +web.siteName = + +# If *web.authenticateAdminPages*=true, users with "admin" role can create +# repositories, create users, and edit repository metadata. +# +# If *web.authenticateAdminPages*=false, any user can execute the aforementioned +# functions. +# +# SINCE 0.5.0 +web.allowAdministration = true + +# Allows rpc clients to list repositories and possibly manage or administer the +# Gitblit server, if the authenticated account has administrator permissions. +# See *web.enableRpcManagement* and *web.enableRpcAdministration*. +# +# SINCE 0.7.0 +web.enableRpcServlet = true + +# Allows rpc clients to manage repositories and users of the Gitblit instance, +# if the authenticated account has administrator permissions. +# Requires *web.enableRpcServlet=true*. +# +# SINCE 0.7.0 +web.enableRpcManagement = false + +# Allows rpc clients to control the server settings and monitor the health of this +# this Gitblit instance, if the authenticated account has administrator permissions. +# Requires *web.enableRpcServlet=true* and *web.enableRpcManagement*. +# +# SINCE 0.7.0 +web.enableRpcAdministration = false + +# Full path to a configurable robots.txt file. With this file you can control +# what parts of your Gitblit server respectable robots are allowed to traverse. +# http://googlewebmastercentral.blogspot.com/2008/06/improving-on-robots-exclusion-protocol.html +# +# SINCE 1.0.0 +web.robots.txt = + +# If true, the web ui layout will respond and adapt to the browser's dimensions. +# if false, the web ui will use a 940px fixed-width layout. +# http://twitter.github.com/bootstrap/scaffolding.html#responsive +# +# SINCE 1.0.0 +web.useResponsiveLayout = true + +# Allow Gravatar images to be displayed in Gitblit pages. +# +# SINCE 0.8.0 +web.allowGravatar = true + +# Allow dynamic zip downloads. +# +# SINCE 0.5.0 +web.allowZipDownloads = true + +# Allow optional Lucene integration. Lucene indexing is an opt-in feature. +# A repository may specify branches to index with Lucene instead of using Git +# commit traversal. There are scenarios where you may want to completely disable +# Lucene indexing despite a repository specifying indexed branches. One such +# scenario is on a resource-constrained federated Gitblit mirror. +# +# SINCE 0.9.0 +web.allowLuceneIndexing = true + +# Controls the length of shortened commit hash ids +# +# SINCE 1.2.0 +web.shortCommitIdLength = 6 + +# Use Clippy (Flash solution) to provide a copy-to-clipboard button. +# If false, a button with a more primitive JavaScript-based prompt box will +# offer a 3-step (click, ctrl+c, enter) copy-to-clipboard alternative. +# +# SINCE 0.8.0 +web.allowFlashCopyToClipboard = true + +# Default number of entries to include in RSS Syndication links +# +# SINCE 0.5.0 +web.syndicationEntries = 25 + +# Show the size of each repository on the repositories page. +# This requires recursive traversal of each repository folder. This may be +# non-performant on some operating systems and/or filesystems. +# +# SINCE 0.5.2 +web.showRepositorySizes = true + +# List of custom regex expressions that can be displayed in the Filters menu +# of the Repositories and Activity pages. Keep them very simple because you +# are likely to run into encoding issues if they are too complex. +# +# Use !!! to separate the filters +# +# SINCE 0.8.0 +web.customFilters = + +# Show federation registrations (without token) and the current pull status +# to non-administrator users. +# +# SINCE 0.6.0 +web.showFederationRegistrations = false + +# This is the message displayed when *web.authenticateViewPages=true*. +# This can point to a file with Markdown content. +# Specifying "gitblit" uses the internal login message. +# +# SINCE 0.7.0 +web.loginMessage = gitblit + +# This is the message displayed above the repositories table. +# This can point to a file with Markdown content. +# Specifying "gitblit" uses the internal welcome message. +# +# SINCE 0.5.0 +web.repositoriesMessage = gitblit + +# Ordered list of charsets/encodings to use when trying to display a blob. +# If empty, UTF-8 and ISO-8859-1 are used. The server's default charset +# is always appended to the encoding list. If all encodings fail to cleanly +# decode the blob content, UTF-8 will be used with the standard malformed +# input/unmappable character replacement strings. +# +# SPACE-DELIMITED +# SINCE 1.0.0 +web.blobEncodings = UTF-8 ISO-8859-1 + +# Manually set the default timezone to be used by Gitblit for display in the +# web ui. This value is independent of the JVM timezone. Specifying a blank +# value will default to the JVM timezone. +# e.g. America/New_York, US/Pacific, UTC, Europe/Berlin +# +# SINCE 0.9.0 +# RESTART REQUIRED +web.timezone = + +# Use the client timezone when formatting dates. +# This uses AJAX to determine the browser's timezone and may require more +# server overhead because a Wicket session is created. All Gitblit pages +# attempt to be stateless, if possible. +# +# SINCE 0.5.0 +# RESTART REQUIRED +web.useClientTimezone = false + +# Time format +# +# +# SINCE 0.8.0 +web.timeFormat = HH:mm + +# Short date format +# +# +# SINCE 0.5.0 +web.datestampShortFormat = yyyy-MM-dd + +# Long date format +# +# SINCE 0.8.0 +web.datestampLongFormat = EEEE, MMMM d, yyyy + +# Long timestamp format +# +# +# SINCE 0.5.0 +web.datetimestampLongFormat = EEEE, MMMM d, yyyy HH:mm Z + +# Mount URL parameters +# This setting controls if pretty or parameter URLs are used. +# i.e. +# if true: +# http://localhost/commit/myrepo/abcdef +# if false: +# http://localhost/commit/?r=myrepo&h=abcdef +# +# SINCE 0.5.0 +# RESTART REQUIRED +web.mountParameters = true + +# Some servlet containers (e.g. Tomcat >= 6.0.10) disallow '/' (%2F) encoding +# in URLs as a security precaution for proxies. This setting tells Gitblit +# to preemptively replace '/' with '*' or '!' for url string parameters. +# +# +# +# Add *-Dorg.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH=true* to your +# *CATALINA_OPTS* or to your JVM launch parameters +# +# SINCE 0.5.2 +web.forwardSlashCharacter = / + +# Show other URLs on the summary page for accessing your git repositories +# Use spaces to separate urls. {0} is the token for the repository name. +# e.g. +# web.otherUrls = ssh://localhost/git/{0} git://localhost/git/{0} +# +# SPACE-DELIMITED +# SINCE 0.5.0 +web.otherUrls = + +# Choose how to present the repositories list. +# grouped = group nested/subfolder repositories together (no sorting) +# flat = flat list of repositories (sorting allowed) +# +# SINCE 0.5.0 +web.repositoryListType = grouped + +# If using a grouped repository list and there are repositories at the +# root level of your repositories folder, you may specify the displayed +# group name with this setting. This value is only used for web presentation. +# +# SINCE 0.5.0 +web.repositoryRootGroupName = main + +# Display the repository swatch color next to the repository name link in the +# repositories list. +# +# SINCE 0.8.0 +web.repositoryListSwatches = true + +# Choose the diff presentation style: gitblt, gitweb, or plain +# +# SINCE 0.5.0 +web.diffStyle = gitblit + +# Control if email addresses are shown in web ui +# +# SINCE 0.5.0 +web.showEmailAddresses = true + +# Shows a combobox in the page links header with commit, committer, and author +# search selection. Default search is commit. +# +# SINCE 0.5.0 +web.showSearchTypeSelection = false + +# Generates a line graph of repository activity over time on the Summary page. +# This uses the Google Charts API. +# +# SINCE 0.5.0 +web.generateActivityGraph = true + +# The number of days to show on the activity page. +# Value must exceed 0 else default of 14 is used +# +# SINCE 0.8.0 +web.activityDuration = 14 + +# The number of commits to display on the summary page +# Value must exceed 0 else default of 20 is used +# +# SINCE 0.5.0 +web.summaryCommitCount = 16 + +# The number of tags/branches to display on the summary page. +# -1 = all tags/branches +# 0 = hide tags/branches +# N = N tags/branches +# +# SINCE 0.5.0 +web.summaryRefsCount = 5 + +# The number of items to show on a page before showing the first, prev, next +# pagination links. A default if 50 is used for any invalid value. +# +# SINCE 0.5.0 +web.itemsPerPage = 50 + +# Registered file extensions to ignore during Lucene indexing +# +# SPACE-DELIMITED +# SINCE 0.9.0 +web.luceneIgnoreExtensions = 7z arc arj bin bmp dll doc docx exe gif gz jar jpg lib lzh odg odf odt pdf ppt png so swf xcf xls xlsx zip + +# Registered extensions for google-code-prettify +# +# SPACE-DELIMITED +# SINCE 0.5.0 +web.prettyPrintExtensions = c cpp cs css frm groovy htm html java js php pl prefs properties py rb scala sh sql xml vb + +# Registered extensions for markdown transformation +# +# SPACE-DELIMITED +# CASE-SENSITIVE +# SINCE 0.5.0 +web.markdownExtensions = md mkd markdown MD MKD + +# Image extensions +# +# SPACE-DELIMITED +# SINCE 0.5.0 +web.imageExtensions = bmp jpg gif png + +# Registered extensions for binary blobs +# +# SPACE-DELIMITED +# SINCE 0.5.0 +web.binaryExtensions = jar pdf tar.gz zip + +# Aggressive heap management will run the garbage collector on every generated +# page. This slows down page generation a little but improves heap consumption. +# +# SINCE 0.5.0 +web.aggressiveHeapManagement = false + +# Run the webapp in debug mode +# +# SINCE 0.5.0 +# RESTART REQUIRED +web.debugMode = false + +# Enable/disable global regex substitutions (i.e. shared across repositories) +# +# SINCE 0.5.0 +regex.global = true + +# Example global regex substitutions +# Use !!! to separate the search pattern and the replace pattern +# searchpattern!!!replacepattern +# SINCE 0.5.0 +regex.global.bug = \\b(Bug:)(\\s*[#]?|-){0,1}(\\d+)\\b!!!Bug-Id: $3 +# SINCE 0.5.0 +regex.global.changeid = \\b(Change-Id:\\s*)([A-Za-z0-9]*)\\b!!!Change-Id: $2 + +# Example per-repository regex substitutions overrides global +# SINCE 0.5.0 +regex.myrepository.bug = \\b(Bug:)(\\s*[#]?|-){0,1}(\\d+)\\b!!!Bug-Id: $3 + +# +# Mail Settings +# SINCE 0.6.0 +# +# Mail settings are used to notify administrators of received federation proposals +# + +# ip or hostname of smtp server +# +# SINCE 0.6.0 +mail.server = + +# port to use for smtp requests +# +# SINCE 0.6.0 +mail.port = 25 + +# debug the mail executor +# +# SINCE 0.6.0 +mail.debug = false + +# if your smtp server requires authentication, supply the credentials here +# +# SINCE 0.6.0 +mail.username = +# SINCE 0.6.0 +mail.password = + +# from address for generated emails +# +# SINCE 0.6.0 +mail.fromAddress = + +# List of email addresses for the Gitblit administrators +# +# SPACE-DELIMITED +# SINCE 0.6.0 +mail.adminAddresses = + +# List of email addresses for sending push email notifications. +# +# This key currently requires use of the sendemail.groovy hook script. +# If you set sendemail.groovy in *groovy.postReceiveScripts* then email +# notifications for all repositories (regardless of access restrictions!) +# will be sent to these addresses. +# +# SPACE-DELIMITED +# SINCE 0.8.0 +mail.mailingLists = + +# +# Federation Settings +# SINCE 0.6.0 +# +# A Gitblit federation is a way to backup one Gitblit instance to another. +# +# *git.enableGitServlet* must be true to use this feature. + +# Your federation name is used for federation status acknowledgments. If it is +# unset, and you elect to send a status acknowledgment, your Gitblit instance +# will be identified by its hostname, if available, else your internal ip address. +# The source Gitblit instance will also append your external IP address to your +# identification to differentiate multiple pulling systems behind a single proxy. +# +# SINCE 0.6.0 +federation.name = + +# Specify the passphrase of this Gitblit instance. +# +# An unspecified (empty) passphrase disables processing federation requests. +# +# This value can be anything you want: an integer, a sentence, an haiku, etc. +# Keep the value simple, though, to avoid Java properties file encoding issues. +# +# Changing your passphrase will break any registrations you have established with other +# Gitblit instances. +# +# CASE-SENSITIVE +# SINCE 0.6.0 +# RESTART REQUIRED *(only to enable or disable federation)* +federation.passphrase = + +# Control whether or not this Gitblit instance can receive federation proposals +# from another Gitblit instance. Registering a federated Gitblit is a manual +# process. Proposals help to simplify that process by allowing a remote Gitblit +# instance to send your Gitblit instance the federation pull data. +# +# SINCE 0.6.0 +federation.allowProposals = false + +# The destination folder for cached federation proposals. +# Use forward slashes even on Windows!! +# +# SINCE 0.6.0 +federation.proposalsFolder = proposals + +# The default pull frequency if frequency is unspecified on a registration +# +# SINCE 0.6.0 +federation.defaultFrequency = 60 mins + +# Federation Sets are named groups of repositories. The Federation Sets are +# available for selection in the repository settings page. You can assign a +# repository to one or more sets and then distribute the token for the set. +# This allows you to grant federation pull access to a subset of your available +# repositories. Tokens for federation sets only grant repository pull access. +# +# SPACE-DELIMITED +# CASE-SENSITIVE +# SINCE 0.6.0 +federation.sets = + +# Federation pull registrations +# Registrations are read once, at startup. +# +# RESTART REQUIRED +# +# frequency: +# The shortest frequency allowed is every 5 minutes +# Decimal frequency values are cast to integers +# Frequency values may be specified in mins, hours, or days +# Values that can not be parsed or are unspecified default to *federation.defaultFrequency* +# +# folder: +# if unspecified, the folder is *git.repositoriesFolder* +# if specified, the folder is relative to *git.repositoriesFolder* +# +# bare: +# if true, each repository will be created as a *bare* repository and will not +# have a working directory. +# +# if false, each repository will be created as a normal repository suitable +# for local work. +# +# mirror: +# if true, each repository HEAD is reset to *origin/master* after each pull. +# The repository will be flagged *isFrozen* after the initial clone. +# +# if false, each repository HEAD will point to the FETCH_HEAD of the initial +# clone from the origin until pushed to or otherwise manipulated. +# +# mergeAccounts: +# if true, remote accounts and their permissions are merged into your +# users.properties file +# +# notifyOnError: +# if true and the mail configuration is properly set, administrators will be +# notified by email of pull failures +# +# include and exclude: +# Space-delimited list of repositories to include or exclude from pull +# may be * wildcard to include or exclude all +# may use fuzzy match (e.g. org.eclipse.*) + +# +# (Nearly) Perfect Mirror example +# + +#federation.example1.url = https://go.gitblit.com +#federation.example1.token = 6f3b8a24bf970f17289b234284c94f43eb42f0e4 +#federation.example1.frequency = 120 mins +#federation.example1.folder = +#federation.example1.bare = true +#federation.example1.mirror = true +#federation.example1.mergeAccounts = true + +# +# Advanced Realm Settings +# + +# URL of the LDAP server. +# To use encrypted transport, use either ldaps:// URL for SSL or ldap+tls:// to +# send StartTLS command. +# +# SINCE 1.0.0 +realm.ldap.server = ldap://localhost + +# Login username for LDAP searches. +# If this value is unspecified, anonymous LDAP login will be used. +# +# e.g. mydomain\\username +# +# SINCE 1.0.0 +realm.ldap.username = cn=Directory Manager + +# Login password for LDAP searches. +# +# SINCE 1.0.0 +realm.ldap.password = password + +# The LdapUserService must be backed by another user service for standard user +# and team management. +# default: users.conf +# +# SINCE 1.0.0 +# RESTART REQUIRED +realm.ldap.backingUserService = users.conf + +# Delegate team membership control to LDAP. +# +# If true, team user memberships will be specified by LDAP groups. This will +# disable team selection in Edit User and user selection in Edit Team. +# +# If false, LDAP will only be used for authentication and Gitblit will maintain +# team memberships with the *realm.ldap.backingUserService*. +# +# SINCE 1.0.0 +realm.ldap.maintainTeams = false + +# Root node for all LDAP users +# +# This is the root node from which subtree user searches will begin. +# If blank, Gitblit will search ALL nodes. +# +# SINCE 1.0.0 +realm.ldap.accountBase = OU=Users,OU=UserControl,OU=MyOrganization,DC=MyDomain + +# Filter criteria for LDAP users +# +# Query pattern to use when searching for a user account. This may be any valid +# LDAP query expression, including the standard (&) and (|) operators. +# +# Variables may be injected via the ${variableName} syntax. +# Recognized variables are: +# ${username} - The text entered as the user name +# +# SINCE 1.0.0 +realm.ldap.accountPattern = (&(objectClass=person)(sAMAccountName=${username})) + +# Root node for all LDAP groups to be used as Gitblit Teams +# +# This is the root node from which subtree team searches will begin. +# If blank, Gitblit will search ALL nodes. +# +# SINCE 1.0.0 +realm.ldap.groupBase = OU=Groups,OU=UserControl,OU=MyOrganization,DC=MyDomain + +# Filter criteria for LDAP groups +# +# Query pattern to use when searching for a team. This may be any valid +# LDAP query expression, including the standard (&) and (|) operators. +# +# Variables may be injected via the ${variableName} syntax. +# Recognized variables are: +# ${username} - The text entered as the user name +# ${dn} - The Distinguished Name of the user logged in +# +# All attributes from the LDAP User record are available. For example, if a user +# has an attribute "fullName" set to "John", "(fn=${fullName})" will be +# translated to "(fn=John)". +# +# SINCE 1.0.0 +realm.ldap.groupMemberPattern = (&(objectClass=group)(member=${dn})) + +# LDAP users or groups that should be given administrator privileges. +# +# Teams are specified with a leading '@' character. Groups with spaces in the +# name can be entered as "@team name". +# +# e.g. realm.ldap.admins = john @git_admins "@git admins" +# +# SPACE-DELIMITED +# SINCE 1.0.0 +realm.ldap.admins = @Git_Admins + +# Attribute(s) on the USER record that indicate their display (or full) name. +# Leave blank for no mapping available in LDAP. +# +# This may be a single attribute, or a string of multiple attributes. Examples: +# displayName - Uses the attribute 'displayName' on the user record +# ${personalTitle}. ${givenName} ${surname} - Will concatenate the 3 +# attributes together, with a '.' after personalTitle +# +# SINCE 1.0.0 +realm.ldap.displayName = displayName + +# Attribute(s) on the USER record that indicate their email address. +# Leave blank for no mapping available in LDAP. +# +# This may be a single attribute, or a string of multiple attributes. Examples: +# email - Uses the attribute 'email' on the user record +# ${givenName}.${surname}@gitblit.com -Will concatenate the 2 attributes +# together with a '.' and '@' creating something like first.last@gitblit.com +# +# SINCE 1.0.0 +realm.ldap.email = email + +# Defines whether to synchronize all LDAP users into the backing user service +# +# Valid values: true, false +# If left blank, false is assumed +realm.ldap.synchronizeUsers.enable = false + +# Defines whether to delete non-existent LDAP users from the backing user service +# during synchronization. depends on realm.ldap.synchronizeUsers.enable = true +# +# Valid values: true, false +# If left blank, true is assumed +realm.ldap.synchronizeUsers.removeDeleted = true + +# Attribute on the USER record that indicate their username to be used in gitblit +# when synchronizing users from LDAP +# if blank, Gitblit will use uid +# +# +realm.ldap.uid = uid + +# The RedmineUserService must be backed by another user service for standard user +# and team management. +# default: users.conf +# +# RESTART REQUIRED +realm.redmine.backingUserService = users.conf + +# URL of the Redmine. +realm.redmine.url = http://example.com/redmine + +# +# Server Settings +# + +# The temporary folder to decompress the embedded gitblit webapp. +# +# SINCE 0.5.0 +# RESTART REQUIRED +server.tempFolder = temp + +# Use Jetty NIO connectors. If false, Jetty Socket connectors will be used. +# +# SINCE 0.5.0 +# RESTART REQUIRED +server.useNio = true + +# Context path for the GO application. You might want to change the context +# path if running Gitblit behind a proxy layer such as mod_proxy. +# +# SINCE 0.7.0 +# RESTART REQUIRED +server.contextPath = / + +# Standard http port to serve. <= 0 disables this connector. +# On Unix/Linux systems, ports < 1024 require root permissions. +# Recommended value: 80 or 8080 +# +# SINCE 0.5.0 +# RESTART REQUIRED +server.httpPort = 0 + +# Secure/SSL https port to serve. <= 0 disables this connector. +# On Unix/Linux systems, ports < 1024 require root permissions. +# Recommended value: 443 or 8443 +# +# SINCE 0.5.0 +# RESTART REQUIRED +server.httpsPort = 8443 + +# Port for serving an Apache JServ Protocol (AJP) 1.3 connector for integrating +# Gitblit GO into an Apache HTTP server setup. <= 0 disables this connector. +# Recommended value: 8009 +# +# SINCE 0.9.0 +# RESTART REQUIRED +server.ajpPort = 0 + +# Specify the interface for Jetty to bind the standard connector. +# You may specify an ip or an empty value to bind to all interfaces. +# Specifying localhost will result in Gitblit ONLY listening to requests to +# localhost. +# +# SINCE 0.5.0 +# RESTART REQUIRED +server.httpBindInterface = localhost + +# Specify the interface for Jetty to bind the secure connector. +# You may specify an ip or an empty value to bind to all interfaces. +# Specifying localhost will result in Gitblit ONLY listening to requests to +# localhost. +# +# SINCE 0.5.0 +# RESTART REQUIRED +server.httpsBindInterface = localhost + +# Specify the interface for Jetty to bind the AJP connector. +# You may specify an ip or an empty value to bind to all interfaces. +# Specifying localhost will result in Gitblit ONLY listening to requests to +# localhost. +# +# SINCE 0.9.0 +# RESTART REQUIRED +server.ajpBindInterface = localhost + +# Password for SSL keystore. +# Keystore password and certificate password must match. +# This is provided for convenience, its probably more secure to set this value +# using the --storePassword command line parameter. +# +# If you are using the official JRE or JDK from Oracle you may not have the +# JCE Unlimited Strength Jurisdiction Policy files bundled with your JVM. Because +# of this, your store/key password can not exceed 7 characters. If you require +# longer passwords you may need to install the JCE Unlimited Strength Jurisdiction +# Policy files from Oracle. +# +# http://www.oracle.com/technetwork/java/javase/downloads/index.html +# +# Gitblit and the Gitblit Certificate Authority will both indicate if Unlimited +# Strength encryption is available. +# +# SINCE 0.5.0 +# RESTART REQUIRED +server.storePassword = gitblit + +# If serving over https (recommended) you might consider requiring clients to +# authenticate with ssl certificates. If enabled, only https clients with the +# a valid client certificate will be able to access Gitblit. +# +# If disabled, client certificate authentication is optional and will be tried +# first before falling-back to form authentication or basic authentication. +# +# Requiring client certificates to access any of Gitblit may be too extreme, +# consider this carefully. +# +# SINCE 1.2.0 +# RESTART REQUIRED +server.requireClientCertificates = false + +# Port for shutdown monitor to listen on. +# +# SINCE 0.5.0 +# RESTART REQUIRED +server.shutdownPort = 8081 -- cgit v1.2.3 From 296ace81ee4f6d7d405071a91706511d0aa1bc6d Mon Sep 17 00:00:00 2001 From: mschaefers Date: Mon, 3 Dec 2012 10:21:35 +0100 Subject: cache results of LDAP user synchronization for a configurable amount of time (realm.ldap.ldapCachePeriod = 2 MINUTES) --- distrib/gitblit.properties | 9 +++ src/com/gitblit/LdapUserService.java | 123 ++++++++++++++++++++--------------- 2 files changed, 81 insertions(+), 51 deletions(-) (limited to 'distrib/gitblit.properties') diff --git a/distrib/gitblit.properties b/distrib/gitblit.properties index a1ec3053..76112da2 100644 --- a/distrib/gitblit.properties +++ b/distrib/gitblit.properties @@ -1057,6 +1057,15 @@ realm.ldap.displayName = displayName # SINCE 1.0.0 realm.ldap.email = email +# Defines the cache period to be used when caching LDAP queries. This is currently +# only used for LDAP user synchronization. +# +# Must be of the form ' ' where is one of 'MILLISECONDS', 'SECONDS', 'MINUTES', 'HOURS', 'DAYS' +# default: 2 MINUTES +# +# RESTART REQUIRED +realm.ldap.ldapCachePeriod = 2 MINUTES + # Defines whether to synchronize all LDAP users into the backing user service # # Valid values: true, false diff --git a/src/com/gitblit/LdapUserService.java b/src/com/gitblit/LdapUserService.java index b5d87a66..ddfbaa40 100644 --- a/src/com/gitblit/LdapUserService.java +++ b/src/com/gitblit/LdapUserService.java @@ -23,6 +23,7 @@ import java.security.GeneralSecurityException; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -55,88 +56,108 @@ public class LdapUserService extends GitblitUserService { public static final String LDAP_PASSWORD_KEY = "StoredInLDAP"; private IStoredSettings settings; + private long lastLdapUserSyncTs = 0L; + private long ldapSyncCachePeriod; public LdapUserService() { super(); } + private void initializeLdapCaches() { + final String cacheDuration = settings.getString(Keys.realm.ldap.ldapCachePeriod, "2 MINUTES"); + final long duration; + final TimeUnit timeUnit; + try { + final String[] s = cacheDuration.split(" ", 2); + duration = Long.parseLong(s[0]); + timeUnit = TimeUnit.valueOf(s[1]); + ldapSyncCachePeriod = timeUnit.toMillis(duration); + } catch (RuntimeException ex) { + throw new IllegalArgumentException(Keys.realm.ldap.ldapCachePeriod + " must have format ' ' where is one of 'MILLISECONDS', 'SECONDS', 'MINUTES', 'HOURS', 'DAYS'"); + } + } + @Override public void setup(IStoredSettings settings) { this.settings = settings; String file = settings.getString(Keys.realm.ldap.backingUserService, "users.conf"); File realmFile = GitBlit.getFileOrFolder(file); + initializeLdapCaches(); + serviceImpl = createUserService(realmFile); logger.info("LDAP User Service backed by " + serviceImpl.toString()); synchronizeLdapUsers(); } - protected void synchronizeLdapUsers() { + protected synchronized void synchronizeLdapUsers() { final boolean enabled = settings.getBoolean(Keys.realm.ldap.synchronizeUsers.enable, false); - if (!enabled) { - return; - } - final boolean deleteRemovedLdapUsers = settings.getBoolean(Keys.realm.ldap.synchronizeUsers.removeDeleted, true); - LDAPConnection ldapConnection = getLdapConnection(); - if (ldapConnection != null) { - try { - String accountBase = settings.getString(Keys.realm.ldap.accountBase, ""); - String uidAttribute = settings.getString(Keys.realm.ldap.uid, "uid"); - String accountPattern = settings.getString(Keys.realm.ldap.accountPattern, "(&(objectClass=person)(sAMAccountName=${username}))"); - accountPattern = StringUtils.replace(accountPattern, "${username}", "*"); - - SearchResult result = doSearch(ldapConnection, accountBase, accountPattern); - if (result != null && result.getEntryCount() > 0) { - final Map ldapUsers = new HashMap(); - - for (SearchResultEntry loggingInUser : result.getSearchEntries()) { - - final String username = loggingInUser.getAttribute(uidAttribute).getValue(); - logger.debug("LDAP synchronizing: " + username); - - UserModel user = getUserModel(username); - if (user == null) { - user = new UserModel(username); - } + if (enabled) { + if (lastLdapUserSyncTs + ldapSyncCachePeriod < System.currentTimeMillis()) { + final boolean deleteRemovedLdapUsers = settings.getBoolean(Keys.realm.ldap.synchronizeUsers.removeDeleted, true); + LDAPConnection ldapConnection = getLdapConnection(); + if (ldapConnection != null) { + try { + String accountBase = settings.getString(Keys.realm.ldap.accountBase, ""); + String uidAttribute = settings.getString(Keys.realm.ldap.uid, "uid"); + String accountPattern = settings.getString(Keys.realm.ldap.accountPattern, "(&(objectClass=person)(sAMAccountName=${username}))"); + accountPattern = StringUtils.replace(accountPattern, "${username}", "*"); + + SearchResult result = doSearch(ldapConnection, accountBase, accountPattern); + if (result != null && result.getEntryCount() > 0) { + final Map ldapUsers = new HashMap(); + + for (SearchResultEntry loggingInUser : result.getSearchEntries()) { + + final String username = loggingInUser.getAttribute(uidAttribute).getValue(); + logger.debug("LDAP synchronizing: " + username); + + UserModel user = getUserModel(username); + if (user == null) { + user = new UserModel(username); + } - if (!supportsTeamMembershipChanges()) - getTeamsFromLdap(ldapConnection, username, loggingInUser, user); + if (!supportsTeamMembershipChanges()) + getTeamsFromLdap(ldapConnection, username, loggingInUser, user); - // Get User Attributes - setUserAttributes(user, loggingInUser); + // Get User Attributes + setUserAttributes(user, loggingInUser); - // store in map - ldapUsers.put(username, user); - } + // store in map + ldapUsers.put(username, user); + } - if (deleteRemovedLdapUsers) { - logger.debug("detecting removed LDAP users..."); + if (deleteRemovedLdapUsers) { + logger.debug("detecting removed LDAP users..."); - for (UserModel userModel : super.getAllUsers()) { - if (LDAP_PASSWORD_KEY.equals(userModel.password)) { - if (! ldapUsers.containsKey(userModel.username)) { - logger.info("deleting removed LDAP user " + userModel.username + " from backing user service"); - super.deleteUser(userModel.username); + for (UserModel userModel : super.getAllUsers()) { + if (LDAP_PASSWORD_KEY.equals(userModel.password)) { + if (! ldapUsers.containsKey(userModel.username)) { + logger.info("deleting removed LDAP user " + userModel.username + " from backing user service"); + super.deleteUser(userModel.username); + } + } } } - } - } - super.updateUserModels(ldapUsers.values()); + super.updateUserModels(ldapUsers.values()); - if (!supportsTeamMembershipChanges()) { - final Map userTeams = new HashMap(); - for (UserModel user : ldapUsers.values()) { - for (TeamModel userTeam : user.teams) { - userTeams.put(userTeam.name, userTeam); + if (!supportsTeamMembershipChanges()) { + final Map userTeams = new HashMap(); + for (UserModel user : ldapUsers.values()) { + for (TeamModel userTeam : user.teams) { + userTeams.put(userTeam.name, userTeam); + } + } + updateTeamModels(userTeams.values()); } } - updateTeamModels(userTeams.values()); + lastLdapUserSyncTs = System.currentTimeMillis(); + } finally { + ldapConnection.close(); } } - } finally { - ldapConnection.close(); } } } -- cgit v1.2.3 From 334d158ad313aaf56e33d55981f969fa05d8547e Mon Sep 17 00:00:00 2001 From: mschaefers Date: Tue, 4 Dec 2012 18:55:17 +0100 Subject: FIX: setting line separator back to Windows style --- distrib/gitblit.properties | 2422 +++++++++++++++---------------- src/com/gitblit/ConfigUserService.java | 2148 +++++++++++++-------------- src/com/gitblit/FileUserService.java | 2292 ++++++++++++++--------------- src/com/gitblit/GitblitUserService.java | 608 ++++---- src/com/gitblit/IUserService.java | 650 ++++----- src/com/gitblit/LdapUserService.java | 974 ++++++------- 6 files changed, 4547 insertions(+), 4547 deletions(-) (limited to 'distrib/gitblit.properties') diff --git a/distrib/gitblit.properties b/distrib/gitblit.properties index 76112da2..f611adf9 100644 --- a/distrib/gitblit.properties +++ b/distrib/gitblit.properties @@ -1,1211 +1,1211 @@ -# -# Git Servlet Settings -# - -# Base folder for repositories. -# This folder may contain bare and non-bare repositories but Gitblit will only -# allow you to push to bare repositories. -# Use forward slashes even on Windows!! -# e.g. c:/gitrepos -# -# SINCE 0.5.0 -# RESTART REQUIRED -git.repositoriesFolder = git - -# Build the available repository list at startup and cache this list for reuse. -# This reduces disk io when presenting the repositories page, responding to rpcs, -# etc, but it means that Gitblit will not automatically identify repositories -# added or deleted by external tools. -# -# For this case you can use curl, wget, etc to issue an rpc request to clear the -# cache (e.g. https://localhost/rpc?req=CLEAR_REPOSITORY_CACHE) -# -# SINCE 1.1.0 -git.cacheRepositoryList = true - -# Search the repositories folder subfolders for other repositories. -# Repositories MAY NOT be nested (i.e. one repository within another) -# but they may be grouped together in subfolders. -# e.g. c:/gitrepos/libraries/mylibrary.git -# c:/gitrepos/libraries/myotherlibrary.git -# -# SINCE 0.5.0 -git.searchRepositoriesSubfolders = true - -# Maximum number of folders to recurse into when searching for repositories. -# The default value, -1, disables depth limits. -# -# SINCE 1.1.0 -git.searchRecursionDepth = -1 - -# List of regex exclusion patterns to match against folders found in -# *git.repositoriesFolder*. -# Use forward slashes even on Windows!! -# e.g. test/jgit\.git -# -# SPACE-DELIMITED -# CASE-SENSITIVE -# SINCE 1.1.0 -git.searchExclusions = - -# List of regex url patterns for extracting a repository name when locating -# submodules. -# e.g. git.submoduleUrlPatterns = .*?://github.com/(.*) will extract -# *gitblit/gitblit.git* from *git://github.com/gitblit/gitblit.git* -# If no matches are found then the submodule repository name is assumed to be -# whatever trails the last / character. (e.g. gitblit.git). -# -# SPACE-DELIMITED -# CASE-SENSITIVE -# SINCE 1.1.0 -git.submoduleUrlPatterns = .*?://github.com/(.*) - -# Allow push/pull over http/https with JGit servlet. -# If you do NOT want to allow Git clients to clone/push to Gitblit set this -# to false. You might want to do this if you are only using ssh:// or git://. -# If you set this false, consider changing the *web.otherUrls* setting to -# indicate your clone/push urls. -# -# SINCE 0.5.0 -git.enableGitServlet = true - -# If you want to restrict all git servlet access to those with valid X509 client -# certificates then set this value to true. -# -# SINCE 1.2.0 -git.requiresClientCertificate = false - -# Enforce date checks on client certificates to ensure that they are not being -# used prematurely and that they have not expired. -# -# SINCE 1.2.0 -git.enforceCertificateValidity = true - -# List of OIDs to extract from a client certificate DN to map a certificate to -# an account username. -# -# e.g. git.certificateUsernameOIDs = CN -# e.g. git.certificateUsernameOIDs = FirstName LastName -# -# SPACE-DELIMITED -# SINCE 1.2.0 -git.certificateUsernameOIDs = CN - -# Only serve/display bare repositories. -# If there are non-bare repositories in git.repositoriesFolder and this setting -# is true, they will be excluded from the ui. -# -# SINCE 0.9.0 -git.onlyAccessBareRepositories = false - -# Allow an authenticated user to create a destination repository on a push if -# the repository does not already exist. -# -# Administrator accounts can create a repository in any project. -# These repositories are created with the default access restriction and authorization -# control values. The pushing account is set as the owner. -# -# Non-administrator accounts with the CREATE role may create personal repositories. -# These repositories are created as VIEW restricted for NAMED users. -# The pushing account is set as the owner. -# -# SINCE 1.2.0 -git.allowCreateOnPush = true - -# The default access restriction for new repositories. -# Valid values are NONE, PUSH, CLONE, VIEW -# NONE = anonymous view, clone, & push -# PUSH = anonymous view & clone and authenticated push -# CLONE = anonymous view, authenticated clone & push -# VIEW = authenticated view, clone, & push -# -# SINCE 1.0.0 -git.defaultAccessRestriction = NONE - -# The default authorization control for new repositories. -# Valid values are AUTHENTICATED and NAMED -# AUTHENTICATED = any authenticated user is granted restricted access -# NAMED = only named users/teams are granted restricted access -# -# SINCE 1.1.0 -git.defaultAuthorizationControl = NAMED - -# Enable JGit-based garbage collection. (!!EXPERIMENTAL!!) -# -# USE AT YOUR OWN RISK! -# -# If enabled, the garbage collection executor scans all repositories once a day -# at the hour of your choosing. The GC executor will take each repository "offline", -# one-at-a-time, to check if the repository satisfies it's GC trigger requirements. -# -# While the repository is offline it will be inaccessible from the web UI or from -# any of the other services (git, rpc, rss, etc). -# -# Gitblit's GC Executor MAY NOT PLAY NICE with the other Git kids on the block, -# especially on Windows systems, so if you are using other tools please coordinate -# their usage with your GC Executor schedule or do not use this feature. -# -# The GC algorithm complex and the JGit team advises caution when using their -# young implementation of GC. -# -# http://wiki.eclipse.org/EGit/New_and_Noteworthy/2.1#Garbage_Collector_and_Repository_Storage_Statistics -# -# EXPERIMENTAL -# SINCE 1.2.0 -# RESTART REQUIRED -git.enableGarbageCollection = false - -# Hour of the day for the GC Executor to scan repositories. -# This value is in 24-hour time. -# -# SINCE 1.2.0 -git.garbageCollectionHour = 0 - -# The default minimum total filesize of loose objects to trigger early garbage -# collection. -# -# You may specify a custom threshold for a repository in the repository's settings. -# Common unit suffixes of k, m, or g are supported. -# -# SINCE 1.2.0 -git.defaultGarbageCollectionThreshold = 500k - -# The default period, in days, between GCs for a repository. If the total filesize -# of the loose object exceeds *git.garbageCollectionThreshold* or the repository's -# custom threshold, this period will be short-circuited. -# -# e.g. if a repository collects 100KB of loose objects every day with a 500KB -# threshold and a period of 7 days, it will take 5 days for the loose objects to -# be collected, packed, and pruned. -# -# OR -# -# if a repository collects 10KB of loose objects every day with a 500KB threshold -# and a period of 7 days, it will take the full 7 days for the loose objects to be -# collected, packed, and pruned. -# -# You may specify a custom period for a repository in the repository's settings. -# -# The minimum value is 1 day since the GC Executor only runs once a day. -# -# SINCE 1.2.0 -git.defaultGarbageCollectionPeriod = 7 - -# Number of bytes of a pack file to load into memory in a single read operation. -# This is the "page size" of the JGit buffer cache, used for all pack access -# operations. All disk IO occurs as single window reads. Setting this too large -# may cause the process to load more data than is required; setting this too small -# may increase the frequency of read() system calls. -# -# Default on JGit is 8 KiB on all platforms. -# -# Common unit suffixes of k, m, or g are supported. -# Documentation courtesy of the Gerrit project. -# -# SINCE 1.0.0 -# RESTART REQUIRED -git.packedGitWindowSize = 8k - -# Maximum number of bytes to load and cache in memory from pack files. If JGit -# needs to access more than this many bytes it will unload less frequently used -# windows to reclaim memory space within the process. As this buffer must be shared -# with the rest of the JVM heap, it should be a fraction of the total memory available. -# -# The JGit team recommends setting this value larger than the size of your biggest -# repository. This ensures you can serve most requests from memory. -# -# Default on JGit is 10 MiB on all platforms. -# -# Common unit suffixes of k, m, or g are supported. -# Documentation courtesy of the Gerrit project. -# -# SINCE 1.0.0 -# RESTART REQUIRED -git.packedGitLimit = 10m - -# Maximum number of bytes to reserve for caching base objects that multiple deltafied -# objects reference. By storing the entire decompressed base object in a cache Git -# is able to avoid unpacking and decompressing frequently used base objects multiple times. -# -# Default on JGit is 10 MiB on all platforms. You probably do not need to adjust -# this value. -# -# Common unit suffixes of k, m, or g are supported. -# Documentation courtesy of the Gerrit project. -# -# SINCE 1.0.0 -# RESTART REQUIRED -git.deltaBaseCacheLimit = 10m - -# Maximum number of pack files to have open at once. A pack file must be opened -# in order for any of its data to be available in a cached window. -# -# If you increase this to a larger setting you may need to also adjust the ulimit -# on file descriptors for the host JVM, as Gitblit needs additional file descriptors -# available for network sockets and other repository data manipulation. -# -# Default on JGit is 128 file descriptors on all platforms. -# Documentation courtesy of the Gerrit project. -# -# SINCE 1.0.0 -# RESTART REQUIRED -git.packedGitOpenFiles = 128 - -# Largest object size, in bytes, that JGit will allocate as a contiguous byte -# array. Any file revision larger than this threshold will have to be streamed, -# typically requiring the use of temporary files under $GIT_DIR/objects to implement -# psuedo-random access during delta decompression. -# -# Servers with very high traffic should set this to be larger than the size of -# their common big files. For example a server managing the Android platform -# typically has to deal with ~10-12 MiB XML files, so 15 m would be a reasonable -# setting in that environment. Setting this too high may cause the JVM to run out -# of heap space when handling very big binary files, such as device firmware or -# CD-ROM ISO images. Make sure to adjust your JVM heap accordingly. -# -# Default is 50 MiB on all platforms. -# -# Common unit suffixes of k, m, or g are supported. -# Documentation courtesy of the Gerrit project. -# -# SINCE 1.0.0 -# RESTART REQUIRED -git.streamFileThreshold = 50m - -# When true, JGit will use mmap() rather than malloc()+read() to load data from -# pack files. The use of mmap can be problematic on some JVMs as the garbage -# collector must deduce that a memory mapped segment is no longer in use before -# a call to munmap() can be made by the JVM native code. -# -# In server applications (such as Gitblit) that need to access many pack files, -# setting this to true risks artificially running out of virtual address space, -# as the garbage collector cannot reclaim unused mapped spaces fast enough. -# -# Default on JGit is false. Although potentially slower, it yields much more -# predictable behavior. -# Documentation courtesy of the Gerrit project. -# -# SINCE 1.0.0 -# RESTART REQUIRED -git.packedGitMmap = false - -# -# Groovy Integration -# - -# Location of Groovy scripts to use for Pre and Post receive hooks. -# Use forward slashes even on Windows!! -# e.g. c:/groovy -# -# RESTART REQUIRED -# SINCE 0.8.0 -groovy.scriptsFolder = groovy - -# Specify the directory Grape uses for downloading libraries. -# http://groovy.codehaus.org/Grape -# -# RESTART REQUIRED -# SINCE 1.0.0 -groovy.grapeFolder = groovy/grape - -# Scripts to execute on Pre-Receive. -# -# These scripts execute after an incoming push has been parsed and validated -# but BEFORE the changes are applied to the repository. You might reject a -# push in this script based on the repository and branch the push is attempting -# to change. -# -# Script names are case-sensitive on case-sensitive file systems. You may omit -# the traditional ".groovy" from this list if your file extension is ".groovy" -# -# NOTE: -# These scripts are only executed when pushing to *Gitblit*, not to other Git -# tooling you may be using. Also note that these scripts are shared between -# repositories. These are NOT repository-specific scripts! Within the script -# you may customize the control-flow for a specific repository by checking the -# *repository* variable. -# -# SPACE-DELIMITED -# CASE-SENSITIVE -# SINCE 0.8.0 -groovy.preReceiveScripts = - -# Scripts to execute on Post-Receive. -# -# These scripts execute AFTER an incoming push has been applied to a repository. -# You might trigger a continuous-integration build here or send a notification. -# -# Script names are case-sensitive on case-sensitive file systems. You may omit -# the traditional ".groovy" from this list if your file extension is ".groovy" -# -# NOTE: -# These scripts are only executed when pushing to *Gitblit*, not to other Git -# tooling you may be using. Also note that these scripts are shared between -# repositories. These are NOT repository-specific scripts! Within the script -# you may customize the control-flow for a specific repository by checking the -# *repository* variable. -# -# SPACE-DELIMITED -# CASE-SENSITIVE -# SINCE 0.8.0 -groovy.postReceiveScripts = - -# Repository custom fields for Groovy Hook mechanism -# -# List of key=label pairs of custom fields to prompt for in the Edit Repository -# page. These keys are stored in the repository's git config file in the -# section [gitblit "customFields"]. Key names are alphanumeric only. These -# fields are intended to be used for the Groovy hook mechanism where a script -# can adjust it's execution based on the custom fields stored in the repository -# config. -# -# e.g. "commitMsgRegex=Commit Message Regular Expression" anotherProperty=Another -# -# SPACE-DELIMITED -# SINCE 1.0.0 -groovy.customFields = - -# -# Authentication Settings -# - -# Require authentication to see everything but the admin pages -# -# SINCE 0.5.0 -# RESTART REQUIRED -web.authenticateViewPages = false - -# Require admin authentication for the admin functions and pages -# -# SINCE 0.5.0 -# RESTART REQUIRED -web.authenticateAdminPages = true - -# Allow Gitblit to store a cookie in the user's browser for automatic -# authentication. The cookie is generated by the user service. -# -# SINCE 0.5.0 -web.allowCookieAuthentication = true - -# Config file for storing project metadata -# -# SINCE 1.2.0 -web.projectsFile = projects.conf - -# Either the full path to a user config file (users.conf) -# OR the full path to a simple user properties file (users.properties) -# OR a fully qualified class name that implements the IUserService interface. -# -# Alternative user services: -# com.gitblit.LdapUserService -# com.gitblit.RedmineUserService -# -# Any custom user service implementation must have a public default constructor. -# -# SINCE 0.5.0 -# RESTART REQUIRED -realm.userService = users.conf - -# How to store passwords. -# Valid values are plain, md5, or combined-md5. md5 is the hash of password. -# combined-md5 is the hash of username.toLowerCase()+password. -# Default is md5. -# -# SINCE 0.5.0 -realm.passwordStorage = md5 - -# Minimum valid length for a plain text password. -# Default value is 5. Absolute minimum is 4. -# -# SINCE 0.5.0 -realm.minPasswordLength = 5 - -# -# Gitblit Web Settings -# -# If blank Gitblit is displayed. -# -# SINCE 0.5.0 -web.siteName = - -# If *web.authenticateAdminPages*=true, users with "admin" role can create -# repositories, create users, and edit repository metadata. -# -# If *web.authenticateAdminPages*=false, any user can execute the aforementioned -# functions. -# -# SINCE 0.5.0 -web.allowAdministration = true - -# Allows rpc clients to list repositories and possibly manage or administer the -# Gitblit server, if the authenticated account has administrator permissions. -# See *web.enableRpcManagement* and *web.enableRpcAdministration*. -# -# SINCE 0.7.0 -web.enableRpcServlet = true - -# Allows rpc clients to manage repositories and users of the Gitblit instance, -# if the authenticated account has administrator permissions. -# Requires *web.enableRpcServlet=true*. -# -# SINCE 0.7.0 -web.enableRpcManagement = false - -# Allows rpc clients to control the server settings and monitor the health of this -# this Gitblit instance, if the authenticated account has administrator permissions. -# Requires *web.enableRpcServlet=true* and *web.enableRpcManagement*. -# -# SINCE 0.7.0 -web.enableRpcAdministration = false - -# Full path to a configurable robots.txt file. With this file you can control -# what parts of your Gitblit server respectable robots are allowed to traverse. -# http://googlewebmastercentral.blogspot.com/2008/06/improving-on-robots-exclusion-protocol.html -# -# SINCE 1.0.0 -web.robots.txt = - -# If true, the web ui layout will respond and adapt to the browser's dimensions. -# if false, the web ui will use a 940px fixed-width layout. -# http://twitter.github.com/bootstrap/scaffolding.html#responsive -# -# SINCE 1.0.0 -web.useResponsiveLayout = true - -# Allow Gravatar images to be displayed in Gitblit pages. -# -# SINCE 0.8.0 -web.allowGravatar = true - -# Allow dynamic zip downloads. -# -# SINCE 0.5.0 -web.allowZipDownloads = true - -# If *web.allowZipDownloads=true* the following formats will be displayed for -# download compressed archive links: -# -# zip = standard .zip -# tar = standard tar format (preserves *nix permissions and symlinks) -# gz = gz-compressed tar -# xz = xz-compressed tar -# bzip2 = bzip2-compressed tar -# -# SPACE-DELIMITED -# SINCE 1.2.0 -web.compressedDownloads = zip gz - -# Allow optional Lucene integration. Lucene indexing is an opt-in feature. -# A repository may specify branches to index with Lucene instead of using Git -# commit traversal. There are scenarios where you may want to completely disable -# Lucene indexing despite a repository specifying indexed branches. One such -# scenario is on a resource-constrained federated Gitblit mirror. -# -# SINCE 0.9.0 -web.allowLuceneIndexing = true - -# Controls the length of shortened commit hash ids -# -# SINCE 1.2.0 -web.shortCommitIdLength = 6 - -# Use Clippy (Flash solution) to provide a copy-to-clipboard button. -# If false, a button with a more primitive JavaScript-based prompt box will -# offer a 3-step (click, ctrl+c, enter) copy-to-clipboard alternative. -# -# SINCE 0.8.0 -web.allowFlashCopyToClipboard = true - -# Default number of entries to include in RSS Syndication links -# -# SINCE 0.5.0 -web.syndicationEntries = 25 - -# Show the size of each repository on the repositories page. -# This requires recursive traversal of each repository folder. This may be -# non-performant on some operating systems and/or filesystems. -# -# SINCE 0.5.2 -web.showRepositorySizes = true - -# List of custom regex expressions that can be displayed in the Filters menu -# of the Repositories and Activity pages. Keep them very simple because you -# are likely to run into encoding issues if they are too complex. -# -# Use !!! to separate the filters -# -# SINCE 0.8.0 -web.customFilters = - -# Show federation registrations (without token) and the current pull status -# to non-administrator users. -# -# SINCE 0.6.0 -web.showFederationRegistrations = false - -# This is the message displayed when *web.authenticateViewPages=true*. -# This can point to a file with Markdown content. -# Specifying "gitblit" uses the internal login message. -# -# SINCE 0.7.0 -web.loginMessage = gitblit - -# This is the message displayed above the repositories table. -# This can point to a file with Markdown content. -# Specifying "gitblit" uses the internal welcome message. -# -# SINCE 0.5.0 -web.repositoriesMessage = gitblit - -# Ordered list of charsets/encodings to use when trying to display a blob. -# If empty, UTF-8 and ISO-8859-1 are used. The server's default charset -# is always appended to the encoding list. If all encodings fail to cleanly -# decode the blob content, UTF-8 will be used with the standard malformed -# input/unmappable character replacement strings. -# -# SPACE-DELIMITED -# SINCE 1.0.0 -web.blobEncodings = UTF-8 ISO-8859-1 - -# Manually set the default timezone to be used by Gitblit for display in the -# web ui. This value is independent of the JVM timezone. Specifying a blank -# value will default to the JVM timezone. -# e.g. America/New_York, US/Pacific, UTC, Europe/Berlin -# -# SINCE 0.9.0 -# RESTART REQUIRED -web.timezone = - -# Use the client timezone when formatting dates. -# This uses AJAX to determine the browser's timezone and may require more -# server overhead because a Wicket session is created. All Gitblit pages -# attempt to be stateless, if possible. -# -# SINCE 0.5.0 -# RESTART REQUIRED -web.useClientTimezone = false - -# Time format -# -# -# SINCE 0.8.0 -web.timeFormat = HH:mm - -# Short date format -# -# -# SINCE 0.5.0 -web.datestampShortFormat = yyyy-MM-dd - -# Long date format -# -# SINCE 0.8.0 -web.datestampLongFormat = EEEE, MMMM d, yyyy - -# Long timestamp format -# -# -# SINCE 0.5.0 -web.datetimestampLongFormat = EEEE, MMMM d, yyyy HH:mm Z - -# Mount URL parameters -# This setting controls if pretty or parameter URLs are used. -# i.e. -# if true: -# http://localhost/commit/myrepo/abcdef -# if false: -# http://localhost/commit/?r=myrepo&h=abcdef -# -# SINCE 0.5.0 -# RESTART REQUIRED -web.mountParameters = true - -# Some servlet containers (e.g. Tomcat >= 6.0.10) disallow '/' (%2F) encoding -# in URLs as a security precaution for proxies. This setting tells Gitblit -# to preemptively replace '/' with '*' or '!' for url string parameters. -# -# -# -# Add *-Dorg.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH=true* to your -# *CATALINA_OPTS* or to your JVM launch parameters -# -# SINCE 0.5.2 -web.forwardSlashCharacter = / - -# Show other URLs on the summary page for accessing your git repositories -# Use spaces to separate urls. {0} is the token for the repository name. -# e.g. -# web.otherUrls = ssh://localhost/git/{0} git://localhost/git/{0} -# -# SPACE-DELIMITED -# SINCE 0.5.0 -web.otherUrls = - -# Choose how to present the repositories list. -# grouped = group nested/subfolder repositories together (no sorting) -# flat = flat list of repositories (sorting allowed) -# -# SINCE 0.5.0 -web.repositoryListType = grouped - -# If using a grouped repository list and there are repositories at the -# root level of your repositories folder, you may specify the displayed -# group name with this setting. This value is only used for web presentation. -# -# SINCE 0.5.0 -web.repositoryRootGroupName = main - -# Display the repository swatch color next to the repository name link in the -# repositories list. -# -# SINCE 0.8.0 -web.repositoryListSwatches = true - -# Choose the diff presentation style: gitblt, gitweb, or plain -# -# SINCE 0.5.0 -web.diffStyle = gitblit - -# Control if email addresses are shown in web ui -# -# SINCE 0.5.0 -web.showEmailAddresses = true - -# Shows a combobox in the page links header with commit, committer, and author -# search selection. Default search is commit. -# -# SINCE 0.5.0 -web.showSearchTypeSelection = false - -# Generates a line graph of repository activity over time on the Summary page. -# This uses the Google Charts API. -# -# SINCE 0.5.0 -web.generateActivityGraph = true - -# The number of days to show on the activity page. -# Value must exceed 0 else default of 14 is used -# -# SINCE 0.8.0 -web.activityDuration = 14 - -# The number of commits to display on the summary page -# Value must exceed 0 else default of 20 is used -# -# SINCE 0.5.0 -web.summaryCommitCount = 16 - -# The number of tags/branches to display on the summary page. -# -1 = all tags/branches -# 0 = hide tags/branches -# N = N tags/branches -# -# SINCE 0.5.0 -web.summaryRefsCount = 5 - -# The number of items to show on a page before showing the first, prev, next -# pagination links. A default if 50 is used for any invalid value. -# -# SINCE 0.5.0 -web.itemsPerPage = 50 - -# Registered file extensions to ignore during Lucene indexing -# -# SPACE-DELIMITED -# SINCE 0.9.0 -web.luceneIgnoreExtensions = 7z arc arj bin bmp dll doc docx exe gif gz jar jpg lib lzh odg odf odt pdf ppt png so swf xcf xls xlsx zip - -# Registered extensions for google-code-prettify -# -# SPACE-DELIMITED -# SINCE 0.5.0 -web.prettyPrintExtensions = c cpp cs css frm groovy htm html java js php pl prefs properties py rb scala sh sql xml vb - -# Registered extensions for markdown transformation -# -# SPACE-DELIMITED -# CASE-SENSITIVE -# SINCE 0.5.0 -web.markdownExtensions = md mkd markdown MD MKD - -# Image extensions -# -# SPACE-DELIMITED -# SINCE 0.5.0 -web.imageExtensions = bmp jpg gif png - -# Registered extensions for binary blobs -# -# SPACE-DELIMITED -# SINCE 0.5.0 -web.binaryExtensions = jar pdf tar.gz zip - -# Aggressive heap management will run the garbage collector on every generated -# page. This slows down page generation a little but improves heap consumption. -# -# SINCE 0.5.0 -web.aggressiveHeapManagement = false - -# Run the webapp in debug mode -# -# SINCE 0.5.0 -# RESTART REQUIRED -web.debugMode = false - -# Enable/disable global regex substitutions (i.e. shared across repositories) -# -# SINCE 0.5.0 -regex.global = true - -# Example global regex substitutions -# Use !!! to separate the search pattern and the replace pattern -# searchpattern!!!replacepattern -# SINCE 0.5.0 -regex.global.bug = \\b(Bug:)(\\s*[#]?|-){0,1}(\\d+)\\b!!!Bug-Id: $3 -# SINCE 0.5.0 -regex.global.changeid = \\b(Change-Id:\\s*)([A-Za-z0-9]*)\\b!!!Change-Id: $2 - -# Example per-repository regex substitutions overrides global -# SINCE 0.5.0 -regex.myrepository.bug = \\b(Bug:)(\\s*[#]?|-){0,1}(\\d+)\\b!!!Bug-Id: $3 - -# -# Mail Settings -# SINCE 0.6.0 -# -# Mail settings are used to notify administrators of received federation proposals -# - -# ip or hostname of smtp server -# -# SINCE 0.6.0 -mail.server = - -# port to use for smtp requests -# -# SINCE 0.6.0 -mail.port = 25 - -# debug the mail executor -# -# SINCE 0.6.0 -mail.debug = false - -# if your smtp server requires authentication, supply the credentials here -# -# SINCE 0.6.0 -mail.username = -# SINCE 0.6.0 -mail.password = - -# from address for generated emails -# -# SINCE 0.6.0 -mail.fromAddress = - -# List of email addresses for the Gitblit administrators -# -# SPACE-DELIMITED -# SINCE 0.6.0 -mail.adminAddresses = - -# List of email addresses for sending push email notifications. -# -# This key currently requires use of the sendemail.groovy hook script. -# If you set sendemail.groovy in *groovy.postReceiveScripts* then email -# notifications for all repositories (regardless of access restrictions!) -# will be sent to these addresses. -# -# SPACE-DELIMITED -# SINCE 0.8.0 -mail.mailingLists = - -# -# Federation Settings -# SINCE 0.6.0 -# -# A Gitblit federation is a way to backup one Gitblit instance to another. -# -# *git.enableGitServlet* must be true to use this feature. - -# Your federation name is used for federation status acknowledgments. If it is -# unset, and you elect to send a status acknowledgment, your Gitblit instance -# will be identified by its hostname, if available, else your internal ip address. -# The source Gitblit instance will also append your external IP address to your -# identification to differentiate multiple pulling systems behind a single proxy. -# -# SINCE 0.6.0 -federation.name = - -# Specify the passphrase of this Gitblit instance. -# -# An unspecified (empty) passphrase disables processing federation requests. -# -# This value can be anything you want: an integer, a sentence, an haiku, etc. -# Keep the value simple, though, to avoid Java properties file encoding issues. -# -# Changing your passphrase will break any registrations you have established with other -# Gitblit instances. -# -# CASE-SENSITIVE -# SINCE 0.6.0 -# RESTART REQUIRED *(only to enable or disable federation)* -federation.passphrase = - -# Control whether or not this Gitblit instance can receive federation proposals -# from another Gitblit instance. Registering a federated Gitblit is a manual -# process. Proposals help to simplify that process by allowing a remote Gitblit -# instance to send your Gitblit instance the federation pull data. -# -# SINCE 0.6.0 -federation.allowProposals = false - -# The destination folder for cached federation proposals. -# Use forward slashes even on Windows!! -# -# SINCE 0.6.0 -federation.proposalsFolder = proposals - -# The default pull frequency if frequency is unspecified on a registration -# -# SINCE 0.6.0 -federation.defaultFrequency = 60 mins - -# Federation Sets are named groups of repositories. The Federation Sets are -# available for selection in the repository settings page. You can assign a -# repository to one or more sets and then distribute the token for the set. -# This allows you to grant federation pull access to a subset of your available -# repositories. Tokens for federation sets only grant repository pull access. -# -# SPACE-DELIMITED -# CASE-SENSITIVE -# SINCE 0.6.0 -federation.sets = - -# Federation pull registrations -# Registrations are read once, at startup. -# -# RESTART REQUIRED -# -# frequency: -# The shortest frequency allowed is every 5 minutes -# Decimal frequency values are cast to integers -# Frequency values may be specified in mins, hours, or days -# Values that can not be parsed or are unspecified default to *federation.defaultFrequency* -# -# folder: -# if unspecified, the folder is *git.repositoriesFolder* -# if specified, the folder is relative to *git.repositoriesFolder* -# -# bare: -# if true, each repository will be created as a *bare* repository and will not -# have a working directory. -# -# if false, each repository will be created as a normal repository suitable -# for local work. -# -# mirror: -# if true, each repository HEAD is reset to *origin/master* after each pull. -# The repository will be flagged *isFrozen* after the initial clone. -# -# if false, each repository HEAD will point to the FETCH_HEAD of the initial -# clone from the origin until pushed to or otherwise manipulated. -# -# mergeAccounts: -# if true, remote accounts and their permissions are merged into your -# users.properties file -# -# notifyOnError: -# if true and the mail configuration is properly set, administrators will be -# notified by email of pull failures -# -# include and exclude: -# Space-delimited list of repositories to include or exclude from pull -# may be * wildcard to include or exclude all -# may use fuzzy match (e.g. org.eclipse.*) - -# -# (Nearly) Perfect Mirror example -# - -#federation.example1.url = https://go.gitblit.com -#federation.example1.token = 6f3b8a24bf970f17289b234284c94f43eb42f0e4 -#federation.example1.frequency = 120 mins -#federation.example1.folder = -#federation.example1.bare = true -#federation.example1.mirror = true -#federation.example1.mergeAccounts = true - -# -# Advanced Realm Settings -# - -# URL of the LDAP server. -# To use encrypted transport, use either ldaps:// URL for SSL or ldap+tls:// to -# send StartTLS command. -# -# SINCE 1.0.0 -realm.ldap.server = ldap://localhost - -# Login username for LDAP searches. -# If this value is unspecified, anonymous LDAP login will be used. -# -# e.g. mydomain\\username -# -# SINCE 1.0.0 -realm.ldap.username = cn=Directory Manager - -# Login password for LDAP searches. -# -# SINCE 1.0.0 -realm.ldap.password = password - -# The LdapUserService must be backed by another user service for standard user -# and team management. -# default: users.conf -# -# SINCE 1.0.0 -# RESTART REQUIRED -realm.ldap.backingUserService = users.conf - -# Delegate team membership control to LDAP. -# -# If true, team user memberships will be specified by LDAP groups. This will -# disable team selection in Edit User and user selection in Edit Team. -# -# If false, LDAP will only be used for authentication and Gitblit will maintain -# team memberships with the *realm.ldap.backingUserService*. -# -# SINCE 1.0.0 -realm.ldap.maintainTeams = false - -# Root node for all LDAP users -# -# This is the root node from which subtree user searches will begin. -# If blank, Gitblit will search ALL nodes. -# -# SINCE 1.0.0 -realm.ldap.accountBase = OU=Users,OU=UserControl,OU=MyOrganization,DC=MyDomain - -# Filter criteria for LDAP users -# -# Query pattern to use when searching for a user account. This may be any valid -# LDAP query expression, including the standard (&) and (|) operators. -# -# Variables may be injected via the ${variableName} syntax. -# Recognized variables are: -# ${username} - The text entered as the user name -# -# SINCE 1.0.0 -realm.ldap.accountPattern = (&(objectClass=person)(sAMAccountName=${username})) - -# Root node for all LDAP groups to be used as Gitblit Teams -# -# This is the root node from which subtree team searches will begin. -# If blank, Gitblit will search ALL nodes. -# -# SINCE 1.0.0 -realm.ldap.groupBase = OU=Groups,OU=UserControl,OU=MyOrganization,DC=MyDomain - -# Filter criteria for LDAP groups -# -# Query pattern to use when searching for a team. This may be any valid -# LDAP query expression, including the standard (&) and (|) operators. -# -# Variables may be injected via the ${variableName} syntax. -# Recognized variables are: -# ${username} - The text entered as the user name -# ${dn} - The Distinguished Name of the user logged in -# -# All attributes from the LDAP User record are available. For example, if a user -# has an attribute "fullName" set to "John", "(fn=${fullName})" will be -# translated to "(fn=John)". -# -# SINCE 1.0.0 -realm.ldap.groupMemberPattern = (&(objectClass=group)(member=${dn})) - -# LDAP users or groups that should be given administrator privileges. -# -# Teams are specified with a leading '@' character. Groups with spaces in the -# name can be entered as "@team name". -# -# e.g. realm.ldap.admins = john @git_admins "@git admins" -# -# SPACE-DELIMITED -# SINCE 1.0.0 -realm.ldap.admins = @Git_Admins - -# Attribute(s) on the USER record that indicate their display (or full) name. -# Leave blank for no mapping available in LDAP. -# -# This may be a single attribute, or a string of multiple attributes. Examples: -# displayName - Uses the attribute 'displayName' on the user record -# ${personalTitle}. ${givenName} ${surname} - Will concatenate the 3 -# attributes together, with a '.' after personalTitle -# -# SINCE 1.0.0 -realm.ldap.displayName = displayName - -# Attribute(s) on the USER record that indicate their email address. -# Leave blank for no mapping available in LDAP. -# -# This may be a single attribute, or a string of multiple attributes. Examples: -# email - Uses the attribute 'email' on the user record -# ${givenName}.${surname}@gitblit.com -Will concatenate the 2 attributes -# together with a '.' and '@' creating something like first.last@gitblit.com -# -# SINCE 1.0.0 -realm.ldap.email = email - -# Defines the cache period to be used when caching LDAP queries. This is currently -# only used for LDAP user synchronization. -# -# Must be of the form ' ' where is one of 'MILLISECONDS', 'SECONDS', 'MINUTES', 'HOURS', 'DAYS' -# default: 2 MINUTES -# -# RESTART REQUIRED -realm.ldap.ldapCachePeriod = 2 MINUTES - -# Defines whether to synchronize all LDAP users into the backing user service -# -# Valid values: true, false -# If left blank, false is assumed -realm.ldap.synchronizeUsers.enable = false - -# Defines whether to delete non-existent LDAP users from the backing user service -# during synchronization. depends on realm.ldap.synchronizeUsers.enable = true -# -# Valid values: true, false -# If left blank, true is assumed -realm.ldap.synchronizeUsers.removeDeleted = true - -# Attribute on the USER record that indicate their username to be used in gitblit -# when synchronizing users from LDAP -# if blank, Gitblit will use uid -# -# -realm.ldap.uid = uid - -# The RedmineUserService must be backed by another user service for standard user -# and team management. -# default: users.conf -# -# RESTART REQUIRED -realm.redmine.backingUserService = users.conf - -# URL of the Redmine. -realm.redmine.url = http://example.com/redmine - -# -# Server Settings -# - -# The temporary folder to decompress the embedded gitblit webapp. -# -# SINCE 0.5.0 -# RESTART REQUIRED -server.tempFolder = temp - -# Use Jetty NIO connectors. If false, Jetty Socket connectors will be used. -# -# SINCE 0.5.0 -# RESTART REQUIRED -server.useNio = true - -# Context path for the GO application. You might want to change the context -# path if running Gitblit behind a proxy layer such as mod_proxy. -# -# SINCE 0.7.0 -# RESTART REQUIRED -server.contextPath = / - -# Standard http port to serve. <= 0 disables this connector. -# On Unix/Linux systems, ports < 1024 require root permissions. -# Recommended value: 80 or 8080 -# -# SINCE 0.5.0 -# RESTART REQUIRED -server.httpPort = 0 - -# Secure/SSL https port to serve. <= 0 disables this connector. -# On Unix/Linux systems, ports < 1024 require root permissions. -# Recommended value: 443 or 8443 -# -# SINCE 0.5.0 -# RESTART REQUIRED -server.httpsPort = 8443 - -# Port for serving an Apache JServ Protocol (AJP) 1.3 connector for integrating -# Gitblit GO into an Apache HTTP server setup. <= 0 disables this connector. -# Recommended value: 8009 -# -# SINCE 0.9.0 -# RESTART REQUIRED -server.ajpPort = 0 - -# Specify the interface for Jetty to bind the standard connector. -# You may specify an ip or an empty value to bind to all interfaces. -# Specifying localhost will result in Gitblit ONLY listening to requests to -# localhost. -# -# SINCE 0.5.0 -# RESTART REQUIRED -server.httpBindInterface = localhost - -# Specify the interface for Jetty to bind the secure connector. -# You may specify an ip or an empty value to bind to all interfaces. -# Specifying localhost will result in Gitblit ONLY listening to requests to -# localhost. -# -# SINCE 0.5.0 -# RESTART REQUIRED -server.httpsBindInterface = localhost - -# Specify the interface for Jetty to bind the AJP connector. -# You may specify an ip or an empty value to bind to all interfaces. -# Specifying localhost will result in Gitblit ONLY listening to requests to -# localhost. -# -# SINCE 0.9.0 -# RESTART REQUIRED -server.ajpBindInterface = localhost - -# Password for SSL keystore. -# Keystore password and certificate password must match. -# This is provided for convenience, its probably more secure to set this value -# using the --storePassword command line parameter. -# -# If you are using the official JRE or JDK from Oracle you may not have the -# JCE Unlimited Strength Jurisdiction Policy files bundled with your JVM. Because -# of this, your store/key password can not exceed 7 characters. If you require -# longer passwords you may need to install the JCE Unlimited Strength Jurisdiction -# Policy files from Oracle. -# -# http://www.oracle.com/technetwork/java/javase/downloads/index.html -# -# Gitblit and the Gitblit Certificate Authority will both indicate if Unlimited -# Strength encryption is available. -# -# SINCE 0.5.0 -# RESTART REQUIRED -server.storePassword = gitblit - -# If serving over https (recommended) you might consider requiring clients to -# authenticate with ssl certificates. If enabled, only https clients with the -# a valid client certificate will be able to access Gitblit. -# -# If disabled, client certificate authentication is optional and will be tried -# first before falling-back to form authentication or basic authentication. -# -# Requiring client certificates to access any of Gitblit may be too extreme, -# consider this carefully. -# -# SINCE 1.2.0 -# RESTART REQUIRED -server.requireClientCertificates = false - -# Port for shutdown monitor to listen on. -# -# SINCE 0.5.0 -# RESTART REQUIRED -server.shutdownPort = 8081 +# +# Git Servlet Settings +# + +# Base folder for repositories. +# This folder may contain bare and non-bare repositories but Gitblit will only +# allow you to push to bare repositories. +# Use forward slashes even on Windows!! +# e.g. c:/gitrepos +# +# SINCE 0.5.0 +# RESTART REQUIRED +git.repositoriesFolder = git + +# Build the available repository list at startup and cache this list for reuse. +# This reduces disk io when presenting the repositories page, responding to rpcs, +# etc, but it means that Gitblit will not automatically identify repositories +# added or deleted by external tools. +# +# For this case you can use curl, wget, etc to issue an rpc request to clear the +# cache (e.g. https://localhost/rpc?req=CLEAR_REPOSITORY_CACHE) +# +# SINCE 1.1.0 +git.cacheRepositoryList = true + +# Search the repositories folder subfolders for other repositories. +# Repositories MAY NOT be nested (i.e. one repository within another) +# but they may be grouped together in subfolders. +# e.g. c:/gitrepos/libraries/mylibrary.git +# c:/gitrepos/libraries/myotherlibrary.git +# +# SINCE 0.5.0 +git.searchRepositoriesSubfolders = true + +# Maximum number of folders to recurse into when searching for repositories. +# The default value, -1, disables depth limits. +# +# SINCE 1.1.0 +git.searchRecursionDepth = -1 + +# List of regex exclusion patterns to match against folders found in +# *git.repositoriesFolder*. +# Use forward slashes even on Windows!! +# e.g. test/jgit\.git +# +# SPACE-DELIMITED +# CASE-SENSITIVE +# SINCE 1.1.0 +git.searchExclusions = + +# List of regex url patterns for extracting a repository name when locating +# submodules. +# e.g. git.submoduleUrlPatterns = .*?://github.com/(.*) will extract +# *gitblit/gitblit.git* from *git://github.com/gitblit/gitblit.git* +# If no matches are found then the submodule repository name is assumed to be +# whatever trails the last / character. (e.g. gitblit.git). +# +# SPACE-DELIMITED +# CASE-SENSITIVE +# SINCE 1.1.0 +git.submoduleUrlPatterns = .*?://github.com/(.*) + +# Allow push/pull over http/https with JGit servlet. +# If you do NOT want to allow Git clients to clone/push to Gitblit set this +# to false. You might want to do this if you are only using ssh:// or git://. +# If you set this false, consider changing the *web.otherUrls* setting to +# indicate your clone/push urls. +# +# SINCE 0.5.0 +git.enableGitServlet = true + +# If you want to restrict all git servlet access to those with valid X509 client +# certificates then set this value to true. +# +# SINCE 1.2.0 +git.requiresClientCertificate = false + +# Enforce date checks on client certificates to ensure that they are not being +# used prematurely and that they have not expired. +# +# SINCE 1.2.0 +git.enforceCertificateValidity = true + +# List of OIDs to extract from a client certificate DN to map a certificate to +# an account username. +# +# e.g. git.certificateUsernameOIDs = CN +# e.g. git.certificateUsernameOIDs = FirstName LastName +# +# SPACE-DELIMITED +# SINCE 1.2.0 +git.certificateUsernameOIDs = CN + +# Only serve/display bare repositories. +# If there are non-bare repositories in git.repositoriesFolder and this setting +# is true, they will be excluded from the ui. +# +# SINCE 0.9.0 +git.onlyAccessBareRepositories = false + +# Allow an authenticated user to create a destination repository on a push if +# the repository does not already exist. +# +# Administrator accounts can create a repository in any project. +# These repositories are created with the default access restriction and authorization +# control values. The pushing account is set as the owner. +# +# Non-administrator accounts with the CREATE role may create personal repositories. +# These repositories are created as VIEW restricted for NAMED users. +# The pushing account is set as the owner. +# +# SINCE 1.2.0 +git.allowCreateOnPush = true + +# The default access restriction for new repositories. +# Valid values are NONE, PUSH, CLONE, VIEW +# NONE = anonymous view, clone, & push +# PUSH = anonymous view & clone and authenticated push +# CLONE = anonymous view, authenticated clone & push +# VIEW = authenticated view, clone, & push +# +# SINCE 1.0.0 +git.defaultAccessRestriction = NONE + +# The default authorization control for new repositories. +# Valid values are AUTHENTICATED and NAMED +# AUTHENTICATED = any authenticated user is granted restricted access +# NAMED = only named users/teams are granted restricted access +# +# SINCE 1.1.0 +git.defaultAuthorizationControl = NAMED + +# Enable JGit-based garbage collection. (!!EXPERIMENTAL!!) +# +# USE AT YOUR OWN RISK! +# +# If enabled, the garbage collection executor scans all repositories once a day +# at the hour of your choosing. The GC executor will take each repository "offline", +# one-at-a-time, to check if the repository satisfies it's GC trigger requirements. +# +# While the repository is offline it will be inaccessible from the web UI or from +# any of the other services (git, rpc, rss, etc). +# +# Gitblit's GC Executor MAY NOT PLAY NICE with the other Git kids on the block, +# especially on Windows systems, so if you are using other tools please coordinate +# their usage with your GC Executor schedule or do not use this feature. +# +# The GC algorithm complex and the JGit team advises caution when using their +# young implementation of GC. +# +# http://wiki.eclipse.org/EGit/New_and_Noteworthy/2.1#Garbage_Collector_and_Repository_Storage_Statistics +# +# EXPERIMENTAL +# SINCE 1.2.0 +# RESTART REQUIRED +git.enableGarbageCollection = false + +# Hour of the day for the GC Executor to scan repositories. +# This value is in 24-hour time. +# +# SINCE 1.2.0 +git.garbageCollectionHour = 0 + +# The default minimum total filesize of loose objects to trigger early garbage +# collection. +# +# You may specify a custom threshold for a repository in the repository's settings. +# Common unit suffixes of k, m, or g are supported. +# +# SINCE 1.2.0 +git.defaultGarbageCollectionThreshold = 500k + +# The default period, in days, between GCs for a repository. If the total filesize +# of the loose object exceeds *git.garbageCollectionThreshold* or the repository's +# custom threshold, this period will be short-circuited. +# +# e.g. if a repository collects 100KB of loose objects every day with a 500KB +# threshold and a period of 7 days, it will take 5 days for the loose objects to +# be collected, packed, and pruned. +# +# OR +# +# if a repository collects 10KB of loose objects every day with a 500KB threshold +# and a period of 7 days, it will take the full 7 days for the loose objects to be +# collected, packed, and pruned. +# +# You may specify a custom period for a repository in the repository's settings. +# +# The minimum value is 1 day since the GC Executor only runs once a day. +# +# SINCE 1.2.0 +git.defaultGarbageCollectionPeriod = 7 + +# Number of bytes of a pack file to load into memory in a single read operation. +# This is the "page size" of the JGit buffer cache, used for all pack access +# operations. All disk IO occurs as single window reads. Setting this too large +# may cause the process to load more data than is required; setting this too small +# may increase the frequency of read() system calls. +# +# Default on JGit is 8 KiB on all platforms. +# +# Common unit suffixes of k, m, or g are supported. +# Documentation courtesy of the Gerrit project. +# +# SINCE 1.0.0 +# RESTART REQUIRED +git.packedGitWindowSize = 8k + +# Maximum number of bytes to load and cache in memory from pack files. If JGit +# needs to access more than this many bytes it will unload less frequently used +# windows to reclaim memory space within the process. As this buffer must be shared +# with the rest of the JVM heap, it should be a fraction of the total memory available. +# +# The JGit team recommends setting this value larger than the size of your biggest +# repository. This ensures you can serve most requests from memory. +# +# Default on JGit is 10 MiB on all platforms. +# +# Common unit suffixes of k, m, or g are supported. +# Documentation courtesy of the Gerrit project. +# +# SINCE 1.0.0 +# RESTART REQUIRED +git.packedGitLimit = 10m + +# Maximum number of bytes to reserve for caching base objects that multiple deltafied +# objects reference. By storing the entire decompressed base object in a cache Git +# is able to avoid unpacking and decompressing frequently used base objects multiple times. +# +# Default on JGit is 10 MiB on all platforms. You probably do not need to adjust +# this value. +# +# Common unit suffixes of k, m, or g are supported. +# Documentation courtesy of the Gerrit project. +# +# SINCE 1.0.0 +# RESTART REQUIRED +git.deltaBaseCacheLimit = 10m + +# Maximum number of pack files to have open at once. A pack file must be opened +# in order for any of its data to be available in a cached window. +# +# If you increase this to a larger setting you may need to also adjust the ulimit +# on file descriptors for the host JVM, as Gitblit needs additional file descriptors +# available for network sockets and other repository data manipulation. +# +# Default on JGit is 128 file descriptors on all platforms. +# Documentation courtesy of the Gerrit project. +# +# SINCE 1.0.0 +# RESTART REQUIRED +git.packedGitOpenFiles = 128 + +# Largest object size, in bytes, that JGit will allocate as a contiguous byte +# array. Any file revision larger than this threshold will have to be streamed, +# typically requiring the use of temporary files under $GIT_DIR/objects to implement +# psuedo-random access during delta decompression. +# +# Servers with very high traffic should set this to be larger than the size of +# their common big files. For example a server managing the Android platform +# typically has to deal with ~10-12 MiB XML files, so 15 m would be a reasonable +# setting in that environment. Setting this too high may cause the JVM to run out +# of heap space when handling very big binary files, such as device firmware or +# CD-ROM ISO images. Make sure to adjust your JVM heap accordingly. +# +# Default is 50 MiB on all platforms. +# +# Common unit suffixes of k, m, or g are supported. +# Documentation courtesy of the Gerrit project. +# +# SINCE 1.0.0 +# RESTART REQUIRED +git.streamFileThreshold = 50m + +# When true, JGit will use mmap() rather than malloc()+read() to load data from +# pack files. The use of mmap can be problematic on some JVMs as the garbage +# collector must deduce that a memory mapped segment is no longer in use before +# a call to munmap() can be made by the JVM native code. +# +# In server applications (such as Gitblit) that need to access many pack files, +# setting this to true risks artificially running out of virtual address space, +# as the garbage collector cannot reclaim unused mapped spaces fast enough. +# +# Default on JGit is false. Although potentially slower, it yields much more +# predictable behavior. +# Documentation courtesy of the Gerrit project. +# +# SINCE 1.0.0 +# RESTART REQUIRED +git.packedGitMmap = false + +# +# Groovy Integration +# + +# Location of Groovy scripts to use for Pre and Post receive hooks. +# Use forward slashes even on Windows!! +# e.g. c:/groovy +# +# RESTART REQUIRED +# SINCE 0.8.0 +groovy.scriptsFolder = groovy + +# Specify the directory Grape uses for downloading libraries. +# http://groovy.codehaus.org/Grape +# +# RESTART REQUIRED +# SINCE 1.0.0 +groovy.grapeFolder = groovy/grape + +# Scripts to execute on Pre-Receive. +# +# These scripts execute after an incoming push has been parsed and validated +# but BEFORE the changes are applied to the repository. You might reject a +# push in this script based on the repository and branch the push is attempting +# to change. +# +# Script names are case-sensitive on case-sensitive file systems. You may omit +# the traditional ".groovy" from this list if your file extension is ".groovy" +# +# NOTE: +# These scripts are only executed when pushing to *Gitblit*, not to other Git +# tooling you may be using. Also note that these scripts are shared between +# repositories. These are NOT repository-specific scripts! Within the script +# you may customize the control-flow for a specific repository by checking the +# *repository* variable. +# +# SPACE-DELIMITED +# CASE-SENSITIVE +# SINCE 0.8.0 +groovy.preReceiveScripts = + +# Scripts to execute on Post-Receive. +# +# These scripts execute AFTER an incoming push has been applied to a repository. +# You might trigger a continuous-integration build here or send a notification. +# +# Script names are case-sensitive on case-sensitive file systems. You may omit +# the traditional ".groovy" from this list if your file extension is ".groovy" +# +# NOTE: +# These scripts are only executed when pushing to *Gitblit*, not to other Git +# tooling you may be using. Also note that these scripts are shared between +# repositories. These are NOT repository-specific scripts! Within the script +# you may customize the control-flow for a specific repository by checking the +# *repository* variable. +# +# SPACE-DELIMITED +# CASE-SENSITIVE +# SINCE 0.8.0 +groovy.postReceiveScripts = + +# Repository custom fields for Groovy Hook mechanism +# +# List of key=label pairs of custom fields to prompt for in the Edit Repository +# page. These keys are stored in the repository's git config file in the +# section [gitblit "customFields"]. Key names are alphanumeric only. These +# fields are intended to be used for the Groovy hook mechanism where a script +# can adjust it's execution based on the custom fields stored in the repository +# config. +# +# e.g. "commitMsgRegex=Commit Message Regular Expression" anotherProperty=Another +# +# SPACE-DELIMITED +# SINCE 1.0.0 +groovy.customFields = + +# +# Authentication Settings +# + +# Require authentication to see everything but the admin pages +# +# SINCE 0.5.0 +# RESTART REQUIRED +web.authenticateViewPages = false + +# Require admin authentication for the admin functions and pages +# +# SINCE 0.5.0 +# RESTART REQUIRED +web.authenticateAdminPages = true + +# Allow Gitblit to store a cookie in the user's browser for automatic +# authentication. The cookie is generated by the user service. +# +# SINCE 0.5.0 +web.allowCookieAuthentication = true + +# Config file for storing project metadata +# +# SINCE 1.2.0 +web.projectsFile = projects.conf + +# Either the full path to a user config file (users.conf) +# OR the full path to a simple user properties file (users.properties) +# OR a fully qualified class name that implements the IUserService interface. +# +# Alternative user services: +# com.gitblit.LdapUserService +# com.gitblit.RedmineUserService +# +# Any custom user service implementation must have a public default constructor. +# +# SINCE 0.5.0 +# RESTART REQUIRED +realm.userService = users.conf + +# How to store passwords. +# Valid values are plain, md5, or combined-md5. md5 is the hash of password. +# combined-md5 is the hash of username.toLowerCase()+password. +# Default is md5. +# +# SINCE 0.5.0 +realm.passwordStorage = md5 + +# Minimum valid length for a plain text password. +# Default value is 5. Absolute minimum is 4. +# +# SINCE 0.5.0 +realm.minPasswordLength = 5 + +# +# Gitblit Web Settings +# +# If blank Gitblit is displayed. +# +# SINCE 0.5.0 +web.siteName = + +# If *web.authenticateAdminPages*=true, users with "admin" role can create +# repositories, create users, and edit repository metadata. +# +# If *web.authenticateAdminPages*=false, any user can execute the aforementioned +# functions. +# +# SINCE 0.5.0 +web.allowAdministration = true + +# Allows rpc clients to list repositories and possibly manage or administer the +# Gitblit server, if the authenticated account has administrator permissions. +# See *web.enableRpcManagement* and *web.enableRpcAdministration*. +# +# SINCE 0.7.0 +web.enableRpcServlet = true + +# Allows rpc clients to manage repositories and users of the Gitblit instance, +# if the authenticated account has administrator permissions. +# Requires *web.enableRpcServlet=true*. +# +# SINCE 0.7.0 +web.enableRpcManagement = false + +# Allows rpc clients to control the server settings and monitor the health of this +# this Gitblit instance, if the authenticated account has administrator permissions. +# Requires *web.enableRpcServlet=true* and *web.enableRpcManagement*. +# +# SINCE 0.7.0 +web.enableRpcAdministration = false + +# Full path to a configurable robots.txt file. With this file you can control +# what parts of your Gitblit server respectable robots are allowed to traverse. +# http://googlewebmastercentral.blogspot.com/2008/06/improving-on-robots-exclusion-protocol.html +# +# SINCE 1.0.0 +web.robots.txt = + +# If true, the web ui layout will respond and adapt to the browser's dimensions. +# if false, the web ui will use a 940px fixed-width layout. +# http://twitter.github.com/bootstrap/scaffolding.html#responsive +# +# SINCE 1.0.0 +web.useResponsiveLayout = true + +# Allow Gravatar images to be displayed in Gitblit pages. +# +# SINCE 0.8.0 +web.allowGravatar = true + +# Allow dynamic zip downloads. +# +# SINCE 0.5.0 +web.allowZipDownloads = true + +# If *web.allowZipDownloads=true* the following formats will be displayed for +# download compressed archive links: +# +# zip = standard .zip +# tar = standard tar format (preserves *nix permissions and symlinks) +# gz = gz-compressed tar +# xz = xz-compressed tar +# bzip2 = bzip2-compressed tar +# +# SPACE-DELIMITED +# SINCE 1.2.0 +web.compressedDownloads = zip gz + +# Allow optional Lucene integration. Lucene indexing is an opt-in feature. +# A repository may specify branches to index with Lucene instead of using Git +# commit traversal. There are scenarios where you may want to completely disable +# Lucene indexing despite a repository specifying indexed branches. One such +# scenario is on a resource-constrained federated Gitblit mirror. +# +# SINCE 0.9.0 +web.allowLuceneIndexing = true + +# Controls the length of shortened commit hash ids +# +# SINCE 1.2.0 +web.shortCommitIdLength = 6 + +# Use Clippy (Flash solution) to provide a copy-to-clipboard button. +# If false, a button with a more primitive JavaScript-based prompt box will +# offer a 3-step (click, ctrl+c, enter) copy-to-clipboard alternative. +# +# SINCE 0.8.0 +web.allowFlashCopyToClipboard = true + +# Default number of entries to include in RSS Syndication links +# +# SINCE 0.5.0 +web.syndicationEntries = 25 + +# Show the size of each repository on the repositories page. +# This requires recursive traversal of each repository folder. This may be +# non-performant on some operating systems and/or filesystems. +# +# SINCE 0.5.2 +web.showRepositorySizes = true + +# List of custom regex expressions that can be displayed in the Filters menu +# of the Repositories and Activity pages. Keep them very simple because you +# are likely to run into encoding issues if they are too complex. +# +# Use !!! to separate the filters +# +# SINCE 0.8.0 +web.customFilters = + +# Show federation registrations (without token) and the current pull status +# to non-administrator users. +# +# SINCE 0.6.0 +web.showFederationRegistrations = false + +# This is the message displayed when *web.authenticateViewPages=true*. +# This can point to a file with Markdown content. +# Specifying "gitblit" uses the internal login message. +# +# SINCE 0.7.0 +web.loginMessage = gitblit + +# This is the message displayed above the repositories table. +# This can point to a file with Markdown content. +# Specifying "gitblit" uses the internal welcome message. +# +# SINCE 0.5.0 +web.repositoriesMessage = gitblit + +# Ordered list of charsets/encodings to use when trying to display a blob. +# If empty, UTF-8 and ISO-8859-1 are used. The server's default charset +# is always appended to the encoding list. If all encodings fail to cleanly +# decode the blob content, UTF-8 will be used with the standard malformed +# input/unmappable character replacement strings. +# +# SPACE-DELIMITED +# SINCE 1.0.0 +web.blobEncodings = UTF-8 ISO-8859-1 + +# Manually set the default timezone to be used by Gitblit for display in the +# web ui. This value is independent of the JVM timezone. Specifying a blank +# value will default to the JVM timezone. +# e.g. America/New_York, US/Pacific, UTC, Europe/Berlin +# +# SINCE 0.9.0 +# RESTART REQUIRED +web.timezone = + +# Use the client timezone when formatting dates. +# This uses AJAX to determine the browser's timezone and may require more +# server overhead because a Wicket session is created. All Gitblit pages +# attempt to be stateless, if possible. +# +# SINCE 0.5.0 +# RESTART REQUIRED +web.useClientTimezone = false + +# Time format +# +# +# SINCE 0.8.0 +web.timeFormat = HH:mm + +# Short date format +# +# +# SINCE 0.5.0 +web.datestampShortFormat = yyyy-MM-dd + +# Long date format +# +# SINCE 0.8.0 +web.datestampLongFormat = EEEE, MMMM d, yyyy + +# Long timestamp format +# +# +# SINCE 0.5.0 +web.datetimestampLongFormat = EEEE, MMMM d, yyyy HH:mm Z + +# Mount URL parameters +# This setting controls if pretty or parameter URLs are used. +# i.e. +# if true: +# http://localhost/commit/myrepo/abcdef +# if false: +# http://localhost/commit/?r=myrepo&h=abcdef +# +# SINCE 0.5.0 +# RESTART REQUIRED +web.mountParameters = true + +# Some servlet containers (e.g. Tomcat >= 6.0.10) disallow '/' (%2F) encoding +# in URLs as a security precaution for proxies. This setting tells Gitblit +# to preemptively replace '/' with '*' or '!' for url string parameters. +# +# +# +# Add *-Dorg.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH=true* to your +# *CATALINA_OPTS* or to your JVM launch parameters +# +# SINCE 0.5.2 +web.forwardSlashCharacter = / + +# Show other URLs on the summary page for accessing your git repositories +# Use spaces to separate urls. {0} is the token for the repository name. +# e.g. +# web.otherUrls = ssh://localhost/git/{0} git://localhost/git/{0} +# +# SPACE-DELIMITED +# SINCE 0.5.0 +web.otherUrls = + +# Choose how to present the repositories list. +# grouped = group nested/subfolder repositories together (no sorting) +# flat = flat list of repositories (sorting allowed) +# +# SINCE 0.5.0 +web.repositoryListType = grouped + +# If using a grouped repository list and there are repositories at the +# root level of your repositories folder, you may specify the displayed +# group name with this setting. This value is only used for web presentation. +# +# SINCE 0.5.0 +web.repositoryRootGroupName = main + +# Display the repository swatch color next to the repository name link in the +# repositories list. +# +# SINCE 0.8.0 +web.repositoryListSwatches = true + +# Choose the diff presentation style: gitblt, gitweb, or plain +# +# SINCE 0.5.0 +web.diffStyle = gitblit + +# Control if email addresses are shown in web ui +# +# SINCE 0.5.0 +web.showEmailAddresses = true + +# Shows a combobox in the page links header with commit, committer, and author +# search selection. Default search is commit. +# +# SINCE 0.5.0 +web.showSearchTypeSelection = false + +# Generates a line graph of repository activity over time on the Summary page. +# This uses the Google Charts API. +# +# SINCE 0.5.0 +web.generateActivityGraph = true + +# The number of days to show on the activity page. +# Value must exceed 0 else default of 14 is used +# +# SINCE 0.8.0 +web.activityDuration = 14 + +# The number of commits to display on the summary page +# Value must exceed 0 else default of 20 is used +# +# SINCE 0.5.0 +web.summaryCommitCount = 16 + +# The number of tags/branches to display on the summary page. +# -1 = all tags/branches +# 0 = hide tags/branches +# N = N tags/branches +# +# SINCE 0.5.0 +web.summaryRefsCount = 5 + +# The number of items to show on a page before showing the first, prev, next +# pagination links. A default if 50 is used for any invalid value. +# +# SINCE 0.5.0 +web.itemsPerPage = 50 + +# Registered file extensions to ignore during Lucene indexing +# +# SPACE-DELIMITED +# SINCE 0.9.0 +web.luceneIgnoreExtensions = 7z arc arj bin bmp dll doc docx exe gif gz jar jpg lib lzh odg odf odt pdf ppt png so swf xcf xls xlsx zip + +# Registered extensions for google-code-prettify +# +# SPACE-DELIMITED +# SINCE 0.5.0 +web.prettyPrintExtensions = c cpp cs css frm groovy htm html java js php pl prefs properties py rb scala sh sql xml vb + +# Registered extensions for markdown transformation +# +# SPACE-DELIMITED +# CASE-SENSITIVE +# SINCE 0.5.0 +web.markdownExtensions = md mkd markdown MD MKD + +# Image extensions +# +# SPACE-DELIMITED +# SINCE 0.5.0 +web.imageExtensions = bmp jpg gif png + +# Registered extensions for binary blobs +# +# SPACE-DELIMITED +# SINCE 0.5.0 +web.binaryExtensions = jar pdf tar.gz zip + +# Aggressive heap management will run the garbage collector on every generated +# page. This slows down page generation a little but improves heap consumption. +# +# SINCE 0.5.0 +web.aggressiveHeapManagement = false + +# Run the webapp in debug mode +# +# SINCE 0.5.0 +# RESTART REQUIRED +web.debugMode = false + +# Enable/disable global regex substitutions (i.e. shared across repositories) +# +# SINCE 0.5.0 +regex.global = true + +# Example global regex substitutions +# Use !!! to separate the search pattern and the replace pattern +# searchpattern!!!replacepattern +# SINCE 0.5.0 +regex.global.bug = \\b(Bug:)(\\s*[#]?|-){0,1}(\\d+)\\b!!!Bug-Id: $3 +# SINCE 0.5.0 +regex.global.changeid = \\b(Change-Id:\\s*)([A-Za-z0-9]*)\\b!!!Change-Id: $2 + +# Example per-repository regex substitutions overrides global +# SINCE 0.5.0 +regex.myrepository.bug = \\b(Bug:)(\\s*[#]?|-){0,1}(\\d+)\\b!!!Bug-Id: $3 + +# +# Mail Settings +# SINCE 0.6.0 +# +# Mail settings are used to notify administrators of received federation proposals +# + +# ip or hostname of smtp server +# +# SINCE 0.6.0 +mail.server = + +# port to use for smtp requests +# +# SINCE 0.6.0 +mail.port = 25 + +# debug the mail executor +# +# SINCE 0.6.0 +mail.debug = false + +# if your smtp server requires authentication, supply the credentials here +# +# SINCE 0.6.0 +mail.username = +# SINCE 0.6.0 +mail.password = + +# from address for generated emails +# +# SINCE 0.6.0 +mail.fromAddress = + +# List of email addresses for the Gitblit administrators +# +# SPACE-DELIMITED +# SINCE 0.6.0 +mail.adminAddresses = + +# List of email addresses for sending push email notifications. +# +# This key currently requires use of the sendemail.groovy hook script. +# If you set sendemail.groovy in *groovy.postReceiveScripts* then email +# notifications for all repositories (regardless of access restrictions!) +# will be sent to these addresses. +# +# SPACE-DELIMITED +# SINCE 0.8.0 +mail.mailingLists = + +# +# Federation Settings +# SINCE 0.6.0 +# +# A Gitblit federation is a way to backup one Gitblit instance to another. +# +# *git.enableGitServlet* must be true to use this feature. + +# Your federation name is used for federation status acknowledgments. If it is +# unset, and you elect to send a status acknowledgment, your Gitblit instance +# will be identified by its hostname, if available, else your internal ip address. +# The source Gitblit instance will also append your external IP address to your +# identification to differentiate multiple pulling systems behind a single proxy. +# +# SINCE 0.6.0 +federation.name = + +# Specify the passphrase of this Gitblit instance. +# +# An unspecified (empty) passphrase disables processing federation requests. +# +# This value can be anything you want: an integer, a sentence, an haiku, etc. +# Keep the value simple, though, to avoid Java properties file encoding issues. +# +# Changing your passphrase will break any registrations you have established with other +# Gitblit instances. +# +# CASE-SENSITIVE +# SINCE 0.6.0 +# RESTART REQUIRED *(only to enable or disable federation)* +federation.passphrase = + +# Control whether or not this Gitblit instance can receive federation proposals +# from another Gitblit instance. Registering a federated Gitblit is a manual +# process. Proposals help to simplify that process by allowing a remote Gitblit +# instance to send your Gitblit instance the federation pull data. +# +# SINCE 0.6.0 +federation.allowProposals = false + +# The destination folder for cached federation proposals. +# Use forward slashes even on Windows!! +# +# SINCE 0.6.0 +federation.proposalsFolder = proposals + +# The default pull frequency if frequency is unspecified on a registration +# +# SINCE 0.6.0 +federation.defaultFrequency = 60 mins + +# Federation Sets are named groups of repositories. The Federation Sets are +# available for selection in the repository settings page. You can assign a +# repository to one or more sets and then distribute the token for the set. +# This allows you to grant federation pull access to a subset of your available +# repositories. Tokens for federation sets only grant repository pull access. +# +# SPACE-DELIMITED +# CASE-SENSITIVE +# SINCE 0.6.0 +federation.sets = + +# Federation pull registrations +# Registrations are read once, at startup. +# +# RESTART REQUIRED +# +# frequency: +# The shortest frequency allowed is every 5 minutes +# Decimal frequency values are cast to integers +# Frequency values may be specified in mins, hours, or days +# Values that can not be parsed or are unspecified default to *federation.defaultFrequency* +# +# folder: +# if unspecified, the folder is *git.repositoriesFolder* +# if specified, the folder is relative to *git.repositoriesFolder* +# +# bare: +# if true, each repository will be created as a *bare* repository and will not +# have a working directory. +# +# if false, each repository will be created as a normal repository suitable +# for local work. +# +# mirror: +# if true, each repository HEAD is reset to *origin/master* after each pull. +# The repository will be flagged *isFrozen* after the initial clone. +# +# if false, each repository HEAD will point to the FETCH_HEAD of the initial +# clone from the origin until pushed to or otherwise manipulated. +# +# mergeAccounts: +# if true, remote accounts and their permissions are merged into your +# users.properties file +# +# notifyOnError: +# if true and the mail configuration is properly set, administrators will be +# notified by email of pull failures +# +# include and exclude: +# Space-delimited list of repositories to include or exclude from pull +# may be * wildcard to include or exclude all +# may use fuzzy match (e.g. org.eclipse.*) + +# +# (Nearly) Perfect Mirror example +# + +#federation.example1.url = https://go.gitblit.com +#federation.example1.token = 6f3b8a24bf970f17289b234284c94f43eb42f0e4 +#federation.example1.frequency = 120 mins +#federation.example1.folder = +#federation.example1.bare = true +#federation.example1.mirror = true +#federation.example1.mergeAccounts = true + +# +# Advanced Realm Settings +# + +# URL of the LDAP server. +# To use encrypted transport, use either ldaps:// URL for SSL or ldap+tls:// to +# send StartTLS command. +# +# SINCE 1.0.0 +realm.ldap.server = ldap://localhost + +# Login username for LDAP searches. +# If this value is unspecified, anonymous LDAP login will be used. +# +# e.g. mydomain\\username +# +# SINCE 1.0.0 +realm.ldap.username = cn=Directory Manager + +# Login password for LDAP searches. +# +# SINCE 1.0.0 +realm.ldap.password = password + +# The LdapUserService must be backed by another user service for standard user +# and team management. +# default: users.conf +# +# SINCE 1.0.0 +# RESTART REQUIRED +realm.ldap.backingUserService = users.conf + +# Delegate team membership control to LDAP. +# +# If true, team user memberships will be specified by LDAP groups. This will +# disable team selection in Edit User and user selection in Edit Team. +# +# If false, LDAP will only be used for authentication and Gitblit will maintain +# team memberships with the *realm.ldap.backingUserService*. +# +# SINCE 1.0.0 +realm.ldap.maintainTeams = false + +# Root node for all LDAP users +# +# This is the root node from which subtree user searches will begin. +# If blank, Gitblit will search ALL nodes. +# +# SINCE 1.0.0 +realm.ldap.accountBase = OU=Users,OU=UserControl,OU=MyOrganization,DC=MyDomain + +# Filter criteria for LDAP users +# +# Query pattern to use when searching for a user account. This may be any valid +# LDAP query expression, including the standard (&) and (|) operators. +# +# Variables may be injected via the ${variableName} syntax. +# Recognized variables are: +# ${username} - The text entered as the user name +# +# SINCE 1.0.0 +realm.ldap.accountPattern = (&(objectClass=person)(sAMAccountName=${username})) + +# Root node for all LDAP groups to be used as Gitblit Teams +# +# This is the root node from which subtree team searches will begin. +# If blank, Gitblit will search ALL nodes. +# +# SINCE 1.0.0 +realm.ldap.groupBase = OU=Groups,OU=UserControl,OU=MyOrganization,DC=MyDomain + +# Filter criteria for LDAP groups +# +# Query pattern to use when searching for a team. This may be any valid +# LDAP query expression, including the standard (&) and (|) operators. +# +# Variables may be injected via the ${variableName} syntax. +# Recognized variables are: +# ${username} - The text entered as the user name +# ${dn} - The Distinguished Name of the user logged in +# +# All attributes from the LDAP User record are available. For example, if a user +# has an attribute "fullName" set to "John", "(fn=${fullName})" will be +# translated to "(fn=John)". +# +# SINCE 1.0.0 +realm.ldap.groupMemberPattern = (&(objectClass=group)(member=${dn})) + +# LDAP users or groups that should be given administrator privileges. +# +# Teams are specified with a leading '@' character. Groups with spaces in the +# name can be entered as "@team name". +# +# e.g. realm.ldap.admins = john @git_admins "@git admins" +# +# SPACE-DELIMITED +# SINCE 1.0.0 +realm.ldap.admins = @Git_Admins + +# Attribute(s) on the USER record that indicate their display (or full) name. +# Leave blank for no mapping available in LDAP. +# +# This may be a single attribute, or a string of multiple attributes. Examples: +# displayName - Uses the attribute 'displayName' on the user record +# ${personalTitle}. ${givenName} ${surname} - Will concatenate the 3 +# attributes together, with a '.' after personalTitle +# +# SINCE 1.0.0 +realm.ldap.displayName = displayName + +# Attribute(s) on the USER record that indicate their email address. +# Leave blank for no mapping available in LDAP. +# +# This may be a single attribute, or a string of multiple attributes. Examples: +# email - Uses the attribute 'email' on the user record +# ${givenName}.${surname}@gitblit.com -Will concatenate the 2 attributes +# together with a '.' and '@' creating something like first.last@gitblit.com +# +# SINCE 1.0.0 +realm.ldap.email = email + +# Defines the cache period to be used when caching LDAP queries. This is currently +# only used for LDAP user synchronization. +# +# Must be of the form ' ' where is one of 'MILLISECONDS', 'SECONDS', 'MINUTES', 'HOURS', 'DAYS' +# default: 2 MINUTES +# +# RESTART REQUIRED +realm.ldap.ldapCachePeriod = 2 MINUTES + +# Defines whether to synchronize all LDAP users into the backing user service +# +# Valid values: true, false +# If left blank, false is assumed +realm.ldap.synchronizeUsers.enable = false + +# Defines whether to delete non-existent LDAP users from the backing user service +# during synchronization. depends on realm.ldap.synchronizeUsers.enable = true +# +# Valid values: true, false +# If left blank, true is assumed +realm.ldap.synchronizeUsers.removeDeleted = true + +# Attribute on the USER record that indicate their username to be used in gitblit +# when synchronizing users from LDAP +# if blank, Gitblit will use uid +# +# +realm.ldap.uid = uid + +# The RedmineUserService must be backed by another user service for standard user +# and team management. +# default: users.conf +# +# RESTART REQUIRED +realm.redmine.backingUserService = users.conf + +# URL of the Redmine. +realm.redmine.url = http://example.com/redmine + +# +# Server Settings +# + +# The temporary folder to decompress the embedded gitblit webapp. +# +# SINCE 0.5.0 +# RESTART REQUIRED +server.tempFolder = temp + +# Use Jetty NIO connectors. If false, Jetty Socket connectors will be used. +# +# SINCE 0.5.0 +# RESTART REQUIRED +server.useNio = true + +# Context path for the GO application. You might want to change the context +# path if running Gitblit behind a proxy layer such as mod_proxy. +# +# SINCE 0.7.0 +# RESTART REQUIRED +server.contextPath = / + +# Standard http port to serve. <= 0 disables this connector. +# On Unix/Linux systems, ports < 1024 require root permissions. +# Recommended value: 80 or 8080 +# +# SINCE 0.5.0 +# RESTART REQUIRED +server.httpPort = 0 + +# Secure/SSL https port to serve. <= 0 disables this connector. +# On Unix/Linux systems, ports < 1024 require root permissions. +# Recommended value: 443 or 8443 +# +# SINCE 0.5.0 +# RESTART REQUIRED +server.httpsPort = 8443 + +# Port for serving an Apache JServ Protocol (AJP) 1.3 connector for integrating +# Gitblit GO into an Apache HTTP server setup. <= 0 disables this connector. +# Recommended value: 8009 +# +# SINCE 0.9.0 +# RESTART REQUIRED +server.ajpPort = 0 + +# Specify the interface for Jetty to bind the standard connector. +# You may specify an ip or an empty value to bind to all interfaces. +# Specifying localhost will result in Gitblit ONLY listening to requests to +# localhost. +# +# SINCE 0.5.0 +# RESTART REQUIRED +server.httpBindInterface = localhost + +# Specify the interface for Jetty to bind the secure connector. +# You may specify an ip or an empty value to bind to all interfaces. +# Specifying localhost will result in Gitblit ONLY listening to requests to +# localhost. +# +# SINCE 0.5.0 +# RESTART REQUIRED +server.httpsBindInterface = localhost + +# Specify the interface for Jetty to bind the AJP connector. +# You may specify an ip or an empty value to bind to all interfaces. +# Specifying localhost will result in Gitblit ONLY listening to requests to +# localhost. +# +# SINCE 0.9.0 +# RESTART REQUIRED +server.ajpBindInterface = localhost + +# Password for SSL keystore. +# Keystore password and certificate password must match. +# This is provided for convenience, its probably more secure to set this value +# using the --storePassword command line parameter. +# +# If you are using the official JRE or JDK from Oracle you may not have the +# JCE Unlimited Strength Jurisdiction Policy files bundled with your JVM. Because +# of this, your store/key password can not exceed 7 characters. If you require +# longer passwords you may need to install the JCE Unlimited Strength Jurisdiction +# Policy files from Oracle. +# +# http://www.oracle.com/technetwork/java/javase/downloads/index.html +# +# Gitblit and the Gitblit Certificate Authority will both indicate if Unlimited +# Strength encryption is available. +# +# SINCE 0.5.0 +# RESTART REQUIRED +server.storePassword = gitblit + +# If serving over https (recommended) you might consider requiring clients to +# authenticate with ssl certificates. If enabled, only https clients with the +# a valid client certificate will be able to access Gitblit. +# +# If disabled, client certificate authentication is optional and will be tried +# first before falling-back to form authentication or basic authentication. +# +# Requiring client certificates to access any of Gitblit may be too extreme, +# consider this carefully. +# +# SINCE 1.2.0 +# RESTART REQUIRED +server.requireClientCertificates = false + +# Port for shutdown monitor to listen on. +# +# SINCE 0.5.0 +# RESTART REQUIRED +server.shutdownPort = 8081 diff --git a/src/com/gitblit/ConfigUserService.java b/src/com/gitblit/ConfigUserService.java index 478d6b31..e3cdbe6b 100644 --- a/src/com/gitblit/ConfigUserService.java +++ b/src/com/gitblit/ConfigUserService.java @@ -1,1074 +1,1074 @@ -/* - * Copyright 2011 gitblit.com. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.gitblit; - -import java.io.File; -import java.io.IOException; -import java.text.MessageFormat; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; - -import org.eclipse.jgit.lib.StoredConfig; -import org.eclipse.jgit.storage.file.FileBasedConfig; -import org.eclipse.jgit.util.FS; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.gitblit.Constants.AccessPermission; -import com.gitblit.models.TeamModel; -import com.gitblit.models.UserModel; -import com.gitblit.utils.ArrayUtils; -import com.gitblit.utils.DeepCopier; -import com.gitblit.utils.StringUtils; - -/** - * ConfigUserService is Gitblit's default user service implementation since - * version 0.8.0. - * - * Users and their repository memberships are stored in a git-style config file - * which is cached and dynamically reloaded when modified. This file is - * plain-text, human-readable, and may be edited with a text editor. - * - * Additionally, this format allows for expansion of the user model without - * bringing in the complexity of a database. - * - * @author James Moger - * - */ -public class ConfigUserService implements IUserService { - - private static final String TEAM = "team"; - - private static final String USER = "user"; - - private static final String PASSWORD = "password"; - - private static final String DISPLAYNAME = "displayName"; - - private static final String EMAILADDRESS = "emailAddress"; - - private static final String ORGANIZATIONALUNIT = "organizationalUnit"; - - private static final String ORGANIZATION = "organization"; - - private static final String LOCALITY = "locality"; - - private static final String STATEPROVINCE = "stateProvince"; - - private static final String COUNTRYCODE = "countryCode"; - - private static final String COOKIE = "cookie"; - - private static final String REPOSITORY = "repository"; - - private static final String ROLE = "role"; - - private static final String MAILINGLIST = "mailingList"; - - private static final String PRERECEIVE = "preReceiveScript"; - - private static final String POSTRECEIVE = "postReceiveScript"; - - private final File realmFile; - - private final Logger logger = LoggerFactory.getLogger(ConfigUserService.class); - - private final Map users = new ConcurrentHashMap(); - - private final Map cookies = new ConcurrentHashMap(); - - private final Map teams = new ConcurrentHashMap(); - - private volatile long lastModified; - - private volatile boolean forceReload; - - public ConfigUserService(File realmFile) { - this.realmFile = realmFile; - } - - /** - * Setup the user service. - * - * @param settings - * @since 0.7.0 - */ - @Override - public void setup(IStoredSettings settings) { - } - - /** - * Does the user service support changes to credentials? - * - * @return true or false - * @since 1.0.0 - */ - @Override - public boolean supportsCredentialChanges() { - return true; - } - - /** - * Does the user service support changes to user display name? - * - * @return true or false - * @since 1.0.0 - */ - @Override - public boolean supportsDisplayNameChanges() { - return true; - } - - /** - * Does the user service support changes to user email address? - * - * @return true or false - * @since 1.0.0 - */ - @Override - public boolean supportsEmailAddressChanges() { - return true; - } - - /** - * Does the user service support changes to team memberships? - * - * @return true or false - * @since 1.0.0 - */ - public boolean supportsTeamMembershipChanges() { - return true; - } - - /** - * Does the user service support cookie authentication? - * - * @return true or false - */ - @Override - public boolean supportsCookies() { - return true; - } - - /** - * Returns the cookie value for the specified user. - * - * @param model - * @return cookie value - */ - @Override - public String getCookie(UserModel model) { - if (!StringUtils.isEmpty(model.cookie)) { - return model.cookie; - } - read(); - UserModel storedModel = users.get(model.username.toLowerCase()); - return storedModel.cookie; - } - - /** - * Authenticate a user based on their cookie. - * - * @param cookie - * @return a user object or null - */ - @Override - public UserModel authenticate(char[] cookie) { - String hash = new String(cookie); - if (StringUtils.isEmpty(hash)) { - return null; - } - read(); - UserModel model = null; - if (cookies.containsKey(hash)) { - model = cookies.get(hash); - } - return model; - } - - /** - * Authenticate a user based on a username and password. - * - * @param username - * @param password - * @return a user object or null - */ - @Override - public UserModel authenticate(String username, char[] password) { - read(); - UserModel returnedUser = null; - UserModel user = getUserModel(username); - if (user == null) { - return null; - } - if (user.password.startsWith(StringUtils.MD5_TYPE)) { - // password digest - String md5 = StringUtils.MD5_TYPE + StringUtils.getMD5(new String(password)); - if (user.password.equalsIgnoreCase(md5)) { - returnedUser = user; - } - } else if (user.password.startsWith(StringUtils.COMBINED_MD5_TYPE)) { - // username+password digest - String md5 = StringUtils.COMBINED_MD5_TYPE - + StringUtils.getMD5(username.toLowerCase() + new String(password)); - if (user.password.equalsIgnoreCase(md5)) { - returnedUser = user; - } - } else if (user.password.equals(new String(password))) { - // plain-text password - returnedUser = user; - } - return returnedUser; - } - - /** - * Logout a user. - * - * @param user - */ - @Override - public void logout(UserModel user) { - } - - /** - * Retrieve the user object for the specified username. - * - * @param username - * @return a user object or null - */ - @Override - public UserModel getUserModel(String username) { - read(); - UserModel model = users.get(username.toLowerCase()); - if (model != null) { - // clone the model, otherwise all changes to this object are - // live and unpersisted - model = DeepCopier.copy(model); - } - return model; - } - - /** - * Updates/writes a complete user object. - * - * @param model - * @return true if update is successful - */ - @Override - public boolean updateUserModel(UserModel model) { - return updateUserModel(model.username, model); - } - - /** - * Updates/writes all specified user objects. - * - * @param models a list of user models - * @return true if update is successful - * @since 1.2.0 - */ - @Override - public boolean updateUserModels(Collection models) { - try { - read(); - for (UserModel model : models) { - UserModel originalUser = users.remove(model.username.toLowerCase()); - users.put(model.username.toLowerCase(), model); - // null check on "final" teams because JSON-sourced UserModel - // can have a null teams object - if (model.teams != null) { - for (TeamModel team : model.teams) { - TeamModel t = teams.get(team.name.toLowerCase()); - if (t == null) { - // new team - team.addUser(model.username); - teams.put(team.name.toLowerCase(), team); - } else { - // do not clobber existing team definition - // maybe because this is a federated user - t.addUser(model.username); - } - } - - // check for implicit team removal - if (originalUser != null) { - for (TeamModel team : originalUser.teams) { - if (!model.isTeamMember(team.name)) { - team.removeUser(model.username); - } - } - } - } - } - write(); - return true; - } catch (Throwable t) { - logger.error(MessageFormat.format("Failed to update user {0} models!", models.size()), - t); - } - return false; - } - - /** - * Updates/writes and replaces a complete user object keyed by username. - * This method allows for renaming a user. - * - * @param username - * the old username - * @param model - * the user object to use for username - * @return true if update is successful - */ - @Override - public boolean updateUserModel(String username, UserModel model) { - UserModel originalUser = null; - try { - read(); - originalUser = users.remove(username.toLowerCase()); - users.put(model.username.toLowerCase(), model); - // null check on "final" teams because JSON-sourced UserModel - // can have a null teams object - if (model.teams != null) { - for (TeamModel team : model.teams) { - TeamModel t = teams.get(team.name.toLowerCase()); - if (t == null) { - // new team - team.addUser(username); - teams.put(team.name.toLowerCase(), team); - } else { - // do not clobber existing team definition - // maybe because this is a federated user - t.removeUser(username); - t.addUser(model.username); - } - } - - // check for implicit team removal - if (originalUser != null) { - for (TeamModel team : originalUser.teams) { - if (!model.isTeamMember(team.name)) { - team.removeUser(username); - } - } - } - } - write(); - return true; - } catch (Throwable t) { - if (originalUser != null) { - // restore original user - users.put(originalUser.username.toLowerCase(), originalUser); - } else { - // drop attempted add - users.remove(model.username.toLowerCase()); - } - logger.error(MessageFormat.format("Failed to update user model {0}!", model.username), - t); - } - return false; - } - - /** - * Deletes the user object from the user service. - * - * @param model - * @return true if successful - */ - @Override - public boolean deleteUserModel(UserModel model) { - return deleteUser(model.username); - } - - /** - * Delete the user object with the specified username - * - * @param username - * @return true if successful - */ - @Override - public boolean deleteUser(String username) { - try { - // Read realm file - read(); - UserModel model = users.remove(username.toLowerCase()); - // remove user from team - for (TeamModel team : model.teams) { - TeamModel t = teams.get(team.name); - if (t == null) { - // new team - team.removeUser(username); - teams.put(team.name.toLowerCase(), team); - } else { - // existing team - t.removeUser(username); - } - } - write(); - return true; - } catch (Throwable t) { - logger.error(MessageFormat.format("Failed to delete user {0}!", username), t); - } - return false; - } - - /** - * Returns the list of all teams available to the login service. - * - * @return list of all teams - * @since 0.8.0 - */ - @Override - public List getAllTeamNames() { - read(); - List list = new ArrayList(teams.keySet()); - Collections.sort(list); - return list; - } - - /** - * Returns the list of all teams available to the login service. - * - * @return list of all teams - * @since 0.8.0 - */ - @Override - public List getAllTeams() { - read(); - List list = new ArrayList(teams.values()); - list = DeepCopier.copy(list); - Collections.sort(list); - return list; - } - - /** - * Returns the list of all users who are allowed to bypass the access - * restriction placed on the specified repository. - * - * @param role - * the repository name - * @return list of all usernames that can bypass the access restriction - */ - @Override - public List getTeamnamesForRepositoryRole(String role) { - List list = new ArrayList(); - try { - read(); - for (Map.Entry entry : teams.entrySet()) { - TeamModel model = entry.getValue(); - if (model.hasRepositoryPermission(role)) { - list.add(model.name); - } - } - } catch (Throwable t) { - logger.error(MessageFormat.format("Failed to get teamnames for role {0}!", role), t); - } - Collections.sort(list); - return list; - } - - /** - * Sets the list of all teams who are allowed to bypass the access - * restriction placed on the specified repository. - * - * @param role - * the repository name - * @param teamnames - * @return true if successful - */ - @Override - public boolean setTeamnamesForRepositoryRole(String role, List teamnames) { - try { - Set specifiedTeams = new HashSet(); - for (String teamname : teamnames) { - specifiedTeams.add(teamname.toLowerCase()); - } - - read(); - - // identify teams which require add or remove role - for (TeamModel team : teams.values()) { - // team has role, check against revised team list - if (specifiedTeams.contains(team.name.toLowerCase())) { - team.addRepositoryPermission(role); - } else { - // remove role from team - team.removeRepositoryPermission(role); - } - } - - // persist changes - write(); - return true; - } catch (Throwable t) { - logger.error(MessageFormat.format("Failed to set teams for role {0}!", role), t); - } - return false; - } - - /** - * Retrieve the team object for the specified team name. - * - * @param teamname - * @return a team object or null - * @since 0.8.0 - */ - @Override - public TeamModel getTeamModel(String teamname) { - read(); - TeamModel model = teams.get(teamname.toLowerCase()); - if (model != null) { - // clone the model, otherwise all changes to this object are - // live and unpersisted - model = DeepCopier.copy(model); - } - return model; - } - - /** - * Updates/writes a complete team object. - * - * @param model - * @return true if update is successful - * @since 0.8.0 - */ - @Override - public boolean updateTeamModel(TeamModel model) { - return updateTeamModel(model.name, model); - } - - /** - * Updates/writes all specified team objects. - * - * @param models a list of team models - * @return true if update is successful - * @since 1.2.0 - */ - @Override - public boolean updateTeamModels(Collection models) { - try { - read(); - for (TeamModel team : models) { - teams.put(team.name.toLowerCase(), team); - } - write(); - return true; - } catch (Throwable t) { - logger.error(MessageFormat.format("Failed to update team {0} models!", models.size()), t); - } - return false; - } - - /** - * Updates/writes and replaces a complete team object keyed by teamname. - * This method allows for renaming a team. - * - * @param teamname - * the old teamname - * @param model - * the team object to use for teamname - * @return true if update is successful - * @since 0.8.0 - */ - @Override - public boolean updateTeamModel(String teamname, TeamModel model) { - TeamModel original = null; - try { - read(); - original = teams.remove(teamname.toLowerCase()); - teams.put(model.name.toLowerCase(), model); - write(); - return true; - } catch (Throwable t) { - if (original != null) { - // restore original team - teams.put(original.name.toLowerCase(), original); - } else { - // drop attempted add - teams.remove(model.name.toLowerCase()); - } - logger.error(MessageFormat.format("Failed to update team model {0}!", model.name), t); - } - return false; - } - - /** - * Deletes the team object from the user service. - * - * @param model - * @return true if successful - * @since 0.8.0 - */ - @Override - public boolean deleteTeamModel(TeamModel model) { - return deleteTeam(model.name); - } - - /** - * Delete the team object with the specified teamname - * - * @param teamname - * @return true if successful - * @since 0.8.0 - */ - @Override - public boolean deleteTeam(String teamname) { - try { - // Read realm file - read(); - teams.remove(teamname.toLowerCase()); - write(); - return true; - } catch (Throwable t) { - logger.error(MessageFormat.format("Failed to delete team {0}!", teamname), t); - } - return false; - } - - /** - * Returns the list of all users available to the login service. - * - * @return list of all usernames - */ - @Override - public List getAllUsernames() { - read(); - List list = new ArrayList(users.keySet()); - Collections.sort(list); - return list; - } - - /** - * Returns the list of all users available to the login service. - * - * @return list of all usernames - */ - @Override - public List getAllUsers() { - read(); - List list = new ArrayList(users.values()); - list = DeepCopier.copy(list); - Collections.sort(list); - return list; - } - - /** - * Returns the list of all users who are allowed to bypass the access - * restriction placed on the specified repository. - * - * @param role - * the repository name - * @return list of all usernames that can bypass the access restriction - */ - @Override - public List getUsernamesForRepositoryRole(String role) { - List list = new ArrayList(); - try { - read(); - for (Map.Entry entry : users.entrySet()) { - UserModel model = entry.getValue(); - if (model.hasRepositoryPermission(role)) { - list.add(model.username); - } - } - } catch (Throwable t) { - logger.error(MessageFormat.format("Failed to get usernames for role {0}!", role), t); - } - Collections.sort(list); - return list; - } - - /** - * Sets the list of all uses who are allowed to bypass the access - * restriction placed on the specified repository. - * - * @param role - * the repository name - * @param usernames - * @return true if successful - */ - @Override - @Deprecated - public boolean setUsernamesForRepositoryRole(String role, List usernames) { - try { - Set specifiedUsers = new HashSet(); - for (String username : usernames) { - specifiedUsers.add(username.toLowerCase()); - } - - read(); - - // identify users which require add or remove role - for (UserModel user : users.values()) { - // user has role, check against revised user list - if (specifiedUsers.contains(user.username.toLowerCase())) { - user.addRepositoryPermission(role); - } else { - // remove role from user - user.removeRepositoryPermission(role); - } - } - - // persist changes - write(); - return true; - } catch (Throwable t) { - logger.error(MessageFormat.format("Failed to set usernames for role {0}!", role), t); - } - return false; - } - - /** - * Renames a repository role. - * - * @param oldRole - * @param newRole - * @return true if successful - */ - @Override - public boolean renameRepositoryRole(String oldRole, String newRole) { - try { - read(); - // identify users which require role rename - for (UserModel model : users.values()) { - if (model.hasRepositoryPermission(oldRole)) { - AccessPermission permission = model.removeRepositoryPermission(oldRole); - model.setRepositoryPermission(newRole, permission); - } - } - - // identify teams which require role rename - for (TeamModel model : teams.values()) { - if (model.hasRepositoryPermission(oldRole)) { - AccessPermission permission = model.removeRepositoryPermission(oldRole); - model.setRepositoryPermission(newRole, permission); - } - } - // persist changes - write(); - return true; - } catch (Throwable t) { - logger.error( - MessageFormat.format("Failed to rename role {0} to {1}!", oldRole, newRole), t); - } - return false; - } - - /** - * Removes a repository role from all users. - * - * @param role - * @return true if successful - */ - @Override - public boolean deleteRepositoryRole(String role) { - try { - read(); - - // identify users which require role rename - for (UserModel user : users.values()) { - user.removeRepositoryPermission(role); - } - - // identify teams which require role rename - for (TeamModel team : teams.values()) { - team.removeRepositoryPermission(role); - } - - // persist changes - write(); - return true; - } catch (Throwable t) { - logger.error(MessageFormat.format("Failed to delete role {0}!", role), t); - } - return false; - } - - /** - * Writes the properties file. - * - * @throws IOException - */ - private synchronized void write() throws IOException { - // Write a temporary copy of the users file - File realmFileCopy = new File(realmFile.getAbsolutePath() + ".tmp"); - - StoredConfig config = new FileBasedConfig(realmFileCopy, FS.detect()); - - // write users - for (UserModel model : users.values()) { - if (!StringUtils.isEmpty(model.password)) { - config.setString(USER, model.username, PASSWORD, model.password); - } - if (!StringUtils.isEmpty(model.cookie)) { - config.setString(USER, model.username, COOKIE, model.cookie); - } - if (!StringUtils.isEmpty(model.displayName)) { - config.setString(USER, model.username, DISPLAYNAME, model.displayName); - } - if (!StringUtils.isEmpty(model.emailAddress)) { - config.setString(USER, model.username, EMAILADDRESS, model.emailAddress); - } - if (!StringUtils.isEmpty(model.organizationalUnit)) { - config.setString(USER, model.username, ORGANIZATIONALUNIT, model.organizationalUnit); - } - if (!StringUtils.isEmpty(model.organization)) { - config.setString(USER, model.username, ORGANIZATION, model.organization); - } - if (!StringUtils.isEmpty(model.locality)) { - config.setString(USER, model.username, LOCALITY, model.locality); - } - if (!StringUtils.isEmpty(model.stateProvince)) { - config.setString(USER, model.username, STATEPROVINCE, model.stateProvince); - } - if (!StringUtils.isEmpty(model.countryCode)) { - config.setString(USER, model.username, COUNTRYCODE, model.countryCode); - } - - // user roles - List roles = new ArrayList(); - if (model.canAdmin) { - roles.add(Constants.ADMIN_ROLE); - } - if (model.canFork) { - roles.add(Constants.FORK_ROLE); - } - if (model.canCreate) { - roles.add(Constants.CREATE_ROLE); - } - if (model.excludeFromFederation) { - roles.add(Constants.NOT_FEDERATED_ROLE); - } - if (roles.size() == 0) { - // we do this to ensure that user record with no password - // is written. otherwise, StoredConfig optimizes that account - // away. :( - roles.add(Constants.NO_ROLE); - } - config.setStringList(USER, model.username, ROLE, roles); - - // discrete repository permissions - if (model.permissions != null && !model.canAdmin) { - List permissions = new ArrayList(); - for (Map.Entry entry : model.permissions.entrySet()) { - if (entry.getValue().exceeds(AccessPermission.NONE)) { - permissions.add(entry.getValue().asRole(entry.getKey())); - } - } - config.setStringList(USER, model.username, REPOSITORY, permissions); - } - } - - // write teams - for (TeamModel model : teams.values()) { - // team roles - List roles = new ArrayList(); - if (model.canAdmin) { - roles.add(Constants.ADMIN_ROLE); - } - if (model.canFork) { - roles.add(Constants.FORK_ROLE); - } - if (model.canCreate) { - roles.add(Constants.CREATE_ROLE); - } - if (roles.size() == 0) { - // we do this to ensure that team record is written. - // Otherwise, StoredConfig might optimizes that record away. - roles.add(Constants.NO_ROLE); - } - config.setStringList(TEAM, model.name, ROLE, roles); - - if (!model.canAdmin) { - // write team permission for non-admin teams - if (model.permissions == null) { - // null check on "final" repositories because JSON-sourced TeamModel - // can have a null repositories object - if (!ArrayUtils.isEmpty(model.repositories)) { - config.setStringList(TEAM, model.name, REPOSITORY, new ArrayList( - model.repositories)); - } - } else { - // discrete repository permissions - List permissions = new ArrayList(); - for (Map.Entry entry : model.permissions.entrySet()) { - if (entry.getValue().exceeds(AccessPermission.NONE)) { - // code:repository (e.g. RW+:~james/myrepo.git - permissions.add(entry.getValue().asRole(entry.getKey())); - } - } - config.setStringList(TEAM, model.name, REPOSITORY, permissions); - } - } - - // null check on "final" users because JSON-sourced TeamModel - // can have a null users object - if (!ArrayUtils.isEmpty(model.users)) { - config.setStringList(TEAM, model.name, USER, new ArrayList(model.users)); - } - - // null check on "final" mailing lists because JSON-sourced - // TeamModel can have a null users object - if (!ArrayUtils.isEmpty(model.mailingLists)) { - config.setStringList(TEAM, model.name, MAILINGLIST, new ArrayList( - model.mailingLists)); - } - - // null check on "final" preReceiveScripts because JSON-sourced - // TeamModel can have a null preReceiveScripts object - if (!ArrayUtils.isEmpty(model.preReceiveScripts)) { - config.setStringList(TEAM, model.name, PRERECEIVE, model.preReceiveScripts); - } - - // null check on "final" postReceiveScripts because JSON-sourced - // TeamModel can have a null postReceiveScripts object - if (!ArrayUtils.isEmpty(model.postReceiveScripts)) { - config.setStringList(TEAM, model.name, POSTRECEIVE, model.postReceiveScripts); - } - } - - config.save(); - // manually set the forceReload flag because not all JVMs support real - // millisecond resolution of lastModified. (issue-55) - forceReload = true; - - // If the write is successful, delete the current file and rename - // the temporary copy to the original filename. - if (realmFileCopy.exists() && realmFileCopy.length() > 0) { - if (realmFile.exists()) { - if (!realmFile.delete()) { - throw new IOException(MessageFormat.format("Failed to delete {0}!", - realmFile.getAbsolutePath())); - } - } - if (!realmFileCopy.renameTo(realmFile)) { - throw new IOException(MessageFormat.format("Failed to rename {0} to {1}!", - realmFileCopy.getAbsolutePath(), realmFile.getAbsolutePath())); - } - } else { - throw new IOException(MessageFormat.format("Failed to save {0}!", - realmFileCopy.getAbsolutePath())); - } - } - - /** - * Reads the realm file and rebuilds the in-memory lookup tables. - */ - protected synchronized void read() { - if (realmFile.exists() && (forceReload || (realmFile.lastModified() != lastModified))) { - forceReload = false; - lastModified = realmFile.lastModified(); - users.clear(); - cookies.clear(); - teams.clear(); - - try { - StoredConfig config = new FileBasedConfig(realmFile, FS.detect()); - config.load(); - Set usernames = config.getSubsections(USER); - for (String username : usernames) { - UserModel user = new UserModel(username.toLowerCase()); - user.password = config.getString(USER, username, PASSWORD); - user.displayName = config.getString(USER, username, DISPLAYNAME); - user.emailAddress = config.getString(USER, username, EMAILADDRESS); - user.organizationalUnit = config.getString(USER, username, ORGANIZATIONALUNIT); - user.organization = config.getString(USER, username, ORGANIZATION); - user.locality = config.getString(USER, username, LOCALITY); - user.stateProvince = config.getString(USER, username, STATEPROVINCE); - user.countryCode = config.getString(USER, username, COUNTRYCODE); - user.cookie = config.getString(USER, username, COOKIE); - if (StringUtils.isEmpty(user.cookie) && !StringUtils.isEmpty(user.password)) { - user.cookie = StringUtils.getSHA1(user.username + user.password); - } - - // user roles - Set roles = new HashSet(Arrays.asList(config.getStringList( - USER, username, ROLE))); - user.canAdmin = roles.contains(Constants.ADMIN_ROLE); - user.canFork = roles.contains(Constants.FORK_ROLE); - user.canCreate = roles.contains(Constants.CREATE_ROLE); - user.excludeFromFederation = roles.contains(Constants.NOT_FEDERATED_ROLE); - - // repository memberships - if (!user.canAdmin) { - // non-admin, read permissions - Set repositories = new HashSet(Arrays.asList(config - .getStringList(USER, username, REPOSITORY))); - for (String repository : repositories) { - user.addRepositoryPermission(repository); - } - } - - // update cache - users.put(user.username, user); - if (!StringUtils.isEmpty(user.cookie)) { - cookies.put(user.cookie, user); - } - } - - // load the teams - Set teamnames = config.getSubsections(TEAM); - for (String teamname : teamnames) { - TeamModel team = new TeamModel(teamname); - Set roles = new HashSet(Arrays.asList(config.getStringList( - TEAM, teamname, ROLE))); - team.canAdmin = roles.contains(Constants.ADMIN_ROLE); - team.canFork = roles.contains(Constants.FORK_ROLE); - team.canCreate = roles.contains(Constants.CREATE_ROLE); - - if (!team.canAdmin) { - // non-admin team, read permissions - team.addRepositoryPermissions(Arrays.asList(config.getStringList(TEAM, teamname, - REPOSITORY))); - } - team.addUsers(Arrays.asList(config.getStringList(TEAM, teamname, USER))); - team.addMailingLists(Arrays.asList(config.getStringList(TEAM, teamname, - MAILINGLIST))); - team.preReceiveScripts.addAll(Arrays.asList(config.getStringList(TEAM, - teamname, PRERECEIVE))); - team.postReceiveScripts.addAll(Arrays.asList(config.getStringList(TEAM, - teamname, POSTRECEIVE))); - - teams.put(team.name.toLowerCase(), team); - - // set the teams on the users - for (String user : team.users) { - UserModel model = users.get(user); - if (model != null) { - model.teams.add(team); - } - } - } - } catch (Exception e) { - logger.error(MessageFormat.format("Failed to read {0}", realmFile), e); - } - } - } - - protected long lastModified() { - return lastModified; - } - - @Override - public String toString() { - return getClass().getSimpleName() + "(" + realmFile.getAbsolutePath() + ")"; - } -} +/* + * Copyright 2011 gitblit.com. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.gitblit; + +import java.io.File; +import java.io.IOException; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.eclipse.jgit.lib.StoredConfig; +import org.eclipse.jgit.storage.file.FileBasedConfig; +import org.eclipse.jgit.util.FS; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.gitblit.Constants.AccessPermission; +import com.gitblit.models.TeamModel; +import com.gitblit.models.UserModel; +import com.gitblit.utils.ArrayUtils; +import com.gitblit.utils.DeepCopier; +import com.gitblit.utils.StringUtils; + +/** + * ConfigUserService is Gitblit's default user service implementation since + * version 0.8.0. + * + * Users and their repository memberships are stored in a git-style config file + * which is cached and dynamically reloaded when modified. This file is + * plain-text, human-readable, and may be edited with a text editor. + * + * Additionally, this format allows for expansion of the user model without + * bringing in the complexity of a database. + * + * @author James Moger + * + */ +public class ConfigUserService implements IUserService { + + private static final String TEAM = "team"; + + private static final String USER = "user"; + + private static final String PASSWORD = "password"; + + private static final String DISPLAYNAME = "displayName"; + + private static final String EMAILADDRESS = "emailAddress"; + + private static final String ORGANIZATIONALUNIT = "organizationalUnit"; + + private static final String ORGANIZATION = "organization"; + + private static final String LOCALITY = "locality"; + + private static final String STATEPROVINCE = "stateProvince"; + + private static final String COUNTRYCODE = "countryCode"; + + private static final String COOKIE = "cookie"; + + private static final String REPOSITORY = "repository"; + + private static final String ROLE = "role"; + + private static final String MAILINGLIST = "mailingList"; + + private static final String PRERECEIVE = "preReceiveScript"; + + private static final String POSTRECEIVE = "postReceiveScript"; + + private final File realmFile; + + private final Logger logger = LoggerFactory.getLogger(ConfigUserService.class); + + private final Map users = new ConcurrentHashMap(); + + private final Map cookies = new ConcurrentHashMap(); + + private final Map teams = new ConcurrentHashMap(); + + private volatile long lastModified; + + private volatile boolean forceReload; + + public ConfigUserService(File realmFile) { + this.realmFile = realmFile; + } + + /** + * Setup the user service. + * + * @param settings + * @since 0.7.0 + */ + @Override + public void setup(IStoredSettings settings) { + } + + /** + * Does the user service support changes to credentials? + * + * @return true or false + * @since 1.0.0 + */ + @Override + public boolean supportsCredentialChanges() { + return true; + } + + /** + * Does the user service support changes to user display name? + * + * @return true or false + * @since 1.0.0 + */ + @Override + public boolean supportsDisplayNameChanges() { + return true; + } + + /** + * Does the user service support changes to user email address? + * + * @return true or false + * @since 1.0.0 + */ + @Override + public boolean supportsEmailAddressChanges() { + return true; + } + + /** + * Does the user service support changes to team memberships? + * + * @return true or false + * @since 1.0.0 + */ + public boolean supportsTeamMembershipChanges() { + return true; + } + + /** + * Does the user service support cookie authentication? + * + * @return true or false + */ + @Override + public boolean supportsCookies() { + return true; + } + + /** + * Returns the cookie value for the specified user. + * + * @param model + * @return cookie value + */ + @Override + public String getCookie(UserModel model) { + if (!StringUtils.isEmpty(model.cookie)) { + return model.cookie; + } + read(); + UserModel storedModel = users.get(model.username.toLowerCase()); + return storedModel.cookie; + } + + /** + * Authenticate a user based on their cookie. + * + * @param cookie + * @return a user object or null + */ + @Override + public UserModel authenticate(char[] cookie) { + String hash = new String(cookie); + if (StringUtils.isEmpty(hash)) { + return null; + } + read(); + UserModel model = null; + if (cookies.containsKey(hash)) { + model = cookies.get(hash); + } + return model; + } + + /** + * Authenticate a user based on a username and password. + * + * @param username + * @param password + * @return a user object or null + */ + @Override + public UserModel authenticate(String username, char[] password) { + read(); + UserModel returnedUser = null; + UserModel user = getUserModel(username); + if (user == null) { + return null; + } + if (user.password.startsWith(StringUtils.MD5_TYPE)) { + // password digest + String md5 = StringUtils.MD5_TYPE + StringUtils.getMD5(new String(password)); + if (user.password.equalsIgnoreCase(md5)) { + returnedUser = user; + } + } else if (user.password.startsWith(StringUtils.COMBINED_MD5_TYPE)) { + // username+password digest + String md5 = StringUtils.COMBINED_MD5_TYPE + + StringUtils.getMD5(username.toLowerCase() + new String(password)); + if (user.password.equalsIgnoreCase(md5)) { + returnedUser = user; + } + } else if (user.password.equals(new String(password))) { + // plain-text password + returnedUser = user; + } + return returnedUser; + } + + /** + * Logout a user. + * + * @param user + */ + @Override + public void logout(UserModel user) { + } + + /** + * Retrieve the user object for the specified username. + * + * @param username + * @return a user object or null + */ + @Override + public UserModel getUserModel(String username) { + read(); + UserModel model = users.get(username.toLowerCase()); + if (model != null) { + // clone the model, otherwise all changes to this object are + // live and unpersisted + model = DeepCopier.copy(model); + } + return model; + } + + /** + * Updates/writes a complete user object. + * + * @param model + * @return true if update is successful + */ + @Override + public boolean updateUserModel(UserModel model) { + return updateUserModel(model.username, model); + } + + /** + * Updates/writes all specified user objects. + * + * @param models a list of user models + * @return true if update is successful + * @since 1.2.0 + */ + @Override + public boolean updateUserModels(Collection models) { + try { + read(); + for (UserModel model : models) { + UserModel originalUser = users.remove(model.username.toLowerCase()); + users.put(model.username.toLowerCase(), model); + // null check on "final" teams because JSON-sourced UserModel + // can have a null teams object + if (model.teams != null) { + for (TeamModel team : model.teams) { + TeamModel t = teams.get(team.name.toLowerCase()); + if (t == null) { + // new team + team.addUser(model.username); + teams.put(team.name.toLowerCase(), team); + } else { + // do not clobber existing team definition + // maybe because this is a federated user + t.addUser(model.username); + } + } + + // check for implicit team removal + if (originalUser != null) { + for (TeamModel team : originalUser.teams) { + if (!model.isTeamMember(team.name)) { + team.removeUser(model.username); + } + } + } + } + } + write(); + return true; + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to update user {0} models!", models.size()), + t); + } + return false; + } + + /** + * Updates/writes and replaces a complete user object keyed by username. + * This method allows for renaming a user. + * + * @param username + * the old username + * @param model + * the user object to use for username + * @return true if update is successful + */ + @Override + public boolean updateUserModel(String username, UserModel model) { + UserModel originalUser = null; + try { + read(); + originalUser = users.remove(username.toLowerCase()); + users.put(model.username.toLowerCase(), model); + // null check on "final" teams because JSON-sourced UserModel + // can have a null teams object + if (model.teams != null) { + for (TeamModel team : model.teams) { + TeamModel t = teams.get(team.name.toLowerCase()); + if (t == null) { + // new team + team.addUser(username); + teams.put(team.name.toLowerCase(), team); + } else { + // do not clobber existing team definition + // maybe because this is a federated user + t.removeUser(username); + t.addUser(model.username); + } + } + + // check for implicit team removal + if (originalUser != null) { + for (TeamModel team : originalUser.teams) { + if (!model.isTeamMember(team.name)) { + team.removeUser(username); + } + } + } + } + write(); + return true; + } catch (Throwable t) { + if (originalUser != null) { + // restore original user + users.put(originalUser.username.toLowerCase(), originalUser); + } else { + // drop attempted add + users.remove(model.username.toLowerCase()); + } + logger.error(MessageFormat.format("Failed to update user model {0}!", model.username), + t); + } + return false; + } + + /** + * Deletes the user object from the user service. + * + * @param model + * @return true if successful + */ + @Override + public boolean deleteUserModel(UserModel model) { + return deleteUser(model.username); + } + + /** + * Delete the user object with the specified username + * + * @param username + * @return true if successful + */ + @Override + public boolean deleteUser(String username) { + try { + // Read realm file + read(); + UserModel model = users.remove(username.toLowerCase()); + // remove user from team + for (TeamModel team : model.teams) { + TeamModel t = teams.get(team.name); + if (t == null) { + // new team + team.removeUser(username); + teams.put(team.name.toLowerCase(), team); + } else { + // existing team + t.removeUser(username); + } + } + write(); + return true; + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to delete user {0}!", username), t); + } + return false; + } + + /** + * Returns the list of all teams available to the login service. + * + * @return list of all teams + * @since 0.8.0 + */ + @Override + public List getAllTeamNames() { + read(); + List list = new ArrayList(teams.keySet()); + Collections.sort(list); + return list; + } + + /** + * Returns the list of all teams available to the login service. + * + * @return list of all teams + * @since 0.8.0 + */ + @Override + public List getAllTeams() { + read(); + List list = new ArrayList(teams.values()); + list = DeepCopier.copy(list); + Collections.sort(list); + return list; + } + + /** + * Returns the list of all users who are allowed to bypass the access + * restriction placed on the specified repository. + * + * @param role + * the repository name + * @return list of all usernames that can bypass the access restriction + */ + @Override + public List getTeamnamesForRepositoryRole(String role) { + List list = new ArrayList(); + try { + read(); + for (Map.Entry entry : teams.entrySet()) { + TeamModel model = entry.getValue(); + if (model.hasRepositoryPermission(role)) { + list.add(model.name); + } + } + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to get teamnames for role {0}!", role), t); + } + Collections.sort(list); + return list; + } + + /** + * Sets the list of all teams who are allowed to bypass the access + * restriction placed on the specified repository. + * + * @param role + * the repository name + * @param teamnames + * @return true if successful + */ + @Override + public boolean setTeamnamesForRepositoryRole(String role, List teamnames) { + try { + Set specifiedTeams = new HashSet(); + for (String teamname : teamnames) { + specifiedTeams.add(teamname.toLowerCase()); + } + + read(); + + // identify teams which require add or remove role + for (TeamModel team : teams.values()) { + // team has role, check against revised team list + if (specifiedTeams.contains(team.name.toLowerCase())) { + team.addRepositoryPermission(role); + } else { + // remove role from team + team.removeRepositoryPermission(role); + } + } + + // persist changes + write(); + return true; + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to set teams for role {0}!", role), t); + } + return false; + } + + /** + * Retrieve the team object for the specified team name. + * + * @param teamname + * @return a team object or null + * @since 0.8.0 + */ + @Override + public TeamModel getTeamModel(String teamname) { + read(); + TeamModel model = teams.get(teamname.toLowerCase()); + if (model != null) { + // clone the model, otherwise all changes to this object are + // live and unpersisted + model = DeepCopier.copy(model); + } + return model; + } + + /** + * Updates/writes a complete team object. + * + * @param model + * @return true if update is successful + * @since 0.8.0 + */ + @Override + public boolean updateTeamModel(TeamModel model) { + return updateTeamModel(model.name, model); + } + + /** + * Updates/writes all specified team objects. + * + * @param models a list of team models + * @return true if update is successful + * @since 1.2.0 + */ + @Override + public boolean updateTeamModels(Collection models) { + try { + read(); + for (TeamModel team : models) { + teams.put(team.name.toLowerCase(), team); + } + write(); + return true; + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to update team {0} models!", models.size()), t); + } + return false; + } + + /** + * Updates/writes and replaces a complete team object keyed by teamname. + * This method allows for renaming a team. + * + * @param teamname + * the old teamname + * @param model + * the team object to use for teamname + * @return true if update is successful + * @since 0.8.0 + */ + @Override + public boolean updateTeamModel(String teamname, TeamModel model) { + TeamModel original = null; + try { + read(); + original = teams.remove(teamname.toLowerCase()); + teams.put(model.name.toLowerCase(), model); + write(); + return true; + } catch (Throwable t) { + if (original != null) { + // restore original team + teams.put(original.name.toLowerCase(), original); + } else { + // drop attempted add + teams.remove(model.name.toLowerCase()); + } + logger.error(MessageFormat.format("Failed to update team model {0}!", model.name), t); + } + return false; + } + + /** + * Deletes the team object from the user service. + * + * @param model + * @return true if successful + * @since 0.8.0 + */ + @Override + public boolean deleteTeamModel(TeamModel model) { + return deleteTeam(model.name); + } + + /** + * Delete the team object with the specified teamname + * + * @param teamname + * @return true if successful + * @since 0.8.0 + */ + @Override + public boolean deleteTeam(String teamname) { + try { + // Read realm file + read(); + teams.remove(teamname.toLowerCase()); + write(); + return true; + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to delete team {0}!", teamname), t); + } + return false; + } + + /** + * Returns the list of all users available to the login service. + * + * @return list of all usernames + */ + @Override + public List getAllUsernames() { + read(); + List list = new ArrayList(users.keySet()); + Collections.sort(list); + return list; + } + + /** + * Returns the list of all users available to the login service. + * + * @return list of all usernames + */ + @Override + public List getAllUsers() { + read(); + List list = new ArrayList(users.values()); + list = DeepCopier.copy(list); + Collections.sort(list); + return list; + } + + /** + * Returns the list of all users who are allowed to bypass the access + * restriction placed on the specified repository. + * + * @param role + * the repository name + * @return list of all usernames that can bypass the access restriction + */ + @Override + public List getUsernamesForRepositoryRole(String role) { + List list = new ArrayList(); + try { + read(); + for (Map.Entry entry : users.entrySet()) { + UserModel model = entry.getValue(); + if (model.hasRepositoryPermission(role)) { + list.add(model.username); + } + } + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to get usernames for role {0}!", role), t); + } + Collections.sort(list); + return list; + } + + /** + * Sets the list of all uses who are allowed to bypass the access + * restriction placed on the specified repository. + * + * @param role + * the repository name + * @param usernames + * @return true if successful + */ + @Override + @Deprecated + public boolean setUsernamesForRepositoryRole(String role, List usernames) { + try { + Set specifiedUsers = new HashSet(); + for (String username : usernames) { + specifiedUsers.add(username.toLowerCase()); + } + + read(); + + // identify users which require add or remove role + for (UserModel user : users.values()) { + // user has role, check against revised user list + if (specifiedUsers.contains(user.username.toLowerCase())) { + user.addRepositoryPermission(role); + } else { + // remove role from user + user.removeRepositoryPermission(role); + } + } + + // persist changes + write(); + return true; + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to set usernames for role {0}!", role), t); + } + return false; + } + + /** + * Renames a repository role. + * + * @param oldRole + * @param newRole + * @return true if successful + */ + @Override + public boolean renameRepositoryRole(String oldRole, String newRole) { + try { + read(); + // identify users which require role rename + for (UserModel model : users.values()) { + if (model.hasRepositoryPermission(oldRole)) { + AccessPermission permission = model.removeRepositoryPermission(oldRole); + model.setRepositoryPermission(newRole, permission); + } + } + + // identify teams which require role rename + for (TeamModel model : teams.values()) { + if (model.hasRepositoryPermission(oldRole)) { + AccessPermission permission = model.removeRepositoryPermission(oldRole); + model.setRepositoryPermission(newRole, permission); + } + } + // persist changes + write(); + return true; + } catch (Throwable t) { + logger.error( + MessageFormat.format("Failed to rename role {0} to {1}!", oldRole, newRole), t); + } + return false; + } + + /** + * Removes a repository role from all users. + * + * @param role + * @return true if successful + */ + @Override + public boolean deleteRepositoryRole(String role) { + try { + read(); + + // identify users which require role rename + for (UserModel user : users.values()) { + user.removeRepositoryPermission(role); + } + + // identify teams which require role rename + for (TeamModel team : teams.values()) { + team.removeRepositoryPermission(role); + } + + // persist changes + write(); + return true; + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to delete role {0}!", role), t); + } + return false; + } + + /** + * Writes the properties file. + * + * @throws IOException + */ + private synchronized void write() throws IOException { + // Write a temporary copy of the users file + File realmFileCopy = new File(realmFile.getAbsolutePath() + ".tmp"); + + StoredConfig config = new FileBasedConfig(realmFileCopy, FS.detect()); + + // write users + for (UserModel model : users.values()) { + if (!StringUtils.isEmpty(model.password)) { + config.setString(USER, model.username, PASSWORD, model.password); + } + if (!StringUtils.isEmpty(model.cookie)) { + config.setString(USER, model.username, COOKIE, model.cookie); + } + if (!StringUtils.isEmpty(model.displayName)) { + config.setString(USER, model.username, DISPLAYNAME, model.displayName); + } + if (!StringUtils.isEmpty(model.emailAddress)) { + config.setString(USER, model.username, EMAILADDRESS, model.emailAddress); + } + if (!StringUtils.isEmpty(model.organizationalUnit)) { + config.setString(USER, model.username, ORGANIZATIONALUNIT, model.organizationalUnit); + } + if (!StringUtils.isEmpty(model.organization)) { + config.setString(USER, model.username, ORGANIZATION, model.organization); + } + if (!StringUtils.isEmpty(model.locality)) { + config.setString(USER, model.username, LOCALITY, model.locality); + } + if (!StringUtils.isEmpty(model.stateProvince)) { + config.setString(USER, model.username, STATEPROVINCE, model.stateProvince); + } + if (!StringUtils.isEmpty(model.countryCode)) { + config.setString(USER, model.username, COUNTRYCODE, model.countryCode); + } + + // user roles + List roles = new ArrayList(); + if (model.canAdmin) { + roles.add(Constants.ADMIN_ROLE); + } + if (model.canFork) { + roles.add(Constants.FORK_ROLE); + } + if (model.canCreate) { + roles.add(Constants.CREATE_ROLE); + } + if (model.excludeFromFederation) { + roles.add(Constants.NOT_FEDERATED_ROLE); + } + if (roles.size() == 0) { + // we do this to ensure that user record with no password + // is written. otherwise, StoredConfig optimizes that account + // away. :( + roles.add(Constants.NO_ROLE); + } + config.setStringList(USER, model.username, ROLE, roles); + + // discrete repository permissions + if (model.permissions != null && !model.canAdmin) { + List permissions = new ArrayList(); + for (Map.Entry entry : model.permissions.entrySet()) { + if (entry.getValue().exceeds(AccessPermission.NONE)) { + permissions.add(entry.getValue().asRole(entry.getKey())); + } + } + config.setStringList(USER, model.username, REPOSITORY, permissions); + } + } + + // write teams + for (TeamModel model : teams.values()) { + // team roles + List roles = new ArrayList(); + if (model.canAdmin) { + roles.add(Constants.ADMIN_ROLE); + } + if (model.canFork) { + roles.add(Constants.FORK_ROLE); + } + if (model.canCreate) { + roles.add(Constants.CREATE_ROLE); + } + if (roles.size() == 0) { + // we do this to ensure that team record is written. + // Otherwise, StoredConfig might optimizes that record away. + roles.add(Constants.NO_ROLE); + } + config.setStringList(TEAM, model.name, ROLE, roles); + + if (!model.canAdmin) { + // write team permission for non-admin teams + if (model.permissions == null) { + // null check on "final" repositories because JSON-sourced TeamModel + // can have a null repositories object + if (!ArrayUtils.isEmpty(model.repositories)) { + config.setStringList(TEAM, model.name, REPOSITORY, new ArrayList( + model.repositories)); + } + } else { + // discrete repository permissions + List permissions = new ArrayList(); + for (Map.Entry entry : model.permissions.entrySet()) { + if (entry.getValue().exceeds(AccessPermission.NONE)) { + // code:repository (e.g. RW+:~james/myrepo.git + permissions.add(entry.getValue().asRole(entry.getKey())); + } + } + config.setStringList(TEAM, model.name, REPOSITORY, permissions); + } + } + + // null check on "final" users because JSON-sourced TeamModel + // can have a null users object + if (!ArrayUtils.isEmpty(model.users)) { + config.setStringList(TEAM, model.name, USER, new ArrayList(model.users)); + } + + // null check on "final" mailing lists because JSON-sourced + // TeamModel can have a null users object + if (!ArrayUtils.isEmpty(model.mailingLists)) { + config.setStringList(TEAM, model.name, MAILINGLIST, new ArrayList( + model.mailingLists)); + } + + // null check on "final" preReceiveScripts because JSON-sourced + // TeamModel can have a null preReceiveScripts object + if (!ArrayUtils.isEmpty(model.preReceiveScripts)) { + config.setStringList(TEAM, model.name, PRERECEIVE, model.preReceiveScripts); + } + + // null check on "final" postReceiveScripts because JSON-sourced + // TeamModel can have a null postReceiveScripts object + if (!ArrayUtils.isEmpty(model.postReceiveScripts)) { + config.setStringList(TEAM, model.name, POSTRECEIVE, model.postReceiveScripts); + } + } + + config.save(); + // manually set the forceReload flag because not all JVMs support real + // millisecond resolution of lastModified. (issue-55) + forceReload = true; + + // If the write is successful, delete the current file and rename + // the temporary copy to the original filename. + if (realmFileCopy.exists() && realmFileCopy.length() > 0) { + if (realmFile.exists()) { + if (!realmFile.delete()) { + throw new IOException(MessageFormat.format("Failed to delete {0}!", + realmFile.getAbsolutePath())); + } + } + if (!realmFileCopy.renameTo(realmFile)) { + throw new IOException(MessageFormat.format("Failed to rename {0} to {1}!", + realmFileCopy.getAbsolutePath(), realmFile.getAbsolutePath())); + } + } else { + throw new IOException(MessageFormat.format("Failed to save {0}!", + realmFileCopy.getAbsolutePath())); + } + } + + /** + * Reads the realm file and rebuilds the in-memory lookup tables. + */ + protected synchronized void read() { + if (realmFile.exists() && (forceReload || (realmFile.lastModified() != lastModified))) { + forceReload = false; + lastModified = realmFile.lastModified(); + users.clear(); + cookies.clear(); + teams.clear(); + + try { + StoredConfig config = new FileBasedConfig(realmFile, FS.detect()); + config.load(); + Set usernames = config.getSubsections(USER); + for (String username : usernames) { + UserModel user = new UserModel(username.toLowerCase()); + user.password = config.getString(USER, username, PASSWORD); + user.displayName = config.getString(USER, username, DISPLAYNAME); + user.emailAddress = config.getString(USER, username, EMAILADDRESS); + user.organizationalUnit = config.getString(USER, username, ORGANIZATIONALUNIT); + user.organization = config.getString(USER, username, ORGANIZATION); + user.locality = config.getString(USER, username, LOCALITY); + user.stateProvince = config.getString(USER, username, STATEPROVINCE); + user.countryCode = config.getString(USER, username, COUNTRYCODE); + user.cookie = config.getString(USER, username, COOKIE); + if (StringUtils.isEmpty(user.cookie) && !StringUtils.isEmpty(user.password)) { + user.cookie = StringUtils.getSHA1(user.username + user.password); + } + + // user roles + Set roles = new HashSet(Arrays.asList(config.getStringList( + USER, username, ROLE))); + user.canAdmin = roles.contains(Constants.ADMIN_ROLE); + user.canFork = roles.contains(Constants.FORK_ROLE); + user.canCreate = roles.contains(Constants.CREATE_ROLE); + user.excludeFromFederation = roles.contains(Constants.NOT_FEDERATED_ROLE); + + // repository memberships + if (!user.canAdmin) { + // non-admin, read permissions + Set repositories = new HashSet(Arrays.asList(config + .getStringList(USER, username, REPOSITORY))); + for (String repository : repositories) { + user.addRepositoryPermission(repository); + } + } + + // update cache + users.put(user.username, user); + if (!StringUtils.isEmpty(user.cookie)) { + cookies.put(user.cookie, user); + } + } + + // load the teams + Set teamnames = config.getSubsections(TEAM); + for (String teamname : teamnames) { + TeamModel team = new TeamModel(teamname); + Set roles = new HashSet(Arrays.asList(config.getStringList( + TEAM, teamname, ROLE))); + team.canAdmin = roles.contains(Constants.ADMIN_ROLE); + team.canFork = roles.contains(Constants.FORK_ROLE); + team.canCreate = roles.contains(Constants.CREATE_ROLE); + + if (!team.canAdmin) { + // non-admin team, read permissions + team.addRepositoryPermissions(Arrays.asList(config.getStringList(TEAM, teamname, + REPOSITORY))); + } + team.addUsers(Arrays.asList(config.getStringList(TEAM, teamname, USER))); + team.addMailingLists(Arrays.asList(config.getStringList(TEAM, teamname, + MAILINGLIST))); + team.preReceiveScripts.addAll(Arrays.asList(config.getStringList(TEAM, + teamname, PRERECEIVE))); + team.postReceiveScripts.addAll(Arrays.asList(config.getStringList(TEAM, + teamname, POSTRECEIVE))); + + teams.put(team.name.toLowerCase(), team); + + // set the teams on the users + for (String user : team.users) { + UserModel model = users.get(user); + if (model != null) { + model.teams.add(team); + } + } + } + } catch (Exception e) { + logger.error(MessageFormat.format("Failed to read {0}", realmFile), e); + } + } + } + + protected long lastModified() { + return lastModified; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "(" + realmFile.getAbsolutePath() + ")"; + } +} diff --git a/src/com/gitblit/FileUserService.java b/src/com/gitblit/FileUserService.java index a92cb508..32c24cc4 100644 --- a/src/com/gitblit/FileUserService.java +++ b/src/com/gitblit/FileUserService.java @@ -1,1146 +1,1146 @@ -/* - * Copyright 2011 gitblit.com. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.gitblit; - -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.text.MessageFormat; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Properties; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.gitblit.Constants.AccessPermission; -import com.gitblit.models.TeamModel; -import com.gitblit.models.UserModel; -import com.gitblit.utils.ArrayUtils; -import com.gitblit.utils.DeepCopier; -import com.gitblit.utils.StringUtils; - -/** - * FileUserService is Gitblit's original default user service implementation. - * - * Users and their repository memberships are stored in a simple properties file - * which is cached and dynamically reloaded when modified. - * - * This class was deprecated in Gitblit 0.8.0 in favor of ConfigUserService - * which is still a human-readable, editable, plain-text file but it is more - * flexible for storing additional fields. - * - * @author James Moger - * - */ -@Deprecated -public class FileUserService extends FileSettings implements IUserService { - - private final Logger logger = LoggerFactory.getLogger(FileUserService.class); - - private final Map cookies = new ConcurrentHashMap(); - - private final Map teams = new ConcurrentHashMap(); - - public FileUserService(File realmFile) { - super(realmFile.getAbsolutePath()); - } - - /** - * Setup the user service. - * - * @param settings - * @since 0.7.0 - */ - @Override - public void setup(IStoredSettings settings) { - } - - /** - * Does the user service support changes to credentials? - * - * @return true or false - * @since 1.0.0 - */ - @Override - public boolean supportsCredentialChanges() { - return true; - } - - /** - * Does the user service support changes to user display name? - * - * @return true or false - * @since 1.0.0 - */ - @Override - public boolean supportsDisplayNameChanges() { - return false; - } - - /** - * Does the user service support changes to user email address? - * - * @return true or false - * @since 1.0.0 - */ - @Override - public boolean supportsEmailAddressChanges() { - return false; - } - - /** - * Does the user service support changes to team memberships? - * - * @return true or false - * @since 1.0.0 - */ - public boolean supportsTeamMembershipChanges() { - return true; - } - - /** - * Does the user service support cookie authentication? - * - * @return true or false - */ - @Override - public boolean supportsCookies() { - return true; - } - - /** - * Returns the cookie value for the specified user. - * - * @param model - * @return cookie value - */ - @Override - public String getCookie(UserModel model) { - if (!StringUtils.isEmpty(model.cookie)) { - return model.cookie; - } - Properties allUsers = super.read(); - String value = allUsers.getProperty(model.username); - String[] roles = value.split(","); - String password = roles[0]; - String cookie = StringUtils.getSHA1(model.username + password); - return cookie; - } - - /** - * Authenticate a user based on their cookie. - * - * @param cookie - * @return a user object or null - */ - @Override - public UserModel authenticate(char[] cookie) { - String hash = new String(cookie); - if (StringUtils.isEmpty(hash)) { - return null; - } - read(); - UserModel model = null; - if (cookies.containsKey(hash)) { - String username = cookies.get(hash); - model = getUserModel(username); - } - return model; - } - - /** - * Authenticate a user based on a username and password. - * - * @param username - * @param password - * @return a user object or null - */ - @Override - public UserModel authenticate(String username, char[] password) { - Properties allUsers = read(); - String userInfo = allUsers.getProperty(username); - if (StringUtils.isEmpty(userInfo)) { - return null; - } - UserModel returnedUser = null; - UserModel user = getUserModel(username); - if (user.password.startsWith(StringUtils.MD5_TYPE)) { - // password digest - String md5 = StringUtils.MD5_TYPE + StringUtils.getMD5(new String(password)); - if (user.password.equalsIgnoreCase(md5)) { - returnedUser = user; - } - } else if (user.password.startsWith(StringUtils.COMBINED_MD5_TYPE)) { - // username+password digest - String md5 = StringUtils.COMBINED_MD5_TYPE - + StringUtils.getMD5(username.toLowerCase() + new String(password)); - if (user.password.equalsIgnoreCase(md5)) { - returnedUser = user; - } - } else if (user.password.equals(new String(password))) { - // plain-text password - returnedUser = user; - } - return returnedUser; - } - - /** - * Logout a user. - * - * @param user - */ - @Override - public void logout(UserModel user) { - } - - /** - * Retrieve the user object for the specified username. - * - * @param username - * @return a user object or null - */ - @Override - public UserModel getUserModel(String username) { - Properties allUsers = read(); - String userInfo = allUsers.getProperty(username.toLowerCase()); - if (userInfo == null) { - return null; - } - UserModel model = new UserModel(username.toLowerCase()); - String[] userValues = userInfo.split(","); - model.password = userValues[0]; - for (int i = 1; i < userValues.length; i++) { - String role = userValues[i]; - switch (role.charAt(0)) { - case '#': - // Permissions - if (role.equalsIgnoreCase(Constants.ADMIN_ROLE)) { - model.canAdmin = true; - } else if (role.equalsIgnoreCase(Constants.FORK_ROLE)) { - model.canFork = true; - } else if (role.equalsIgnoreCase(Constants.CREATE_ROLE)) { - model.canCreate = true; - } else if (role.equalsIgnoreCase(Constants.NOT_FEDERATED_ROLE)) { - model.excludeFromFederation = true; - } - break; - default: - model.addRepositoryPermission(role); - } - } - // set the teams for the user - for (TeamModel team : teams.values()) { - if (team.hasUser(username)) { - model.teams.add(DeepCopier.copy(team)); - } - } - return model; - } - - /** - * Updates/writes a complete user object. - * - * @param model - * @return true if update is successful - */ - @Override - public boolean updateUserModel(UserModel model) { - return updateUserModel(model.username, model); - } - - /** - * Updates/writes all specified user objects. - * - * @param models a list of user models - * @return true if update is successful - * @since 1.2.0 - */ - @Override - public boolean updateUserModels(Collection models) { - try { - Properties allUsers = read(); - for (UserModel model : models) { - updateUserCache(allUsers, model.username, model); - } - write(allUsers); - return true; - } catch (Throwable t) { - logger.error(MessageFormat.format("Failed to update {0} user models!", models.size()), - t); - } - return false; - } - - /** - * Updates/writes and replaces a complete user object keyed by username. - * This method allows for renaming a user. - * - * @param username - * the old username - * @param model - * the user object to use for username - * @return true if update is successful - */ - @Override - public boolean updateUserModel(String username, UserModel model) { - try { - Properties allUsers = read(); - updateUserCache(allUsers, username, model); - write(allUsers); - return true; - } catch (Throwable t) { - logger.error(MessageFormat.format("Failed to update user model {0}!", model.username), - t); - } - return false; - } - - /** - * Updates/writes and replaces a complete user object keyed by username. - * This method allows for renaming a user. - * - * @param username - * the old username - * @param model - * the user object to use for username - * @return true if update is successful - */ - private boolean updateUserCache(Properties allUsers, String username, UserModel model) { - try { - UserModel oldUser = getUserModel(username); - List roles; - if (model.permissions == null) { - roles = new ArrayList(); - } else { - // discrete repository permissions - roles = new ArrayList(); - for (Map.Entry entry : model.permissions.entrySet()) { - if (entry.getValue().exceeds(AccessPermission.NONE)) { - // code:repository (e.g. RW+:~james/myrepo.git - roles.add(entry.getValue().asRole(entry.getKey())); - } - } - } - - // Permissions - if (model.canAdmin) { - roles.add(Constants.ADMIN_ROLE); - } - if (model.canFork) { - roles.add(Constants.FORK_ROLE); - } - if (model.canCreate) { - roles.add(Constants.CREATE_ROLE); - } - if (model.excludeFromFederation) { - roles.add(Constants.NOT_FEDERATED_ROLE); - } - - StringBuilder sb = new StringBuilder(); - if (!StringUtils.isEmpty(model.password)) { - sb.append(model.password); - } - sb.append(','); - for (String role : roles) { - sb.append(role); - sb.append(','); - } - // trim trailing comma - sb.setLength(sb.length() - 1); - allUsers.remove(username.toLowerCase()); - allUsers.put(model.username.toLowerCase(), sb.toString()); - - // null check on "final" teams because JSON-sourced UserModel - // can have a null teams object - if (model.teams != null) { - // update team cache - for (TeamModel team : model.teams) { - TeamModel t = getTeamModel(team.name); - if (t == null) { - // new team - t = team; - } - t.removeUser(username); - t.addUser(model.username); - updateTeamCache(allUsers, t.name, t); - } - - // check for implicit team removal - if (oldUser != null) { - for (TeamModel team : oldUser.teams) { - if (!model.isTeamMember(team.name)) { - team.removeUser(username); - updateTeamCache(allUsers, team.name, team); - } - } - } - } - return true; - } catch (Throwable t) { - logger.error(MessageFormat.format("Failed to update user model {0}!", model.username), - t); - } - return false; - } - - /** - * Deletes the user object from the user service. - * - * @param model - * @return true if successful - */ - @Override - public boolean deleteUserModel(UserModel model) { - return deleteUser(model.username); - } - - /** - * Delete the user object with the specified username - * - * @param username - * @return true if successful - */ - @Override - public boolean deleteUser(String username) { - try { - // Read realm file - Properties allUsers = read(); - UserModel user = getUserModel(username); - allUsers.remove(username); - for (TeamModel team : user.teams) { - TeamModel t = getTeamModel(team.name); - if (t == null) { - // new team - t = team; - } - t.removeUser(username); - updateTeamCache(allUsers, t.name, t); - } - write(allUsers); - return true; - } catch (Throwable t) { - logger.error(MessageFormat.format("Failed to delete user {0}!", username), t); - } - return false; - } - - /** - * Returns the list of all users available to the login service. - * - * @return list of all usernames - */ - @Override - public List getAllUsernames() { - Properties allUsers = read(); - List list = new ArrayList(); - for (String user : allUsers.stringPropertyNames()) { - if (user.charAt(0) == '@') { - // skip team user definitions - continue; - } - list.add(user); - } - Collections.sort(list); - return list; - } - - /** - * Returns the list of all users available to the login service. - * - * @return list of all usernames - */ - @Override - public List getAllUsers() { - read(); - List list = new ArrayList(); - for (String username : getAllUsernames()) { - list.add(getUserModel(username)); - } - Collections.sort(list); - return list; - } - - /** - * Returns the list of all users who are allowed to bypass the access - * restriction placed on the specified repository. - * - * @param role - * the repository name - * @return list of all usernames that can bypass the access restriction - */ - @Override - public List getUsernamesForRepositoryRole(String role) { - List list = new ArrayList(); - try { - Properties allUsers = read(); - for (String username : allUsers.stringPropertyNames()) { - if (username.charAt(0) == '@') { - continue; - } - String value = allUsers.getProperty(username); - String[] values = value.split(","); - // skip first value (password) - for (int i = 1; i < values.length; i++) { - String r = values[i]; - if (r.equalsIgnoreCase(role)) { - list.add(username); - break; - } - } - } - } catch (Throwable t) { - logger.error(MessageFormat.format("Failed to get usernames for role {0}!", role), t); - } - Collections.sort(list); - return list; - } - - /** - * Sets the list of all users who are allowed to bypass the access - * restriction placed on the specified repository. - * - * @param role - * the repository name - * @param usernames - * @return true if successful - */ - @Override - public boolean setUsernamesForRepositoryRole(String role, List usernames) { - try { - Set specifiedUsers = new HashSet(usernames); - Set needsAddRole = new HashSet(specifiedUsers); - Set needsRemoveRole = new HashSet(); - - // identify users which require add and remove role - Properties allUsers = read(); - for (String username : allUsers.stringPropertyNames()) { - String value = allUsers.getProperty(username); - String[] values = value.split(","); - // skip first value (password) - for (int i = 1; i < values.length; i++) { - String r = values[i]; - if (r.equalsIgnoreCase(role)) { - // user has role, check against revised user list - if (specifiedUsers.contains(username)) { - needsAddRole.remove(username); - } else { - // remove role from user - needsRemoveRole.add(username); - } - break; - } - } - } - - // add roles to users - for (String user : needsAddRole) { - String userValues = allUsers.getProperty(user); - userValues += "," + role; - allUsers.put(user, userValues); - } - - // remove role from user - for (String user : needsRemoveRole) { - String[] values = allUsers.getProperty(user).split(","); - String password = values[0]; - StringBuilder sb = new StringBuilder(); - sb.append(password); - sb.append(','); - - // skip first value (password) - for (int i = 1; i < values.length; i++) { - String value = values[i]; - if (!value.equalsIgnoreCase(role)) { - sb.append(value); - sb.append(','); - } - } - sb.setLength(sb.length() - 1); - - // update properties - allUsers.put(user, sb.toString()); - } - - // persist changes - write(allUsers); - return true; - } catch (Throwable t) { - logger.error(MessageFormat.format("Failed to set usernames for role {0}!", role), t); - } - return false; - } - - /** - * Renames a repository role. - * - * @param oldRole - * @param newRole - * @return true if successful - */ - @Override - public boolean renameRepositoryRole(String oldRole, String newRole) { - try { - Properties allUsers = read(); - Set needsRenameRole = new HashSet(); - - // identify users which require role rename - for (String username : allUsers.stringPropertyNames()) { - String value = allUsers.getProperty(username); - String[] roles = value.split(","); - // skip first value (password) - for (int i = 1; i < roles.length; i++) { - String repository = AccessPermission.repositoryFromRole(roles[i]); - if (repository.equalsIgnoreCase(oldRole)) { - needsRenameRole.add(username); - break; - } - } - } - - // rename role for identified users - for (String user : needsRenameRole) { - String userValues = allUsers.getProperty(user); - String[] values = userValues.split(","); - String password = values[0]; - StringBuilder sb = new StringBuilder(); - sb.append(password); - sb.append(','); - sb.append(newRole); - sb.append(','); - - // skip first value (password) - for (int i = 1; i < values.length; i++) { - String repository = AccessPermission.repositoryFromRole(values[i]); - if (repository.equalsIgnoreCase(oldRole)) { - AccessPermission permission = AccessPermission.permissionFromRole(values[i]); - sb.append(permission.asRole(newRole)); - sb.append(','); - } else { - sb.append(values[i]); - sb.append(','); - } - } - sb.setLength(sb.length() - 1); - - // update properties - allUsers.put(user, sb.toString()); - } - - // persist changes - write(allUsers); - return true; - } catch (Throwable t) { - logger.error( - MessageFormat.format("Failed to rename role {0} to {1}!", oldRole, newRole), t); - } - return false; - } - - /** - * Removes a repository role from all users. - * - * @param role - * @return true if successful - */ - @Override - public boolean deleteRepositoryRole(String role) { - try { - Properties allUsers = read(); - Set needsDeleteRole = new HashSet(); - - // identify users which require role rename - for (String username : allUsers.stringPropertyNames()) { - String value = allUsers.getProperty(username); - String[] roles = value.split(","); - // skip first value (password) - for (int i = 1; i < roles.length; i++) { - String repository = AccessPermission.repositoryFromRole(roles[i]); - if (repository.equalsIgnoreCase(role)) { - needsDeleteRole.add(username); - break; - } - } - } - - // delete role for identified users - for (String user : needsDeleteRole) { - String userValues = allUsers.getProperty(user); - String[] values = userValues.split(","); - String password = values[0]; - StringBuilder sb = new StringBuilder(); - sb.append(password); - sb.append(','); - // skip first value (password) - for (int i = 1; i < values.length; i++) { - String repository = AccessPermission.repositoryFromRole(values[i]); - if (!repository.equalsIgnoreCase(role)) { - sb.append(values[i]); - sb.append(','); - } - } - sb.setLength(sb.length() - 1); - - // update properties - allUsers.put(user, sb.toString()); - } - - // persist changes - write(allUsers); - return true; - } catch (Throwable t) { - logger.error(MessageFormat.format("Failed to delete role {0}!", role), t); - } - return false; - } - - /** - * Writes the properties file. - * - * @param properties - * @throws IOException - */ - private void write(Properties properties) throws IOException { - // Write a temporary copy of the users file - File realmFileCopy = new File(propertiesFile.getAbsolutePath() + ".tmp"); - FileWriter writer = new FileWriter(realmFileCopy); - properties - .store(writer, - " Gitblit realm file format:\n username=password,\\#permission,repository1,repository2...\n @teamname=!username1,!username2,!username3,repository1,repository2..."); - writer.close(); - // If the write is successful, delete the current file and rename - // the temporary copy to the original filename. - if (realmFileCopy.exists() && realmFileCopy.length() > 0) { - if (propertiesFile.exists()) { - if (!propertiesFile.delete()) { - throw new IOException(MessageFormat.format("Failed to delete {0}!", - propertiesFile.getAbsolutePath())); - } - } - if (!realmFileCopy.renameTo(propertiesFile)) { - throw new IOException(MessageFormat.format("Failed to rename {0} to {1}!", - realmFileCopy.getAbsolutePath(), propertiesFile.getAbsolutePath())); - } - } else { - throw new IOException(MessageFormat.format("Failed to save {0}!", - realmFileCopy.getAbsolutePath())); - } - } - - /** - * Reads the properties file and rebuilds the in-memory cookie lookup table. - */ - @Override - protected synchronized Properties read() { - long lastRead = lastModified(); - boolean reload = forceReload(); - Properties allUsers = super.read(); - if (reload || (lastRead != lastModified())) { - // reload hash cache - cookies.clear(); - teams.clear(); - - for (String username : allUsers.stringPropertyNames()) { - String value = allUsers.getProperty(username); - String[] roles = value.split(","); - if (username.charAt(0) == '@') { - // team definition - TeamModel team = new TeamModel(username.substring(1)); - List repositories = new ArrayList(); - List users = new ArrayList(); - List mailingLists = new ArrayList(); - List preReceive = new ArrayList(); - List postReceive = new ArrayList(); - for (String role : roles) { - if (role.charAt(0) == '!') { - users.add(role.substring(1)); - } else if (role.charAt(0) == '&') { - mailingLists.add(role.substring(1)); - } else if (role.charAt(0) == '^') { - preReceive.add(role.substring(1)); - } else if (role.charAt(0) == '%') { - postReceive.add(role.substring(1)); - } else { - switch (role.charAt(0)) { - case '#': - // Permissions - if (role.equalsIgnoreCase(Constants.ADMIN_ROLE)) { - team.canAdmin = true; - } else if (role.equalsIgnoreCase(Constants.FORK_ROLE)) { - team.canFork = true; - } else if (role.equalsIgnoreCase(Constants.CREATE_ROLE)) { - team.canCreate = true; - } - break; - default: - repositories.add(role); - } - repositories.add(role); - } - } - if (!team.canAdmin) { - // only read permissions for non-admin teams - team.addRepositoryPermissions(repositories); - } - team.addUsers(users); - team.addMailingLists(mailingLists); - team.preReceiveScripts.addAll(preReceive); - team.postReceiveScripts.addAll(postReceive); - teams.put(team.name.toLowerCase(), team); - } else { - // user definition - String password = roles[0]; - cookies.put(StringUtils.getSHA1(username.toLowerCase() + password), username.toLowerCase()); - } - } - } - return allUsers; - } - - @Override - public String toString() { - return getClass().getSimpleName() + "(" + propertiesFile.getAbsolutePath() + ")"; - } - - /** - * Returns the list of all teams available to the login service. - * - * @return list of all teams - * @since 0.8.0 - */ - @Override - public List getAllTeamNames() { - List list = new ArrayList(teams.keySet()); - Collections.sort(list); - return list; - } - - /** - * Returns the list of all teams available to the login service. - * - * @return list of all teams - * @since 0.8.0 - */ - @Override - public List getAllTeams() { - List list = new ArrayList(teams.values()); - list = DeepCopier.copy(list); - Collections.sort(list); - return list; - } - - /** - * Returns the list of all teams who are allowed to bypass the access - * restriction placed on the specified repository. - * - * @param role - * the repository name - * @return list of all teamnames that can bypass the access restriction - */ - @Override - public List getTeamnamesForRepositoryRole(String role) { - List list = new ArrayList(); - try { - Properties allUsers = read(); - for (String team : allUsers.stringPropertyNames()) { - if (team.charAt(0) != '@') { - // skip users - continue; - } - String value = allUsers.getProperty(team); - String[] values = value.split(","); - for (int i = 0; i < values.length; i++) { - String r = values[i]; - if (r.equalsIgnoreCase(role)) { - // strip leading @ - list.add(team.substring(1)); - break; - } - } - } - } catch (Throwable t) { - logger.error(MessageFormat.format("Failed to get teamnames for role {0}!", role), t); - } - Collections.sort(list); - return list; - } - - /** - * Sets the list of all teams who are allowed to bypass the access - * restriction placed on the specified repository. - * - * @param role - * the repository name - * @param teamnames - * @return true if successful - */ - @Override - public boolean setTeamnamesForRepositoryRole(String role, List teamnames) { - try { - Set specifiedTeams = new HashSet(teamnames); - Set needsAddRole = new HashSet(specifiedTeams); - Set needsRemoveRole = new HashSet(); - - // identify teams which require add and remove role - Properties allUsers = read(); - for (String team : allUsers.stringPropertyNames()) { - if (team.charAt(0) != '@') { - // skip users - continue; - } - String name = team.substring(1); - String value = allUsers.getProperty(team); - String[] values = value.split(","); - for (int i = 0; i < values.length; i++) { - String r = values[i]; - if (r.equalsIgnoreCase(role)) { - // team has role, check against revised team list - if (specifiedTeams.contains(name)) { - needsAddRole.remove(name); - } else { - // remove role from team - needsRemoveRole.add(name); - } - break; - } - } - } - - // add roles to teams - for (String name : needsAddRole) { - String team = "@" + name; - String teamValues = allUsers.getProperty(team); - teamValues += "," + role; - allUsers.put(team, teamValues); - } - - // remove role from team - for (String name : needsRemoveRole) { - String team = "@" + name; - String[] values = allUsers.getProperty(team).split(","); - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < values.length; i++) { - String value = values[i]; - if (!value.equalsIgnoreCase(role)) { - sb.append(value); - sb.append(','); - } - } - sb.setLength(sb.length() - 1); - - // update properties - allUsers.put(team, sb.toString()); - } - - // persist changes - write(allUsers); - return true; - } catch (Throwable t) { - logger.error(MessageFormat.format("Failed to set teamnames for role {0}!", role), t); - } - return false; - } - - /** - * Retrieve the team object for the specified team name. - * - * @param teamname - * @return a team object or null - * @since 0.8.0 - */ - @Override - public TeamModel getTeamModel(String teamname) { - read(); - TeamModel team = teams.get(teamname.toLowerCase()); - if (team != null) { - // clone the model, otherwise all changes to this object are - // live and unpersisted - team = DeepCopier.copy(team); - } - return team; - } - - /** - * Updates/writes a complete team object. - * - * @param model - * @return true if update is successful - * @since 0.8.0 - */ - @Override - public boolean updateTeamModel(TeamModel model) { - return updateTeamModel(model.name, model); - } - - /** - * Updates/writes all specified team objects. - * - * @param models a list of team models - * @return true if update is successful - * @since 1.2.0 - */ - public boolean updateTeamModels(Collection models) { - try { - Properties allUsers = read(); - for (TeamModel model : models) { - updateTeamCache(allUsers, model.name, model); - } - write(allUsers); - return true; - } catch (Throwable t) { - logger.error(MessageFormat.format("Failed to update {0} team models!", models.size()), t); - } - return false; - } - - /** - * Updates/writes and replaces a complete team object keyed by teamname. - * This method allows for renaming a team. - * - * @param teamname - * the old teamname - * @param model - * the team object to use for teamname - * @return true if update is successful - * @since 0.8.0 - */ - @Override - public boolean updateTeamModel(String teamname, TeamModel model) { - try { - Properties allUsers = read(); - updateTeamCache(allUsers, teamname, model); - write(allUsers); - return true; - } catch (Throwable t) { - logger.error(MessageFormat.format("Failed to update team model {0}!", model.name), t); - } - return false; - } - - private void updateTeamCache(Properties allUsers, String teamname, TeamModel model) { - StringBuilder sb = new StringBuilder(); - List roles; - if (model.permissions == null) { - // legacy, use repository list - if (model.repositories != null) { - roles = new ArrayList(model.repositories); - } else { - roles = new ArrayList(); - } - } else { - // discrete repository permissions - roles = new ArrayList(); - for (Map.Entry entry : model.permissions.entrySet()) { - if (entry.getValue().exceeds(AccessPermission.NONE)) { - // code:repository (e.g. RW+:~james/myrepo.git - roles.add(entry.getValue().asRole(entry.getKey())); - } - } - } - - // Permissions - if (model.canAdmin) { - roles.add(Constants.ADMIN_ROLE); - } - if (model.canFork) { - roles.add(Constants.FORK_ROLE); - } - if (model.canCreate) { - roles.add(Constants.CREATE_ROLE); - } - - for (String role : roles) { - sb.append(role); - sb.append(','); - } - - if (!ArrayUtils.isEmpty(model.users)) { - for (String user : model.users) { - sb.append('!'); - sb.append(user); - sb.append(','); - } - } - if (!ArrayUtils.isEmpty(model.mailingLists)) { - for (String address : model.mailingLists) { - sb.append('&'); - sb.append(address); - sb.append(','); - } - } - if (!ArrayUtils.isEmpty(model.preReceiveScripts)) { - for (String script : model.preReceiveScripts) { - sb.append('^'); - sb.append(script); - sb.append(','); - } - } - if (!ArrayUtils.isEmpty(model.postReceiveScripts)) { - for (String script : model.postReceiveScripts) { - sb.append('%'); - sb.append(script); - sb.append(','); - } - } - // trim trailing comma - sb.setLength(sb.length() - 1); - allUsers.remove("@" + teamname); - allUsers.put("@" + model.name, sb.toString()); - - // update team cache - teams.remove(teamname.toLowerCase()); - teams.put(model.name.toLowerCase(), model); - } - - /** - * Deletes the team object from the user service. - * - * @param model - * @return true if successful - * @since 0.8.0 - */ - @Override - public boolean deleteTeamModel(TeamModel model) { - return deleteTeam(model.name); - } - - /** - * Delete the team object with the specified teamname - * - * @param teamname - * @return true if successful - * @since 0.8.0 - */ - @Override - public boolean deleteTeam(String teamname) { - Properties allUsers = read(); - teams.remove(teamname.toLowerCase()); - allUsers.remove("@" + teamname); - try { - write(allUsers); - return true; - } catch (Throwable t) { - logger.error(MessageFormat.format("Failed to delete team {0}!", teamname), t); - } - return false; - } -} +/* + * Copyright 2011 gitblit.com. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.gitblit; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.gitblit.Constants.AccessPermission; +import com.gitblit.models.TeamModel; +import com.gitblit.models.UserModel; +import com.gitblit.utils.ArrayUtils; +import com.gitblit.utils.DeepCopier; +import com.gitblit.utils.StringUtils; + +/** + * FileUserService is Gitblit's original default user service implementation. + * + * Users and their repository memberships are stored in a simple properties file + * which is cached and dynamically reloaded when modified. + * + * This class was deprecated in Gitblit 0.8.0 in favor of ConfigUserService + * which is still a human-readable, editable, plain-text file but it is more + * flexible for storing additional fields. + * + * @author James Moger + * + */ +@Deprecated +public class FileUserService extends FileSettings implements IUserService { + + private final Logger logger = LoggerFactory.getLogger(FileUserService.class); + + private final Map cookies = new ConcurrentHashMap(); + + private final Map teams = new ConcurrentHashMap(); + + public FileUserService(File realmFile) { + super(realmFile.getAbsolutePath()); + } + + /** + * Setup the user service. + * + * @param settings + * @since 0.7.0 + */ + @Override + public void setup(IStoredSettings settings) { + } + + /** + * Does the user service support changes to credentials? + * + * @return true or false + * @since 1.0.0 + */ + @Override + public boolean supportsCredentialChanges() { + return true; + } + + /** + * Does the user service support changes to user display name? + * + * @return true or false + * @since 1.0.0 + */ + @Override + public boolean supportsDisplayNameChanges() { + return false; + } + + /** + * Does the user service support changes to user email address? + * + * @return true or false + * @since 1.0.0 + */ + @Override + public boolean supportsEmailAddressChanges() { + return false; + } + + /** + * Does the user service support changes to team memberships? + * + * @return true or false + * @since 1.0.0 + */ + public boolean supportsTeamMembershipChanges() { + return true; + } + + /** + * Does the user service support cookie authentication? + * + * @return true or false + */ + @Override + public boolean supportsCookies() { + return true; + } + + /** + * Returns the cookie value for the specified user. + * + * @param model + * @return cookie value + */ + @Override + public String getCookie(UserModel model) { + if (!StringUtils.isEmpty(model.cookie)) { + return model.cookie; + } + Properties allUsers = super.read(); + String value = allUsers.getProperty(model.username); + String[] roles = value.split(","); + String password = roles[0]; + String cookie = StringUtils.getSHA1(model.username + password); + return cookie; + } + + /** + * Authenticate a user based on their cookie. + * + * @param cookie + * @return a user object or null + */ + @Override + public UserModel authenticate(char[] cookie) { + String hash = new String(cookie); + if (StringUtils.isEmpty(hash)) { + return null; + } + read(); + UserModel model = null; + if (cookies.containsKey(hash)) { + String username = cookies.get(hash); + model = getUserModel(username); + } + return model; + } + + /** + * Authenticate a user based on a username and password. + * + * @param username + * @param password + * @return a user object or null + */ + @Override + public UserModel authenticate(String username, char[] password) { + Properties allUsers = read(); + String userInfo = allUsers.getProperty(username); + if (StringUtils.isEmpty(userInfo)) { + return null; + } + UserModel returnedUser = null; + UserModel user = getUserModel(username); + if (user.password.startsWith(StringUtils.MD5_TYPE)) { + // password digest + String md5 = StringUtils.MD5_TYPE + StringUtils.getMD5(new String(password)); + if (user.password.equalsIgnoreCase(md5)) { + returnedUser = user; + } + } else if (user.password.startsWith(StringUtils.COMBINED_MD5_TYPE)) { + // username+password digest + String md5 = StringUtils.COMBINED_MD5_TYPE + + StringUtils.getMD5(username.toLowerCase() + new String(password)); + if (user.password.equalsIgnoreCase(md5)) { + returnedUser = user; + } + } else if (user.password.equals(new String(password))) { + // plain-text password + returnedUser = user; + } + return returnedUser; + } + + /** + * Logout a user. + * + * @param user + */ + @Override + public void logout(UserModel user) { + } + + /** + * Retrieve the user object for the specified username. + * + * @param username + * @return a user object or null + */ + @Override + public UserModel getUserModel(String username) { + Properties allUsers = read(); + String userInfo = allUsers.getProperty(username.toLowerCase()); + if (userInfo == null) { + return null; + } + UserModel model = new UserModel(username.toLowerCase()); + String[] userValues = userInfo.split(","); + model.password = userValues[0]; + for (int i = 1; i < userValues.length; i++) { + String role = userValues[i]; + switch (role.charAt(0)) { + case '#': + // Permissions + if (role.equalsIgnoreCase(Constants.ADMIN_ROLE)) { + model.canAdmin = true; + } else if (role.equalsIgnoreCase(Constants.FORK_ROLE)) { + model.canFork = true; + } else if (role.equalsIgnoreCase(Constants.CREATE_ROLE)) { + model.canCreate = true; + } else if (role.equalsIgnoreCase(Constants.NOT_FEDERATED_ROLE)) { + model.excludeFromFederation = true; + } + break; + default: + model.addRepositoryPermission(role); + } + } + // set the teams for the user + for (TeamModel team : teams.values()) { + if (team.hasUser(username)) { + model.teams.add(DeepCopier.copy(team)); + } + } + return model; + } + + /** + * Updates/writes a complete user object. + * + * @param model + * @return true if update is successful + */ + @Override + public boolean updateUserModel(UserModel model) { + return updateUserModel(model.username, model); + } + + /** + * Updates/writes all specified user objects. + * + * @param models a list of user models + * @return true if update is successful + * @since 1.2.0 + */ + @Override + public boolean updateUserModels(Collection models) { + try { + Properties allUsers = read(); + for (UserModel model : models) { + updateUserCache(allUsers, model.username, model); + } + write(allUsers); + return true; + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to update {0} user models!", models.size()), + t); + } + return false; + } + + /** + * Updates/writes and replaces a complete user object keyed by username. + * This method allows for renaming a user. + * + * @param username + * the old username + * @param model + * the user object to use for username + * @return true if update is successful + */ + @Override + public boolean updateUserModel(String username, UserModel model) { + try { + Properties allUsers = read(); + updateUserCache(allUsers, username, model); + write(allUsers); + return true; + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to update user model {0}!", model.username), + t); + } + return false; + } + + /** + * Updates/writes and replaces a complete user object keyed by username. + * This method allows for renaming a user. + * + * @param username + * the old username + * @param model + * the user object to use for username + * @return true if update is successful + */ + private boolean updateUserCache(Properties allUsers, String username, UserModel model) { + try { + UserModel oldUser = getUserModel(username); + List roles; + if (model.permissions == null) { + roles = new ArrayList(); + } else { + // discrete repository permissions + roles = new ArrayList(); + for (Map.Entry entry : model.permissions.entrySet()) { + if (entry.getValue().exceeds(AccessPermission.NONE)) { + // code:repository (e.g. RW+:~james/myrepo.git + roles.add(entry.getValue().asRole(entry.getKey())); + } + } + } + + // Permissions + if (model.canAdmin) { + roles.add(Constants.ADMIN_ROLE); + } + if (model.canFork) { + roles.add(Constants.FORK_ROLE); + } + if (model.canCreate) { + roles.add(Constants.CREATE_ROLE); + } + if (model.excludeFromFederation) { + roles.add(Constants.NOT_FEDERATED_ROLE); + } + + StringBuilder sb = new StringBuilder(); + if (!StringUtils.isEmpty(model.password)) { + sb.append(model.password); + } + sb.append(','); + for (String role : roles) { + sb.append(role); + sb.append(','); + } + // trim trailing comma + sb.setLength(sb.length() - 1); + allUsers.remove(username.toLowerCase()); + allUsers.put(model.username.toLowerCase(), sb.toString()); + + // null check on "final" teams because JSON-sourced UserModel + // can have a null teams object + if (model.teams != null) { + // update team cache + for (TeamModel team : model.teams) { + TeamModel t = getTeamModel(team.name); + if (t == null) { + // new team + t = team; + } + t.removeUser(username); + t.addUser(model.username); + updateTeamCache(allUsers, t.name, t); + } + + // check for implicit team removal + if (oldUser != null) { + for (TeamModel team : oldUser.teams) { + if (!model.isTeamMember(team.name)) { + team.removeUser(username); + updateTeamCache(allUsers, team.name, team); + } + } + } + } + return true; + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to update user model {0}!", model.username), + t); + } + return false; + } + + /** + * Deletes the user object from the user service. + * + * @param model + * @return true if successful + */ + @Override + public boolean deleteUserModel(UserModel model) { + return deleteUser(model.username); + } + + /** + * Delete the user object with the specified username + * + * @param username + * @return true if successful + */ + @Override + public boolean deleteUser(String username) { + try { + // Read realm file + Properties allUsers = read(); + UserModel user = getUserModel(username); + allUsers.remove(username); + for (TeamModel team : user.teams) { + TeamModel t = getTeamModel(team.name); + if (t == null) { + // new team + t = team; + } + t.removeUser(username); + updateTeamCache(allUsers, t.name, t); + } + write(allUsers); + return true; + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to delete user {0}!", username), t); + } + return false; + } + + /** + * Returns the list of all users available to the login service. + * + * @return list of all usernames + */ + @Override + public List getAllUsernames() { + Properties allUsers = read(); + List list = new ArrayList(); + for (String user : allUsers.stringPropertyNames()) { + if (user.charAt(0) == '@') { + // skip team user definitions + continue; + } + list.add(user); + } + Collections.sort(list); + return list; + } + + /** + * Returns the list of all users available to the login service. + * + * @return list of all usernames + */ + @Override + public List getAllUsers() { + read(); + List list = new ArrayList(); + for (String username : getAllUsernames()) { + list.add(getUserModel(username)); + } + Collections.sort(list); + return list; + } + + /** + * Returns the list of all users who are allowed to bypass the access + * restriction placed on the specified repository. + * + * @param role + * the repository name + * @return list of all usernames that can bypass the access restriction + */ + @Override + public List getUsernamesForRepositoryRole(String role) { + List list = new ArrayList(); + try { + Properties allUsers = read(); + for (String username : allUsers.stringPropertyNames()) { + if (username.charAt(0) == '@') { + continue; + } + String value = allUsers.getProperty(username); + String[] values = value.split(","); + // skip first value (password) + for (int i = 1; i < values.length; i++) { + String r = values[i]; + if (r.equalsIgnoreCase(role)) { + list.add(username); + break; + } + } + } + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to get usernames for role {0}!", role), t); + } + Collections.sort(list); + return list; + } + + /** + * Sets the list of all users who are allowed to bypass the access + * restriction placed on the specified repository. + * + * @param role + * the repository name + * @param usernames + * @return true if successful + */ + @Override + public boolean setUsernamesForRepositoryRole(String role, List usernames) { + try { + Set specifiedUsers = new HashSet(usernames); + Set needsAddRole = new HashSet(specifiedUsers); + Set needsRemoveRole = new HashSet(); + + // identify users which require add and remove role + Properties allUsers = read(); + for (String username : allUsers.stringPropertyNames()) { + String value = allUsers.getProperty(username); + String[] values = value.split(","); + // skip first value (password) + for (int i = 1; i < values.length; i++) { + String r = values[i]; + if (r.equalsIgnoreCase(role)) { + // user has role, check against revised user list + if (specifiedUsers.contains(username)) { + needsAddRole.remove(username); + } else { + // remove role from user + needsRemoveRole.add(username); + } + break; + } + } + } + + // add roles to users + for (String user : needsAddRole) { + String userValues = allUsers.getProperty(user); + userValues += "," + role; + allUsers.put(user, userValues); + } + + // remove role from user + for (String user : needsRemoveRole) { + String[] values = allUsers.getProperty(user).split(","); + String password = values[0]; + StringBuilder sb = new StringBuilder(); + sb.append(password); + sb.append(','); + + // skip first value (password) + for (int i = 1; i < values.length; i++) { + String value = values[i]; + if (!value.equalsIgnoreCase(role)) { + sb.append(value); + sb.append(','); + } + } + sb.setLength(sb.length() - 1); + + // update properties + allUsers.put(user, sb.toString()); + } + + // persist changes + write(allUsers); + return true; + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to set usernames for role {0}!", role), t); + } + return false; + } + + /** + * Renames a repository role. + * + * @param oldRole + * @param newRole + * @return true if successful + */ + @Override + public boolean renameRepositoryRole(String oldRole, String newRole) { + try { + Properties allUsers = read(); + Set needsRenameRole = new HashSet(); + + // identify users which require role rename + for (String username : allUsers.stringPropertyNames()) { + String value = allUsers.getProperty(username); + String[] roles = value.split(","); + // skip first value (password) + for (int i = 1; i < roles.length; i++) { + String repository = AccessPermission.repositoryFromRole(roles[i]); + if (repository.equalsIgnoreCase(oldRole)) { + needsRenameRole.add(username); + break; + } + } + } + + // rename role for identified users + for (String user : needsRenameRole) { + String userValues = allUsers.getProperty(user); + String[] values = userValues.split(","); + String password = values[0]; + StringBuilder sb = new StringBuilder(); + sb.append(password); + sb.append(','); + sb.append(newRole); + sb.append(','); + + // skip first value (password) + for (int i = 1; i < values.length; i++) { + String repository = AccessPermission.repositoryFromRole(values[i]); + if (repository.equalsIgnoreCase(oldRole)) { + AccessPermission permission = AccessPermission.permissionFromRole(values[i]); + sb.append(permission.asRole(newRole)); + sb.append(','); + } else { + sb.append(values[i]); + sb.append(','); + } + } + sb.setLength(sb.length() - 1); + + // update properties + allUsers.put(user, sb.toString()); + } + + // persist changes + write(allUsers); + return true; + } catch (Throwable t) { + logger.error( + MessageFormat.format("Failed to rename role {0} to {1}!", oldRole, newRole), t); + } + return false; + } + + /** + * Removes a repository role from all users. + * + * @param role + * @return true if successful + */ + @Override + public boolean deleteRepositoryRole(String role) { + try { + Properties allUsers = read(); + Set needsDeleteRole = new HashSet(); + + // identify users which require role rename + for (String username : allUsers.stringPropertyNames()) { + String value = allUsers.getProperty(username); + String[] roles = value.split(","); + // skip first value (password) + for (int i = 1; i < roles.length; i++) { + String repository = AccessPermission.repositoryFromRole(roles[i]); + if (repository.equalsIgnoreCase(role)) { + needsDeleteRole.add(username); + break; + } + } + } + + // delete role for identified users + for (String user : needsDeleteRole) { + String userValues = allUsers.getProperty(user); + String[] values = userValues.split(","); + String password = values[0]; + StringBuilder sb = new StringBuilder(); + sb.append(password); + sb.append(','); + // skip first value (password) + for (int i = 1; i < values.length; i++) { + String repository = AccessPermission.repositoryFromRole(values[i]); + if (!repository.equalsIgnoreCase(role)) { + sb.append(values[i]); + sb.append(','); + } + } + sb.setLength(sb.length() - 1); + + // update properties + allUsers.put(user, sb.toString()); + } + + // persist changes + write(allUsers); + return true; + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to delete role {0}!", role), t); + } + return false; + } + + /** + * Writes the properties file. + * + * @param properties + * @throws IOException + */ + private void write(Properties properties) throws IOException { + // Write a temporary copy of the users file + File realmFileCopy = new File(propertiesFile.getAbsolutePath() + ".tmp"); + FileWriter writer = new FileWriter(realmFileCopy); + properties + .store(writer, + " Gitblit realm file format:\n username=password,\\#permission,repository1,repository2...\n @teamname=!username1,!username2,!username3,repository1,repository2..."); + writer.close(); + // If the write is successful, delete the current file and rename + // the temporary copy to the original filename. + if (realmFileCopy.exists() && realmFileCopy.length() > 0) { + if (propertiesFile.exists()) { + if (!propertiesFile.delete()) { + throw new IOException(MessageFormat.format("Failed to delete {0}!", + propertiesFile.getAbsolutePath())); + } + } + if (!realmFileCopy.renameTo(propertiesFile)) { + throw new IOException(MessageFormat.format("Failed to rename {0} to {1}!", + realmFileCopy.getAbsolutePath(), propertiesFile.getAbsolutePath())); + } + } else { + throw new IOException(MessageFormat.format("Failed to save {0}!", + realmFileCopy.getAbsolutePath())); + } + } + + /** + * Reads the properties file and rebuilds the in-memory cookie lookup table. + */ + @Override + protected synchronized Properties read() { + long lastRead = lastModified(); + boolean reload = forceReload(); + Properties allUsers = super.read(); + if (reload || (lastRead != lastModified())) { + // reload hash cache + cookies.clear(); + teams.clear(); + + for (String username : allUsers.stringPropertyNames()) { + String value = allUsers.getProperty(username); + String[] roles = value.split(","); + if (username.charAt(0) == '@') { + // team definition + TeamModel team = new TeamModel(username.substring(1)); + List repositories = new ArrayList(); + List users = new ArrayList(); + List mailingLists = new ArrayList(); + List preReceive = new ArrayList(); + List postReceive = new ArrayList(); + for (String role : roles) { + if (role.charAt(0) == '!') { + users.add(role.substring(1)); + } else if (role.charAt(0) == '&') { + mailingLists.add(role.substring(1)); + } else if (role.charAt(0) == '^') { + preReceive.add(role.substring(1)); + } else if (role.charAt(0) == '%') { + postReceive.add(role.substring(1)); + } else { + switch (role.charAt(0)) { + case '#': + // Permissions + if (role.equalsIgnoreCase(Constants.ADMIN_ROLE)) { + team.canAdmin = true; + } else if (role.equalsIgnoreCase(Constants.FORK_ROLE)) { + team.canFork = true; + } else if (role.equalsIgnoreCase(Constants.CREATE_ROLE)) { + team.canCreate = true; + } + break; + default: + repositories.add(role); + } + repositories.add(role); + } + } + if (!team.canAdmin) { + // only read permissions for non-admin teams + team.addRepositoryPermissions(repositories); + } + team.addUsers(users); + team.addMailingLists(mailingLists); + team.preReceiveScripts.addAll(preReceive); + team.postReceiveScripts.addAll(postReceive); + teams.put(team.name.toLowerCase(), team); + } else { + // user definition + String password = roles[0]; + cookies.put(StringUtils.getSHA1(username.toLowerCase() + password), username.toLowerCase()); + } + } + } + return allUsers; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "(" + propertiesFile.getAbsolutePath() + ")"; + } + + /** + * Returns the list of all teams available to the login service. + * + * @return list of all teams + * @since 0.8.0 + */ + @Override + public List getAllTeamNames() { + List list = new ArrayList(teams.keySet()); + Collections.sort(list); + return list; + } + + /** + * Returns the list of all teams available to the login service. + * + * @return list of all teams + * @since 0.8.0 + */ + @Override + public List getAllTeams() { + List list = new ArrayList(teams.values()); + list = DeepCopier.copy(list); + Collections.sort(list); + return list; + } + + /** + * Returns the list of all teams who are allowed to bypass the access + * restriction placed on the specified repository. + * + * @param role + * the repository name + * @return list of all teamnames that can bypass the access restriction + */ + @Override + public List getTeamnamesForRepositoryRole(String role) { + List list = new ArrayList(); + try { + Properties allUsers = read(); + for (String team : allUsers.stringPropertyNames()) { + if (team.charAt(0) != '@') { + // skip users + continue; + } + String value = allUsers.getProperty(team); + String[] values = value.split(","); + for (int i = 0; i < values.length; i++) { + String r = values[i]; + if (r.equalsIgnoreCase(role)) { + // strip leading @ + list.add(team.substring(1)); + break; + } + } + } + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to get teamnames for role {0}!", role), t); + } + Collections.sort(list); + return list; + } + + /** + * Sets the list of all teams who are allowed to bypass the access + * restriction placed on the specified repository. + * + * @param role + * the repository name + * @param teamnames + * @return true if successful + */ + @Override + public boolean setTeamnamesForRepositoryRole(String role, List teamnames) { + try { + Set specifiedTeams = new HashSet(teamnames); + Set needsAddRole = new HashSet(specifiedTeams); + Set needsRemoveRole = new HashSet(); + + // identify teams which require add and remove role + Properties allUsers = read(); + for (String team : allUsers.stringPropertyNames()) { + if (team.charAt(0) != '@') { + // skip users + continue; + } + String name = team.substring(1); + String value = allUsers.getProperty(team); + String[] values = value.split(","); + for (int i = 0; i < values.length; i++) { + String r = values[i]; + if (r.equalsIgnoreCase(role)) { + // team has role, check against revised team list + if (specifiedTeams.contains(name)) { + needsAddRole.remove(name); + } else { + // remove role from team + needsRemoveRole.add(name); + } + break; + } + } + } + + // add roles to teams + for (String name : needsAddRole) { + String team = "@" + name; + String teamValues = allUsers.getProperty(team); + teamValues += "," + role; + allUsers.put(team, teamValues); + } + + // remove role from team + for (String name : needsRemoveRole) { + String team = "@" + name; + String[] values = allUsers.getProperty(team).split(","); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < values.length; i++) { + String value = values[i]; + if (!value.equalsIgnoreCase(role)) { + sb.append(value); + sb.append(','); + } + } + sb.setLength(sb.length() - 1); + + // update properties + allUsers.put(team, sb.toString()); + } + + // persist changes + write(allUsers); + return true; + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to set teamnames for role {0}!", role), t); + } + return false; + } + + /** + * Retrieve the team object for the specified team name. + * + * @param teamname + * @return a team object or null + * @since 0.8.0 + */ + @Override + public TeamModel getTeamModel(String teamname) { + read(); + TeamModel team = teams.get(teamname.toLowerCase()); + if (team != null) { + // clone the model, otherwise all changes to this object are + // live and unpersisted + team = DeepCopier.copy(team); + } + return team; + } + + /** + * Updates/writes a complete team object. + * + * @param model + * @return true if update is successful + * @since 0.8.0 + */ + @Override + public boolean updateTeamModel(TeamModel model) { + return updateTeamModel(model.name, model); + } + + /** + * Updates/writes all specified team objects. + * + * @param models a list of team models + * @return true if update is successful + * @since 1.2.0 + */ + public boolean updateTeamModels(Collection models) { + try { + Properties allUsers = read(); + for (TeamModel model : models) { + updateTeamCache(allUsers, model.name, model); + } + write(allUsers); + return true; + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to update {0} team models!", models.size()), t); + } + return false; + } + + /** + * Updates/writes and replaces a complete team object keyed by teamname. + * This method allows for renaming a team. + * + * @param teamname + * the old teamname + * @param model + * the team object to use for teamname + * @return true if update is successful + * @since 0.8.0 + */ + @Override + public boolean updateTeamModel(String teamname, TeamModel model) { + try { + Properties allUsers = read(); + updateTeamCache(allUsers, teamname, model); + write(allUsers); + return true; + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to update team model {0}!", model.name), t); + } + return false; + } + + private void updateTeamCache(Properties allUsers, String teamname, TeamModel model) { + StringBuilder sb = new StringBuilder(); + List roles; + if (model.permissions == null) { + // legacy, use repository list + if (model.repositories != null) { + roles = new ArrayList(model.repositories); + } else { + roles = new ArrayList(); + } + } else { + // discrete repository permissions + roles = new ArrayList(); + for (Map.Entry entry : model.permissions.entrySet()) { + if (entry.getValue().exceeds(AccessPermission.NONE)) { + // code:repository (e.g. RW+:~james/myrepo.git + roles.add(entry.getValue().asRole(entry.getKey())); + } + } + } + + // Permissions + if (model.canAdmin) { + roles.add(Constants.ADMIN_ROLE); + } + if (model.canFork) { + roles.add(Constants.FORK_ROLE); + } + if (model.canCreate) { + roles.add(Constants.CREATE_ROLE); + } + + for (String role : roles) { + sb.append(role); + sb.append(','); + } + + if (!ArrayUtils.isEmpty(model.users)) { + for (String user : model.users) { + sb.append('!'); + sb.append(user); + sb.append(','); + } + } + if (!ArrayUtils.isEmpty(model.mailingLists)) { + for (String address : model.mailingLists) { + sb.append('&'); + sb.append(address); + sb.append(','); + } + } + if (!ArrayUtils.isEmpty(model.preReceiveScripts)) { + for (String script : model.preReceiveScripts) { + sb.append('^'); + sb.append(script); + sb.append(','); + } + } + if (!ArrayUtils.isEmpty(model.postReceiveScripts)) { + for (String script : model.postReceiveScripts) { + sb.append('%'); + sb.append(script); + sb.append(','); + } + } + // trim trailing comma + sb.setLength(sb.length() - 1); + allUsers.remove("@" + teamname); + allUsers.put("@" + model.name, sb.toString()); + + // update team cache + teams.remove(teamname.toLowerCase()); + teams.put(model.name.toLowerCase(), model); + } + + /** + * Deletes the team object from the user service. + * + * @param model + * @return true if successful + * @since 0.8.0 + */ + @Override + public boolean deleteTeamModel(TeamModel model) { + return deleteTeam(model.name); + } + + /** + * Delete the team object with the specified teamname + * + * @param teamname + * @return true if successful + * @since 0.8.0 + */ + @Override + public boolean deleteTeam(String teamname) { + Properties allUsers = read(); + teams.remove(teamname.toLowerCase()); + allUsers.remove("@" + teamname); + try { + write(allUsers); + return true; + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to delete team {0}!", teamname), t); + } + return false; + } +} diff --git a/src/com/gitblit/GitblitUserService.java b/src/com/gitblit/GitblitUserService.java index 543b8cc8..16d01b92 100644 --- a/src/com/gitblit/GitblitUserService.java +++ b/src/com/gitblit/GitblitUserService.java @@ -1,304 +1,304 @@ -/* - * Copyright 2011 gitblit.com. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.gitblit; - -import java.io.File; -import java.io.IOException; -import java.text.MessageFormat; -import java.util.Collection; -import java.util.List; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.gitblit.models.TeamModel; -import com.gitblit.models.UserModel; -import com.gitblit.utils.DeepCopier; - -/** - * This class wraps the default user service and is recommended as the starting - * point for custom user service implementations. - * - * This does seem a little convoluted, but the idea is to allow IUserService to - * evolve with new methods and implementations without breaking custom - * authentication implementations. - * - * The most common implementation of a custom IUserService is to only override - * authentication and then delegate all other functionality to one of Gitblit's - * user services. This class optimizes that use-case. - * - * Extending GitblitUserService allows for authentication customization without - * having to keep-up-with IUSerService API changes. - * - * @author James Moger - * - */ -public class GitblitUserService implements IUserService { - - protected IUserService serviceImpl; - - private final Logger logger = LoggerFactory.getLogger(GitblitUserService.class); - - public GitblitUserService() { - } - - @Override - public void setup(IStoredSettings settings) { - File realmFile = GitBlit.getFileOrFolder(Keys.realm.userService, "users.conf"); - serviceImpl = createUserService(realmFile); - logger.info("GUS delegating to " + serviceImpl.toString()); - } - - @SuppressWarnings("deprecation") - protected IUserService createUserService(File realmFile) { - IUserService service = null; - if (realmFile.getName().toLowerCase().endsWith(".properties")) { - // v0.5.0 - v0.7.0 properties-based realm file - service = new FileUserService(realmFile); - } else if (realmFile.getName().toLowerCase().endsWith(".conf")) { - // v0.8.0+ config-based realm file - service = new ConfigUserService(realmFile); - } - - assert service != null; - - if (!realmFile.exists()) { - // Create the Administrator account for a new realm file - try { - realmFile.createNewFile(); - } catch (IOException x) { - logger.error(MessageFormat.format("COULD NOT CREATE REALM FILE {0}!", realmFile), x); - } - UserModel admin = new UserModel("admin"); - admin.password = "admin"; - admin.canAdmin = true; - admin.excludeFromFederation = true; - service.updateUserModel(admin); - } - - if (service instanceof FileUserService) { - // automatically create a users.conf realm file from the original - // users.properties file - File usersConfig = new File(realmFile.getParentFile(), "users.conf"); - if (!usersConfig.exists()) { - logger.info(MessageFormat.format("Automatically creating {0} based on {1}", - usersConfig.getAbsolutePath(), realmFile.getAbsolutePath())); - ConfigUserService configService = new ConfigUserService(usersConfig); - for (String username : service.getAllUsernames()) { - UserModel userModel = service.getUserModel(username); - configService.updateUserModel(userModel); - } - } - // issue suggestion about switching to users.conf - logger.warn("Please consider using \"users.conf\" instead of the deprecated \"users.properties\" file"); - } - return service; - } - - @Override - public String toString() { - return getClass().getSimpleName(); - } - - @Override - public boolean supportsCredentialChanges() { - return serviceImpl.supportsCredentialChanges(); - } - - @Override - public boolean supportsDisplayNameChanges() { - return serviceImpl.supportsDisplayNameChanges(); - } - - @Override - public boolean supportsEmailAddressChanges() { - return serviceImpl.supportsEmailAddressChanges(); - } - - @Override - public boolean supportsTeamMembershipChanges() { - return serviceImpl.supportsTeamMembershipChanges(); - } - - @Override - public boolean supportsCookies() { - return serviceImpl.supportsCookies(); - } - - @Override - public String getCookie(UserModel model) { - return serviceImpl.getCookie(model); - } - - @Override - public UserModel authenticate(char[] cookie) { - return serviceImpl.authenticate(cookie); - } - - @Override - public UserModel authenticate(String username, char[] password) { - return serviceImpl.authenticate(username, password); - } - - @Override - public void logout(UserModel user) { - serviceImpl.logout(user); - } - - @Override - public UserModel getUserModel(String username) { - return serviceImpl.getUserModel(username); - } - - @Override - public boolean updateUserModel(UserModel model) { - return serviceImpl.updateUserModel(model); - } - - @Override - public boolean updateUserModels(Collection models) { - return serviceImpl.updateUserModels(models); - } - - @Override - public boolean updateUserModel(String username, UserModel model) { - if (supportsCredentialChanges()) { - if (!supportsTeamMembershipChanges()) { - // teams are externally controlled - copy from original model - UserModel existingModel = getUserModel(username); - - model = DeepCopier.copy(model); - model.teams.clear(); - model.teams.addAll(existingModel.teams); - } - return serviceImpl.updateUserModel(username, model); - } - if (model.username.equals(username)) { - // passwords are not persisted by the backing user service - model.password = null; - if (!supportsTeamMembershipChanges()) { - // teams are externally controlled- copy from original model - UserModel existingModel = getUserModel(username); - - model = DeepCopier.copy(model); - model.teams.clear(); - model.teams.addAll(existingModel.teams); - } - return serviceImpl.updateUserModel(username, model); - } - logger.error("Users can not be renamed!"); - return false; - } - @Override - public boolean deleteUserModel(UserModel model) { - return serviceImpl.deleteUserModel(model); - } - - @Override - public boolean deleteUser(String username) { - return serviceImpl.deleteUser(username); - } - - @Override - public List getAllUsernames() { - return serviceImpl.getAllUsernames(); - } - - @Override - public List getAllUsers() { - return serviceImpl.getAllUsers(); - } - - @Override - public List getAllTeamNames() { - return serviceImpl.getAllTeamNames(); - } - - @Override - public List getAllTeams() { - return serviceImpl.getAllTeams(); - } - - @Override - public List getTeamnamesForRepositoryRole(String role) { - return serviceImpl.getTeamnamesForRepositoryRole(role); - } - - @Override - @Deprecated - public boolean setTeamnamesForRepositoryRole(String role, List teamnames) { - return serviceImpl.setTeamnamesForRepositoryRole(role, teamnames); - } - - @Override - public TeamModel getTeamModel(String teamname) { - return serviceImpl.getTeamModel(teamname); - } - - @Override - public boolean updateTeamModel(TeamModel model) { - return serviceImpl.updateTeamModel(model); - } - - @Override - public boolean updateTeamModels(Collection models) { - return serviceImpl.updateTeamModels(models); - } - - @Override - public boolean updateTeamModel(String teamname, TeamModel model) { - if (!supportsTeamMembershipChanges()) { - // teams are externally controlled - copy from original model - TeamModel existingModel = getTeamModel(teamname); - - model = DeepCopier.copy(model); - model.users.clear(); - model.users.addAll(existingModel.users); - } - return serviceImpl.updateTeamModel(teamname, model); - } - - @Override - public boolean deleteTeamModel(TeamModel model) { - return serviceImpl.deleteTeamModel(model); - } - - @Override - public boolean deleteTeam(String teamname) { - return serviceImpl.deleteTeam(teamname); - } - - @Override - public List getUsernamesForRepositoryRole(String role) { - return serviceImpl.getUsernamesForRepositoryRole(role); - } - - @Override - @Deprecated - public boolean setUsernamesForRepositoryRole(String role, List usernames) { - return serviceImpl.setUsernamesForRepositoryRole(role, usernames); - } - - @Override - public boolean renameRepositoryRole(String oldRole, String newRole) { - return serviceImpl.renameRepositoryRole(oldRole, newRole); - } - - @Override - public boolean deleteRepositoryRole(String role) { - return serviceImpl.deleteRepositoryRole(role); - } -} +/* + * Copyright 2011 gitblit.com. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.gitblit; + +import java.io.File; +import java.io.IOException; +import java.text.MessageFormat; +import java.util.Collection; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.gitblit.models.TeamModel; +import com.gitblit.models.UserModel; +import com.gitblit.utils.DeepCopier; + +/** + * This class wraps the default user service and is recommended as the starting + * point for custom user service implementations. + * + * This does seem a little convoluted, but the idea is to allow IUserService to + * evolve with new methods and implementations without breaking custom + * authentication implementations. + * + * The most common implementation of a custom IUserService is to only override + * authentication and then delegate all other functionality to one of Gitblit's + * user services. This class optimizes that use-case. + * + * Extending GitblitUserService allows for authentication customization without + * having to keep-up-with IUSerService API changes. + * + * @author James Moger + * + */ +public class GitblitUserService implements IUserService { + + protected IUserService serviceImpl; + + private final Logger logger = LoggerFactory.getLogger(GitblitUserService.class); + + public GitblitUserService() { + } + + @Override + public void setup(IStoredSettings settings) { + File realmFile = GitBlit.getFileOrFolder(Keys.realm.userService, "users.conf"); + serviceImpl = createUserService(realmFile); + logger.info("GUS delegating to " + serviceImpl.toString()); + } + + @SuppressWarnings("deprecation") + protected IUserService createUserService(File realmFile) { + IUserService service = null; + if (realmFile.getName().toLowerCase().endsWith(".properties")) { + // v0.5.0 - v0.7.0 properties-based realm file + service = new FileUserService(realmFile); + } else if (realmFile.getName().toLowerCase().endsWith(".conf")) { + // v0.8.0+ config-based realm file + service = new ConfigUserService(realmFile); + } + + assert service != null; + + if (!realmFile.exists()) { + // Create the Administrator account for a new realm file + try { + realmFile.createNewFile(); + } catch (IOException x) { + logger.error(MessageFormat.format("COULD NOT CREATE REALM FILE {0}!", realmFile), x); + } + UserModel admin = new UserModel("admin"); + admin.password = "admin"; + admin.canAdmin = true; + admin.excludeFromFederation = true; + service.updateUserModel(admin); + } + + if (service instanceof FileUserService) { + // automatically create a users.conf realm file from the original + // users.properties file + File usersConfig = new File(realmFile.getParentFile(), "users.conf"); + if (!usersConfig.exists()) { + logger.info(MessageFormat.format("Automatically creating {0} based on {1}", + usersConfig.getAbsolutePath(), realmFile.getAbsolutePath())); + ConfigUserService configService = new ConfigUserService(usersConfig); + for (String username : service.getAllUsernames()) { + UserModel userModel = service.getUserModel(username); + configService.updateUserModel(userModel); + } + } + // issue suggestion about switching to users.conf + logger.warn("Please consider using \"users.conf\" instead of the deprecated \"users.properties\" file"); + } + return service; + } + + @Override + public String toString() { + return getClass().getSimpleName(); + } + + @Override + public boolean supportsCredentialChanges() { + return serviceImpl.supportsCredentialChanges(); + } + + @Override + public boolean supportsDisplayNameChanges() { + return serviceImpl.supportsDisplayNameChanges(); + } + + @Override + public boolean supportsEmailAddressChanges() { + return serviceImpl.supportsEmailAddressChanges(); + } + + @Override + public boolean supportsTeamMembershipChanges() { + return serviceImpl.supportsTeamMembershipChanges(); + } + + @Override + public boolean supportsCookies() { + return serviceImpl.supportsCookies(); + } + + @Override + public String getCookie(UserModel model) { + return serviceImpl.getCookie(model); + } + + @Override + public UserModel authenticate(char[] cookie) { + return serviceImpl.authenticate(cookie); + } + + @Override + public UserModel authenticate(String username, char[] password) { + return serviceImpl.authenticate(username, password); + } + + @Override + public void logout(UserModel user) { + serviceImpl.logout(user); + } + + @Override + public UserModel getUserModel(String username) { + return serviceImpl.getUserModel(username); + } + + @Override + public boolean updateUserModel(UserModel model) { + return serviceImpl.updateUserModel(model); + } + + @Override + public boolean updateUserModels(Collection models) { + return serviceImpl.updateUserModels(models); + } + + @Override + public boolean updateUserModel(String username, UserModel model) { + if (supportsCredentialChanges()) { + if (!supportsTeamMembershipChanges()) { + // teams are externally controlled - copy from original model + UserModel existingModel = getUserModel(username); + + model = DeepCopier.copy(model); + model.teams.clear(); + model.teams.addAll(existingModel.teams); + } + return serviceImpl.updateUserModel(username, model); + } + if (model.username.equals(username)) { + // passwords are not persisted by the backing user service + model.password = null; + if (!supportsTeamMembershipChanges()) { + // teams are externally controlled- copy from original model + UserModel existingModel = getUserModel(username); + + model = DeepCopier.copy(model); + model.teams.clear(); + model.teams.addAll(existingModel.teams); + } + return serviceImpl.updateUserModel(username, model); + } + logger.error("Users can not be renamed!"); + return false; + } + @Override + public boolean deleteUserModel(UserModel model) { + return serviceImpl.deleteUserModel(model); + } + + @Override + public boolean deleteUser(String username) { + return serviceImpl.deleteUser(username); + } + + @Override + public List getAllUsernames() { + return serviceImpl.getAllUsernames(); + } + + @Override + public List getAllUsers() { + return serviceImpl.getAllUsers(); + } + + @Override + public List getAllTeamNames() { + return serviceImpl.getAllTeamNames(); + } + + @Override + public List getAllTeams() { + return serviceImpl.getAllTeams(); + } + + @Override + public List getTeamnamesForRepositoryRole(String role) { + return serviceImpl.getTeamnamesForRepositoryRole(role); + } + + @Override + @Deprecated + public boolean setTeamnamesForRepositoryRole(String role, List teamnames) { + return serviceImpl.setTeamnamesForRepositoryRole(role, teamnames); + } + + @Override + public TeamModel getTeamModel(String teamname) { + return serviceImpl.getTeamModel(teamname); + } + + @Override + public boolean updateTeamModel(TeamModel model) { + return serviceImpl.updateTeamModel(model); + } + + @Override + public boolean updateTeamModels(Collection models) { + return serviceImpl.updateTeamModels(models); + } + + @Override + public boolean updateTeamModel(String teamname, TeamModel model) { + if (!supportsTeamMembershipChanges()) { + // teams are externally controlled - copy from original model + TeamModel existingModel = getTeamModel(teamname); + + model = DeepCopier.copy(model); + model.users.clear(); + model.users.addAll(existingModel.users); + } + return serviceImpl.updateTeamModel(teamname, model); + } + + @Override + public boolean deleteTeamModel(TeamModel model) { + return serviceImpl.deleteTeamModel(model); + } + + @Override + public boolean deleteTeam(String teamname) { + return serviceImpl.deleteTeam(teamname); + } + + @Override + public List getUsernamesForRepositoryRole(String role) { + return serviceImpl.getUsernamesForRepositoryRole(role); + } + + @Override + @Deprecated + public boolean setUsernamesForRepositoryRole(String role, List usernames) { + return serviceImpl.setUsernamesForRepositoryRole(role, usernames); + } + + @Override + public boolean renameRepositoryRole(String oldRole, String newRole) { + return serviceImpl.renameRepositoryRole(oldRole, newRole); + } + + @Override + public boolean deleteRepositoryRole(String role) { + return serviceImpl.deleteRepositoryRole(role); + } +} diff --git a/src/com/gitblit/IUserService.java b/src/com/gitblit/IUserService.java index 6a3a3baf..a57b0da6 100644 --- a/src/com/gitblit/IUserService.java +++ b/src/com/gitblit/IUserService.java @@ -1,325 +1,325 @@ -/* - * Copyright 2011 gitblit.com. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.gitblit; - -import java.util.Collection; -import java.util.List; - -import com.gitblit.models.TeamModel; -import com.gitblit.models.UserModel; - -/** - * Implementations of IUserService control all aspects of UserModel objects and - * user authentication. - * - * @author James Moger - * - */ -public interface IUserService { - - /** - * Setup the user service. This method allows custom implementations to - * retrieve settings from gitblit.properties or the web.xml file without - * relying on the GitBlit static singleton. - * - * @param settings - * @since 0.7.0 - */ - void setup(IStoredSettings settings); - - /** - * Does the user service support changes to credentials? - * - * @return true or false - * @since 1.0.0 - */ - boolean supportsCredentialChanges(); - - /** - * Does the user service support changes to user display name? - * - * @return true or false - * @since 1.0.0 - */ - boolean supportsDisplayNameChanges(); - - /** - * Does the user service support changes to user email address? - * - * @return true or false - * @since 1.0.0 - */ - boolean supportsEmailAddressChanges(); - - /** - * Does the user service support changes to team memberships? - * - * @return true or false - * @since 1.0.0 - */ - boolean supportsTeamMembershipChanges(); - - /** - * Does the user service support cookie authentication? - * - * @return true or false - */ - boolean supportsCookies(); - - /** - * Returns the cookie value for the specified user. - * - * @param model - * @return cookie value - */ - String getCookie(UserModel model); - - /** - * Authenticate a user based on their cookie. - * - * @param cookie - * @return a user object or null - */ - UserModel authenticate(char[] cookie); - - /** - * Authenticate a user based on a username and password. - * - * @param username - * @param password - * @return a user object or null - */ - UserModel authenticate(String username, char[] password); - - /** - * Logout a user. - * - * @param user - */ - void logout(UserModel user); - - /** - * Retrieve the user object for the specified username. - * - * @param username - * @return a user object or null - */ - UserModel getUserModel(String username); - - /** - * Updates/writes a complete user object. - * - * @param model - * @return true if update is successful - */ - boolean updateUserModel(UserModel model); - - /** - * Updates/writes all specified user objects. - * - * @param models a list of user models - * @return true if update is successful - * @since 1.2.0 - */ - boolean updateUserModels(Collection models); - - /** - * Adds/updates a user object keyed by username. This method allows for - * renaming a user. - * - * @param username - * the old username - * @param model - * the user object to use for username - * @return true if update is successful - */ - boolean updateUserModel(String username, UserModel model); - - /** - * Deletes the user object from the user service. - * - * @param model - * @return true if successful - */ - boolean deleteUserModel(UserModel model); - - /** - * Delete the user object with the specified username - * - * @param username - * @return true if successful - */ - boolean deleteUser(String username); - - /** - * Returns the list of all users available to the login service. - * - * @return list of all usernames - */ - List getAllUsernames(); - - /** - * Returns the list of all users available to the login service. - * - * @return list of all users - * @since 0.8.0 - */ - List getAllUsers(); - - /** - * Returns the list of all teams available to the login service. - * - * @return list of all teams - * @since 0.8.0 - */ - List getAllTeamNames(); - - /** - * Returns the list of all teams available to the login service. - * - * @return list of all teams - * @since 0.8.0 - */ - List getAllTeams(); - - /** - * Returns the list of all users who are allowed to bypass the access - * restriction placed on the specified repository. - * - * @param role - * the repository name - * @return list of all usernames that can bypass the access restriction - * @since 0.8.0 - */ - List getTeamnamesForRepositoryRole(String role); - - /** - * Sets the list of all teams who are allowed to bypass the access - * restriction placed on the specified repository. - * - * @param role - * the repository name - * @param teamnames - * @return true if successful - * @since 0.8.0 - */ - @Deprecated - boolean setTeamnamesForRepositoryRole(String role, List teamnames); - - /** - * Retrieve the team object for the specified team name. - * - * @param teamname - * @return a team object or null - * @since 0.8.0 - */ - TeamModel getTeamModel(String teamname); - - /** - * Updates/writes a complete team object. - * - * @param model - * @return true if update is successful - * @since 0.8.0 - */ - boolean updateTeamModel(TeamModel model); - - /** - * Updates/writes all specified team objects. - * - * @param models a list of team models - * @return true if update is successful - * @since 1.2.0 - */ - boolean updateTeamModels(Collection models); - - /** - * Updates/writes and replaces a complete team object keyed by teamname. - * This method allows for renaming a team. - * - * @param teamname - * the old teamname - * @param model - * the team object to use for teamname - * @return true if update is successful - * @since 0.8.0 - */ - boolean updateTeamModel(String teamname, TeamModel model); - - /** - * Deletes the team object from the user service. - * - * @param model - * @return true if successful - * @since 0.8.0 - */ - boolean deleteTeamModel(TeamModel model); - - /** - * Delete the team object with the specified teamname - * - * @param teamname - * @return true if successful - * @since 0.8.0 - */ - boolean deleteTeam(String teamname); - - /** - * Returns the list of all users who are allowed to bypass the access - * restriction placed on the specified repository. - * - * @param role - * the repository name - * @return list of all usernames that can bypass the access restriction - * @since 0.8.0 - */ - List getUsernamesForRepositoryRole(String role); - - /** - * Sets the list of all uses who are allowed to bypass the access - * restriction placed on the specified repository. - * - * @param role - * the repository name - * @param usernames - * @return true if successful - */ - @Deprecated - boolean setUsernamesForRepositoryRole(String role, List usernames); - - /** - * Renames a repository role. - * - * @param oldRole - * @param newRole - * @return true if successful - */ - boolean renameRepositoryRole(String oldRole, String newRole); - - /** - * Removes a repository role from all users. - * - * @param role - * @return true if successful - */ - boolean deleteRepositoryRole(String role); - - /** - * @See java.lang.Object.toString(); - * @return string representation of the login service - */ - String toString(); -} +/* + * Copyright 2011 gitblit.com. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.gitblit; + +import java.util.Collection; +import java.util.List; + +import com.gitblit.models.TeamModel; +import com.gitblit.models.UserModel; + +/** + * Implementations of IUserService control all aspects of UserModel objects and + * user authentication. + * + * @author James Moger + * + */ +public interface IUserService { + + /** + * Setup the user service. This method allows custom implementations to + * retrieve settings from gitblit.properties or the web.xml file without + * relying on the GitBlit static singleton. + * + * @param settings + * @since 0.7.0 + */ + void setup(IStoredSettings settings); + + /** + * Does the user service support changes to credentials? + * + * @return true or false + * @since 1.0.0 + */ + boolean supportsCredentialChanges(); + + /** + * Does the user service support changes to user display name? + * + * @return true or false + * @since 1.0.0 + */ + boolean supportsDisplayNameChanges(); + + /** + * Does the user service support changes to user email address? + * + * @return true or false + * @since 1.0.0 + */ + boolean supportsEmailAddressChanges(); + + /** + * Does the user service support changes to team memberships? + * + * @return true or false + * @since 1.0.0 + */ + boolean supportsTeamMembershipChanges(); + + /** + * Does the user service support cookie authentication? + * + * @return true or false + */ + boolean supportsCookies(); + + /** + * Returns the cookie value for the specified user. + * + * @param model + * @return cookie value + */ + String getCookie(UserModel model); + + /** + * Authenticate a user based on their cookie. + * + * @param cookie + * @return a user object or null + */ + UserModel authenticate(char[] cookie); + + /** + * Authenticate a user based on a username and password. + * + * @param username + * @param password + * @return a user object or null + */ + UserModel authenticate(String username, char[] password); + + /** + * Logout a user. + * + * @param user + */ + void logout(UserModel user); + + /** + * Retrieve the user object for the specified username. + * + * @param username + * @return a user object or null + */ + UserModel getUserModel(String username); + + /** + * Updates/writes a complete user object. + * + * @param model + * @return true if update is successful + */ + boolean updateUserModel(UserModel model); + + /** + * Updates/writes all specified user objects. + * + * @param models a list of user models + * @return true if update is successful + * @since 1.2.0 + */ + boolean updateUserModels(Collection models); + + /** + * Adds/updates a user object keyed by username. This method allows for + * renaming a user. + * + * @param username + * the old username + * @param model + * the user object to use for username + * @return true if update is successful + */ + boolean updateUserModel(String username, UserModel model); + + /** + * Deletes the user object from the user service. + * + * @param model + * @return true if successful + */ + boolean deleteUserModel(UserModel model); + + /** + * Delete the user object with the specified username + * + * @param username + * @return true if successful + */ + boolean deleteUser(String username); + + /** + * Returns the list of all users available to the login service. + * + * @return list of all usernames + */ + List getAllUsernames(); + + /** + * Returns the list of all users available to the login service. + * + * @return list of all users + * @since 0.8.0 + */ + List getAllUsers(); + + /** + * Returns the list of all teams available to the login service. + * + * @return list of all teams + * @since 0.8.0 + */ + List getAllTeamNames(); + + /** + * Returns the list of all teams available to the login service. + * + * @return list of all teams + * @since 0.8.0 + */ + List getAllTeams(); + + /** + * Returns the list of all users who are allowed to bypass the access + * restriction placed on the specified repository. + * + * @param role + * the repository name + * @return list of all usernames that can bypass the access restriction + * @since 0.8.0 + */ + List getTeamnamesForRepositoryRole(String role); + + /** + * Sets the list of all teams who are allowed to bypass the access + * restriction placed on the specified repository. + * + * @param role + * the repository name + * @param teamnames + * @return true if successful + * @since 0.8.0 + */ + @Deprecated + boolean setTeamnamesForRepositoryRole(String role, List teamnames); + + /** + * Retrieve the team object for the specified team name. + * + * @param teamname + * @return a team object or null + * @since 0.8.0 + */ + TeamModel getTeamModel(String teamname); + + /** + * Updates/writes a complete team object. + * + * @param model + * @return true if update is successful + * @since 0.8.0 + */ + boolean updateTeamModel(TeamModel model); + + /** + * Updates/writes all specified team objects. + * + * @param models a list of team models + * @return true if update is successful + * @since 1.2.0 + */ + boolean updateTeamModels(Collection models); + + /** + * Updates/writes and replaces a complete team object keyed by teamname. + * This method allows for renaming a team. + * + * @param teamname + * the old teamname + * @param model + * the team object to use for teamname + * @return true if update is successful + * @since 0.8.0 + */ + boolean updateTeamModel(String teamname, TeamModel model); + + /** + * Deletes the team object from the user service. + * + * @param model + * @return true if successful + * @since 0.8.0 + */ + boolean deleteTeamModel(TeamModel model); + + /** + * Delete the team object with the specified teamname + * + * @param teamname + * @return true if successful + * @since 0.8.0 + */ + boolean deleteTeam(String teamname); + + /** + * Returns the list of all users who are allowed to bypass the access + * restriction placed on the specified repository. + * + * @param role + * the repository name + * @return list of all usernames that can bypass the access restriction + * @since 0.8.0 + */ + List getUsernamesForRepositoryRole(String role); + + /** + * Sets the list of all uses who are allowed to bypass the access + * restriction placed on the specified repository. + * + * @param role + * the repository name + * @param usernames + * @return true if successful + */ + @Deprecated + boolean setUsernamesForRepositoryRole(String role, List usernames); + + /** + * Renames a repository role. + * + * @param oldRole + * @param newRole + * @return true if successful + */ + boolean renameRepositoryRole(String oldRole, String newRole); + + /** + * Removes a repository role from all users. + * + * @param role + * @return true if successful + */ + boolean deleteRepositoryRole(String role); + + /** + * @See java.lang.Object.toString(); + * @return string representation of the login service + */ + String toString(); +} diff --git a/src/com/gitblit/LdapUserService.java b/src/com/gitblit/LdapUserService.java index ddfbaa40..f1533040 100644 --- a/src/com/gitblit/LdapUserService.java +++ b/src/com/gitblit/LdapUserService.java @@ -1,487 +1,487 @@ -/* - * Copyright 2012 John Crygier - * Copyright 2012 gitblit.com - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.gitblit; - -import java.io.File; -import java.net.URI; -import java.net.URISyntaxException; -import java.security.GeneralSecurityException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.TimeUnit; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.gitblit.models.TeamModel; -import com.gitblit.models.UserModel; -import com.gitblit.utils.ArrayUtils; -import com.gitblit.utils.StringUtils; -import com.unboundid.ldap.sdk.Attribute; -import com.unboundid.ldap.sdk.ExtendedResult; -import com.unboundid.ldap.sdk.LDAPConnection; -import com.unboundid.ldap.sdk.LDAPException; -import com.unboundid.ldap.sdk.LDAPSearchException; -import com.unboundid.ldap.sdk.ResultCode; -import com.unboundid.ldap.sdk.SearchResult; -import com.unboundid.ldap.sdk.SearchResultEntry; -import com.unboundid.ldap.sdk.SearchScope; -import com.unboundid.ldap.sdk.extensions.StartTLSExtendedRequest; -import com.unboundid.util.ssl.SSLUtil; -import com.unboundid.util.ssl.TrustAllTrustManager; - -/** - * Implementation of an LDAP user service. - * - * @author John Crygier - */ -public class LdapUserService extends GitblitUserService { - - public static final Logger logger = LoggerFactory.getLogger(LdapUserService.class); - public static final String LDAP_PASSWORD_KEY = "StoredInLDAP"; - - private IStoredSettings settings; - private long lastLdapUserSyncTs = 0L; - private long ldapSyncCachePeriod; - - public LdapUserService() { - super(); - } - - private void initializeLdapCaches() { - final String cacheDuration = settings.getString(Keys.realm.ldap.ldapCachePeriod, "2 MINUTES"); - final long duration; - final TimeUnit timeUnit; - try { - final String[] s = cacheDuration.split(" ", 2); - duration = Long.parseLong(s[0]); - timeUnit = TimeUnit.valueOf(s[1]); - ldapSyncCachePeriod = timeUnit.toMillis(duration); - } catch (RuntimeException ex) { - throw new IllegalArgumentException(Keys.realm.ldap.ldapCachePeriod + " must have format ' ' where is one of 'MILLISECONDS', 'SECONDS', 'MINUTES', 'HOURS', 'DAYS'"); - } - } - - @Override - public void setup(IStoredSettings settings) { - this.settings = settings; - String file = settings.getString(Keys.realm.ldap.backingUserService, "users.conf"); - File realmFile = GitBlit.getFileOrFolder(file); - - initializeLdapCaches(); - - serviceImpl = createUserService(realmFile); - logger.info("LDAP User Service backed by " + serviceImpl.toString()); - - synchronizeLdapUsers(); - } - - protected synchronized void synchronizeLdapUsers() { - final boolean enabled = settings.getBoolean(Keys.realm.ldap.synchronizeUsers.enable, false); - if (enabled) { - if (lastLdapUserSyncTs + ldapSyncCachePeriod < System.currentTimeMillis()) { - final boolean deleteRemovedLdapUsers = settings.getBoolean(Keys.realm.ldap.synchronizeUsers.removeDeleted, true); - LDAPConnection ldapConnection = getLdapConnection(); - if (ldapConnection != null) { - try { - String accountBase = settings.getString(Keys.realm.ldap.accountBase, ""); - String uidAttribute = settings.getString(Keys.realm.ldap.uid, "uid"); - String accountPattern = settings.getString(Keys.realm.ldap.accountPattern, "(&(objectClass=person)(sAMAccountName=${username}))"); - accountPattern = StringUtils.replace(accountPattern, "${username}", "*"); - - SearchResult result = doSearch(ldapConnection, accountBase, accountPattern); - if (result != null && result.getEntryCount() > 0) { - final Map ldapUsers = new HashMap(); - - for (SearchResultEntry loggingInUser : result.getSearchEntries()) { - - final String username = loggingInUser.getAttribute(uidAttribute).getValue(); - logger.debug("LDAP synchronizing: " + username); - - UserModel user = getUserModel(username); - if (user == null) { - user = new UserModel(username); - } - - if (!supportsTeamMembershipChanges()) - getTeamsFromLdap(ldapConnection, username, loggingInUser, user); - - // Get User Attributes - setUserAttributes(user, loggingInUser); - - // store in map - ldapUsers.put(username, user); - } - - if (deleteRemovedLdapUsers) { - logger.debug("detecting removed LDAP users..."); - - for (UserModel userModel : super.getAllUsers()) { - if (LDAP_PASSWORD_KEY.equals(userModel.password)) { - if (! ldapUsers.containsKey(userModel.username)) { - logger.info("deleting removed LDAP user " + userModel.username + " from backing user service"); - super.deleteUser(userModel.username); - } - } - } - } - - super.updateUserModels(ldapUsers.values()); - - if (!supportsTeamMembershipChanges()) { - final Map userTeams = new HashMap(); - for (UserModel user : ldapUsers.values()) { - for (TeamModel userTeam : user.teams) { - userTeams.put(userTeam.name, userTeam); - } - } - updateTeamModels(userTeams.values()); - } - } - lastLdapUserSyncTs = System.currentTimeMillis(); - } finally { - ldapConnection.close(); - } - } - } - } - } - - private LDAPConnection getLdapConnection() { - try { - URI ldapUrl = new URI(settings.getRequiredString(Keys.realm.ldap.server)); - String bindUserName = settings.getString(Keys.realm.ldap.username, ""); - String bindPassword = settings.getString(Keys.realm.ldap.password, ""); - int ldapPort = ldapUrl.getPort(); - - if (ldapUrl.getScheme().equalsIgnoreCase("ldaps")) { // SSL - if (ldapPort == -1) // Default Port - ldapPort = 636; - - SSLUtil sslUtil = new SSLUtil(new TrustAllTrustManager()); - return new LDAPConnection(sslUtil.createSSLSocketFactory(), ldapUrl.getHost(), ldapPort, bindUserName, bindPassword); - } else { - if (ldapPort == -1) // Default Port - ldapPort = 389; - - LDAPConnection conn = new LDAPConnection(ldapUrl.getHost(), ldapPort, bindUserName, bindPassword); - - if (ldapUrl.getScheme().equalsIgnoreCase("ldap+tls")) { - SSLUtil sslUtil = new SSLUtil(new TrustAllTrustManager()); - - ExtendedResult extendedResult = conn.processExtendedOperation( - new StartTLSExtendedRequest(sslUtil.createSSLContext())); - - if (extendedResult.getResultCode() != ResultCode.SUCCESS) { - throw new LDAPException(extendedResult.getResultCode()); - } - } - return conn; - } - } catch (URISyntaxException e) { - logger.error("Bad LDAP URL, should be in the form: ldap(s|+tls)://:", e); - } catch (GeneralSecurityException e) { - logger.error("Unable to create SSL Connection", e); - } catch (LDAPException e) { - logger.error("Error Connecting to LDAP", e); - } - - return null; - } - - /** - * Credentials are defined in the LDAP server and can not be manipulated - * from Gitblit. - * - * @return false - * @since 1.0.0 - */ - @Override - public boolean supportsCredentialChanges() { - return false; - } - - /** - * If no displayName pattern is defined then Gitblit can manage the display name. - * - * @return true if Gitblit can manage the user display name - * @since 1.0.0 - */ - @Override - public boolean supportsDisplayNameChanges() { - return StringUtils.isEmpty(settings.getString(Keys.realm.ldap.displayName, "")); - } - - /** - * If no email pattern is defined then Gitblit can manage the email address. - * - * @return true if Gitblit can manage the user email address - * @since 1.0.0 - */ - @Override - public boolean supportsEmailAddressChanges() { - return StringUtils.isEmpty(settings.getString(Keys.realm.ldap.email, "")); - } - - - /** - * If the LDAP server will maintain team memberships then LdapUserService - * will not allow team membership changes. In this scenario all team - * changes must be made on the LDAP server by the LDAP administrator. - * - * @return true or false - * @since 1.0.0 - */ - public boolean supportsTeamMembershipChanges() { - return !settings.getBoolean(Keys.realm.ldap.maintainTeams, false); - } - - @Override - public UserModel authenticate(String username, char[] password) { - String simpleUsername = getSimpleUsername(username); - - LDAPConnection ldapConnection = getLdapConnection(); - if (ldapConnection != null) { - try { - // Find the logging in user's DN - String accountBase = settings.getString(Keys.realm.ldap.accountBase, ""); - String accountPattern = settings.getString(Keys.realm.ldap.accountPattern, "(&(objectClass=person)(sAMAccountName=${username}))"); - accountPattern = StringUtils.replace(accountPattern, "${username}", escapeLDAPSearchFilter(simpleUsername)); - - SearchResult result = doSearch(ldapConnection, accountBase, accountPattern); - if (result != null && result.getEntryCount() == 1) { - SearchResultEntry loggingInUser = result.getSearchEntries().get(0); - String loggingInUserDN = loggingInUser.getDN(); - - if (isAuthenticated(ldapConnection, loggingInUserDN, new String(password))) { - logger.debug("LDAP authenticated: " + username); - - UserModel user = getUserModel(simpleUsername); - if (user == null) // create user object for new authenticated user - user = new UserModel(simpleUsername); - - // create a user cookie - if (StringUtils.isEmpty(user.cookie) && !ArrayUtils.isEmpty(password)) { - user.cookie = StringUtils.getSHA1(user.username + new String(password)); - } - - if (!supportsTeamMembershipChanges()) - getTeamsFromLdap(ldapConnection, simpleUsername, loggingInUser, user); - - // Get User Attributes - setUserAttributes(user, loggingInUser); - - // Push the ldap looked up values to backing file - super.updateUserModel(user); - if (!supportsTeamMembershipChanges()) { - for (TeamModel userTeam : user.teams) - updateTeamModel(userTeam); - } - - return user; - } - } - } finally { - ldapConnection.close(); - } - } - return null; - } - - /** - * Set the admin attribute from team memberships retrieved from LDAP. - * If we are not storing teams in LDAP and/or we have not defined any - * administrator teams, then do not change the admin flag. - * - * @param user - */ - private void setAdminAttribute(UserModel user) { - if (!supportsTeamMembershipChanges()) { - List admins = settings.getStrings(Keys.realm.ldap.admins); - // if we have defined administrative teams, then set admin flag - // otherwise leave admin flag unchanged - if (!ArrayUtils.isEmpty(admins)) { - user.canAdmin = false; - for (String admin : admins) { - if (admin.startsWith("@")) { // Team - if (user.getTeam(admin.substring(1)) != null) - user.canAdmin = true; - logger.debug("user "+ user.username+" has administrative rights"); - } else - if (user.getName().equalsIgnoreCase(admin)) - user.canAdmin = true; - } - } - } - } - - private void setUserAttributes(UserModel user, SearchResultEntry userEntry) { - // Is this user an admin? - setAdminAttribute(user); - - // Don't want visibility into the real password, make up a dummy - user.password = LDAP_PASSWORD_KEY; - - // Get full name Attribute - String displayName = settings.getString(Keys.realm.ldap.displayName, ""); - if (!StringUtils.isEmpty(displayName)) { - // Replace embedded ${} with attributes - if (displayName.contains("${")) { - for (Attribute userAttribute : userEntry.getAttributes()) - displayName = StringUtils.replace(displayName, "${" + userAttribute.getName() + "}", userAttribute.getValue()); - - user.displayName = displayName; - } else { - Attribute attribute = userEntry.getAttribute(displayName); - if (attribute != null && attribute.hasValue()) { - user.displayName = attribute.getValue(); - } - } - } - - // Get email address Attribute - String email = settings.getString(Keys.realm.ldap.email, ""); - if (!StringUtils.isEmpty(email)) { - if (email.contains("${")) { - for (Attribute userAttribute : userEntry.getAttributes()) - email = StringUtils.replace(email, "${" + userAttribute.getName() + "}", userAttribute.getValue()); - - user.emailAddress = email; - } else { - Attribute attribute = userEntry.getAttribute(email); - if (attribute != null && attribute.hasValue()) { - user.emailAddress = attribute.getValue(); - } - } - } - } - - private void getTeamsFromLdap(LDAPConnection ldapConnection, String simpleUsername, SearchResultEntry loggingInUser, UserModel user) { - String loggingInUserDN = loggingInUser.getDN(); - - user.teams.clear(); // Clear the users team memberships - we're going to get them from LDAP - String groupBase = settings.getString(Keys.realm.ldap.groupBase, ""); - String groupMemberPattern = settings.getString(Keys.realm.ldap.groupMemberPattern, "(&(objectClass=group)(member=${dn}))"); - - groupMemberPattern = StringUtils.replace(groupMemberPattern, "${dn}", escapeLDAPSearchFilter(loggingInUserDN)); - groupMemberPattern = StringUtils.replace(groupMemberPattern, "${username}", escapeLDAPSearchFilter(simpleUsername)); - - // Fill in attributes into groupMemberPattern - for (Attribute userAttribute : loggingInUser.getAttributes()) - groupMemberPattern = StringUtils.replace(groupMemberPattern, "${" + userAttribute.getName() + "}", escapeLDAPSearchFilter(userAttribute.getValue())); - - SearchResult teamMembershipResult = doSearch(ldapConnection, groupBase, groupMemberPattern); - if (teamMembershipResult != null && teamMembershipResult.getEntryCount() > 0) { - for (int i = 0; i < teamMembershipResult.getEntryCount(); i++) { - SearchResultEntry teamEntry = teamMembershipResult.getSearchEntries().get(i); - String teamName = teamEntry.getAttribute("cn").getValue(); - - TeamModel teamModel = getTeamModel(teamName); - if (teamModel == null) - teamModel = createTeamFromLdap(teamEntry); - - user.teams.add(teamModel); - teamModel.addUser(user.getName()); - } - } - } - - private TeamModel createTeamFromLdap(SearchResultEntry teamEntry) { - TeamModel answer = new TeamModel(teamEntry.getAttributeValue("cn")); - // potentially retrieve other attributes here in the future - - return answer; - } - - private SearchResult doSearch(LDAPConnection ldapConnection, String base, String filter) { - try { - return ldapConnection.search(base, SearchScope.SUB, filter); - } catch (LDAPSearchException e) { - logger.error("Problem Searching LDAP", e); - - return null; - } - } - - private boolean isAuthenticated(LDAPConnection ldapConnection, String userDn, String password) { - try { - // Binding will stop any LDAP-Injection Attacks since the searched-for user needs to bind to that DN - ldapConnection.bind(userDn, password); - return true; - } catch (LDAPException e) { - logger.error("Error authenticating user", e); - return false; - } - } - - - @Override - public List getAllUsernames() { - synchronizeLdapUsers(); - return super.getAllUsernames(); - } - - @Override - public List getAllUsers() { - synchronizeLdapUsers(); - return super.getAllUsers(); - } - - /** - * Returns a simple username without any domain prefixes. - * - * @param username - * @return a simple username - */ - protected String getSimpleUsername(String username) { - int lastSlash = username.lastIndexOf('\\'); - if (lastSlash > -1) { - username = username.substring(lastSlash + 1); - } - - return username; - } - - // From: https://www.owasp.org/index.php/Preventing_LDAP_Injection_in_Java - public static final String escapeLDAPSearchFilter(String filter) { - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < filter.length(); i++) { - char curChar = filter.charAt(i); - switch (curChar) { - case '\\': - sb.append("\\5c"); - break; - case '*': - sb.append("\\2a"); - break; - case '(': - sb.append("\\28"); - break; - case ')': - sb.append("\\29"); - break; - case '\u0000': - sb.append("\\00"); - break; - default: - sb.append(curChar); - } - } - return sb.toString(); - } -} +/* + * Copyright 2012 John Crygier + * Copyright 2012 gitblit.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.gitblit; + +import java.io.File; +import java.net.URI; +import java.net.URISyntaxException; +import java.security.GeneralSecurityException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.gitblit.models.TeamModel; +import com.gitblit.models.UserModel; +import com.gitblit.utils.ArrayUtils; +import com.gitblit.utils.StringUtils; +import com.unboundid.ldap.sdk.Attribute; +import com.unboundid.ldap.sdk.ExtendedResult; +import com.unboundid.ldap.sdk.LDAPConnection; +import com.unboundid.ldap.sdk.LDAPException; +import com.unboundid.ldap.sdk.LDAPSearchException; +import com.unboundid.ldap.sdk.ResultCode; +import com.unboundid.ldap.sdk.SearchResult; +import com.unboundid.ldap.sdk.SearchResultEntry; +import com.unboundid.ldap.sdk.SearchScope; +import com.unboundid.ldap.sdk.extensions.StartTLSExtendedRequest; +import com.unboundid.util.ssl.SSLUtil; +import com.unboundid.util.ssl.TrustAllTrustManager; + +/** + * Implementation of an LDAP user service. + * + * @author John Crygier + */ +public class LdapUserService extends GitblitUserService { + + public static final Logger logger = LoggerFactory.getLogger(LdapUserService.class); + public static final String LDAP_PASSWORD_KEY = "StoredInLDAP"; + + private IStoredSettings settings; + private long lastLdapUserSyncTs = 0L; + private long ldapSyncCachePeriod; + + public LdapUserService() { + super(); + } + + private void initializeLdapCaches() { + final String cacheDuration = settings.getString(Keys.realm.ldap.ldapCachePeriod, "2 MINUTES"); + final long duration; + final TimeUnit timeUnit; + try { + final String[] s = cacheDuration.split(" ", 2); + duration = Long.parseLong(s[0]); + timeUnit = TimeUnit.valueOf(s[1]); + ldapSyncCachePeriod = timeUnit.toMillis(duration); + } catch (RuntimeException ex) { + throw new IllegalArgumentException(Keys.realm.ldap.ldapCachePeriod + " must have format ' ' where is one of 'MILLISECONDS', 'SECONDS', 'MINUTES', 'HOURS', 'DAYS'"); + } + } + + @Override + public void setup(IStoredSettings settings) { + this.settings = settings; + String file = settings.getString(Keys.realm.ldap.backingUserService, "users.conf"); + File realmFile = GitBlit.getFileOrFolder(file); + + initializeLdapCaches(); + + serviceImpl = createUserService(realmFile); + logger.info("LDAP User Service backed by " + serviceImpl.toString()); + + synchronizeLdapUsers(); + } + + protected synchronized void synchronizeLdapUsers() { + final boolean enabled = settings.getBoolean(Keys.realm.ldap.synchronizeUsers.enable, false); + if (enabled) { + if (lastLdapUserSyncTs + ldapSyncCachePeriod < System.currentTimeMillis()) { + final boolean deleteRemovedLdapUsers = settings.getBoolean(Keys.realm.ldap.synchronizeUsers.removeDeleted, true); + LDAPConnection ldapConnection = getLdapConnection(); + if (ldapConnection != null) { + try { + String accountBase = settings.getString(Keys.realm.ldap.accountBase, ""); + String uidAttribute = settings.getString(Keys.realm.ldap.uid, "uid"); + String accountPattern = settings.getString(Keys.realm.ldap.accountPattern, "(&(objectClass=person)(sAMAccountName=${username}))"); + accountPattern = StringUtils.replace(accountPattern, "${username}", "*"); + + SearchResult result = doSearch(ldapConnection, accountBase, accountPattern); + if (result != null && result.getEntryCount() > 0) { + final Map ldapUsers = new HashMap(); + + for (SearchResultEntry loggingInUser : result.getSearchEntries()) { + + final String username = loggingInUser.getAttribute(uidAttribute).getValue(); + logger.debug("LDAP synchronizing: " + username); + + UserModel user = getUserModel(username); + if (user == null) { + user = new UserModel(username); + } + + if (!supportsTeamMembershipChanges()) + getTeamsFromLdap(ldapConnection, username, loggingInUser, user); + + // Get User Attributes + setUserAttributes(user, loggingInUser); + + // store in map + ldapUsers.put(username, user); + } + + if (deleteRemovedLdapUsers) { + logger.debug("detecting removed LDAP users..."); + + for (UserModel userModel : super.getAllUsers()) { + if (LDAP_PASSWORD_KEY.equals(userModel.password)) { + if (! ldapUsers.containsKey(userModel.username)) { + logger.info("deleting removed LDAP user " + userModel.username + " from backing user service"); + super.deleteUser(userModel.username); + } + } + } + } + + super.updateUserModels(ldapUsers.values()); + + if (!supportsTeamMembershipChanges()) { + final Map userTeams = new HashMap(); + for (UserModel user : ldapUsers.values()) { + for (TeamModel userTeam : user.teams) { + userTeams.put(userTeam.name, userTeam); + } + } + updateTeamModels(userTeams.values()); + } + } + lastLdapUserSyncTs = System.currentTimeMillis(); + } finally { + ldapConnection.close(); + } + } + } + } + } + + private LDAPConnection getLdapConnection() { + try { + URI ldapUrl = new URI(settings.getRequiredString(Keys.realm.ldap.server)); + String bindUserName = settings.getString(Keys.realm.ldap.username, ""); + String bindPassword = settings.getString(Keys.realm.ldap.password, ""); + int ldapPort = ldapUrl.getPort(); + + if (ldapUrl.getScheme().equalsIgnoreCase("ldaps")) { // SSL + if (ldapPort == -1) // Default Port + ldapPort = 636; + + SSLUtil sslUtil = new SSLUtil(new TrustAllTrustManager()); + return new LDAPConnection(sslUtil.createSSLSocketFactory(), ldapUrl.getHost(), ldapPort, bindUserName, bindPassword); + } else { + if (ldapPort == -1) // Default Port + ldapPort = 389; + + LDAPConnection conn = new LDAPConnection(ldapUrl.getHost(), ldapPort, bindUserName, bindPassword); + + if (ldapUrl.getScheme().equalsIgnoreCase("ldap+tls")) { + SSLUtil sslUtil = new SSLUtil(new TrustAllTrustManager()); + + ExtendedResult extendedResult = conn.processExtendedOperation( + new StartTLSExtendedRequest(sslUtil.createSSLContext())); + + if (extendedResult.getResultCode() != ResultCode.SUCCESS) { + throw new LDAPException(extendedResult.getResultCode()); + } + } + return conn; + } + } catch (URISyntaxException e) { + logger.error("Bad LDAP URL, should be in the form: ldap(s|+tls)://:", e); + } catch (GeneralSecurityException e) { + logger.error("Unable to create SSL Connection", e); + } catch (LDAPException e) { + logger.error("Error Connecting to LDAP", e); + } + + return null; + } + + /** + * Credentials are defined in the LDAP server and can not be manipulated + * from Gitblit. + * + * @return false + * @since 1.0.0 + */ + @Override + public boolean supportsCredentialChanges() { + return false; + } + + /** + * If no displayName pattern is defined then Gitblit can manage the display name. + * + * @return true if Gitblit can manage the user display name + * @since 1.0.0 + */ + @Override + public boolean supportsDisplayNameChanges() { + return StringUtils.isEmpty(settings.getString(Keys.realm.ldap.displayName, "")); + } + + /** + * If no email pattern is defined then Gitblit can manage the email address. + * + * @return true if Gitblit can manage the user email address + * @since 1.0.0 + */ + @Override + public boolean supportsEmailAddressChanges() { + return StringUtils.isEmpty(settings.getString(Keys.realm.ldap.email, "")); + } + + + /** + * If the LDAP server will maintain team memberships then LdapUserService + * will not allow team membership changes. In this scenario all team + * changes must be made on the LDAP server by the LDAP administrator. + * + * @return true or false + * @since 1.0.0 + */ + public boolean supportsTeamMembershipChanges() { + return !settings.getBoolean(Keys.realm.ldap.maintainTeams, false); + } + + @Override + public UserModel authenticate(String username, char[] password) { + String simpleUsername = getSimpleUsername(username); + + LDAPConnection ldapConnection = getLdapConnection(); + if (ldapConnection != null) { + try { + // Find the logging in user's DN + String accountBase = settings.getString(Keys.realm.ldap.accountBase, ""); + String accountPattern = settings.getString(Keys.realm.ldap.accountPattern, "(&(objectClass=person)(sAMAccountName=${username}))"); + accountPattern = StringUtils.replace(accountPattern, "${username}", escapeLDAPSearchFilter(simpleUsername)); + + SearchResult result = doSearch(ldapConnection, accountBase, accountPattern); + if (result != null && result.getEntryCount() == 1) { + SearchResultEntry loggingInUser = result.getSearchEntries().get(0); + String loggingInUserDN = loggingInUser.getDN(); + + if (isAuthenticated(ldapConnection, loggingInUserDN, new String(password))) { + logger.debug("LDAP authenticated: " + username); + + UserModel user = getUserModel(simpleUsername); + if (user == null) // create user object for new authenticated user + user = new UserModel(simpleUsername); + + // create a user cookie + if (StringUtils.isEmpty(user.cookie) && !ArrayUtils.isEmpty(password)) { + user.cookie = StringUtils.getSHA1(user.username + new String(password)); + } + + if (!supportsTeamMembershipChanges()) + getTeamsFromLdap(ldapConnection, simpleUsername, loggingInUser, user); + + // Get User Attributes + setUserAttributes(user, loggingInUser); + + // Push the ldap looked up values to backing file + super.updateUserModel(user); + if (!supportsTeamMembershipChanges()) { + for (TeamModel userTeam : user.teams) + updateTeamModel(userTeam); + } + + return user; + } + } + } finally { + ldapConnection.close(); + } + } + return null; + } + + /** + * Set the admin attribute from team memberships retrieved from LDAP. + * If we are not storing teams in LDAP and/or we have not defined any + * administrator teams, then do not change the admin flag. + * + * @param user + */ + private void setAdminAttribute(UserModel user) { + if (!supportsTeamMembershipChanges()) { + List admins = settings.getStrings(Keys.realm.ldap.admins); + // if we have defined administrative teams, then set admin flag + // otherwise leave admin flag unchanged + if (!ArrayUtils.isEmpty(admins)) { + user.canAdmin = false; + for (String admin : admins) { + if (admin.startsWith("@")) { // Team + if (user.getTeam(admin.substring(1)) != null) + user.canAdmin = true; + logger.debug("user "+ user.username+" has administrative rights"); + } else + if (user.getName().equalsIgnoreCase(admin)) + user.canAdmin = true; + } + } + } + } + + private void setUserAttributes(UserModel user, SearchResultEntry userEntry) { + // Is this user an admin? + setAdminAttribute(user); + + // Don't want visibility into the real password, make up a dummy + user.password = LDAP_PASSWORD_KEY; + + // Get full name Attribute + String displayName = settings.getString(Keys.realm.ldap.displayName, ""); + if (!StringUtils.isEmpty(displayName)) { + // Replace embedded ${} with attributes + if (displayName.contains("${")) { + for (Attribute userAttribute : userEntry.getAttributes()) + displayName = StringUtils.replace(displayName, "${" + userAttribute.getName() + "}", userAttribute.getValue()); + + user.displayName = displayName; + } else { + Attribute attribute = userEntry.getAttribute(displayName); + if (attribute != null && attribute.hasValue()) { + user.displayName = attribute.getValue(); + } + } + } + + // Get email address Attribute + String email = settings.getString(Keys.realm.ldap.email, ""); + if (!StringUtils.isEmpty(email)) { + if (email.contains("${")) { + for (Attribute userAttribute : userEntry.getAttributes()) + email = StringUtils.replace(email, "${" + userAttribute.getName() + "}", userAttribute.getValue()); + + user.emailAddress = email; + } else { + Attribute attribute = userEntry.getAttribute(email); + if (attribute != null && attribute.hasValue()) { + user.emailAddress = attribute.getValue(); + } + } + } + } + + private void getTeamsFromLdap(LDAPConnection ldapConnection, String simpleUsername, SearchResultEntry loggingInUser, UserModel user) { + String loggingInUserDN = loggingInUser.getDN(); + + user.teams.clear(); // Clear the users team memberships - we're going to get them from LDAP + String groupBase = settings.getString(Keys.realm.ldap.groupBase, ""); + String groupMemberPattern = settings.getString(Keys.realm.ldap.groupMemberPattern, "(&(objectClass=group)(member=${dn}))"); + + groupMemberPattern = StringUtils.replace(groupMemberPattern, "${dn}", escapeLDAPSearchFilter(loggingInUserDN)); + groupMemberPattern = StringUtils.replace(groupMemberPattern, "${username}", escapeLDAPSearchFilter(simpleUsername)); + + // Fill in attributes into groupMemberPattern + for (Attribute userAttribute : loggingInUser.getAttributes()) + groupMemberPattern = StringUtils.replace(groupMemberPattern, "${" + userAttribute.getName() + "}", escapeLDAPSearchFilter(userAttribute.getValue())); + + SearchResult teamMembershipResult = doSearch(ldapConnection, groupBase, groupMemberPattern); + if (teamMembershipResult != null && teamMembershipResult.getEntryCount() > 0) { + for (int i = 0; i < teamMembershipResult.getEntryCount(); i++) { + SearchResultEntry teamEntry = teamMembershipResult.getSearchEntries().get(i); + String teamName = teamEntry.getAttribute("cn").getValue(); + + TeamModel teamModel = getTeamModel(teamName); + if (teamModel == null) + teamModel = createTeamFromLdap(teamEntry); + + user.teams.add(teamModel); + teamModel.addUser(user.getName()); + } + } + } + + private TeamModel createTeamFromLdap(SearchResultEntry teamEntry) { + TeamModel answer = new TeamModel(teamEntry.getAttributeValue("cn")); + // potentially retrieve other attributes here in the future + + return answer; + } + + private SearchResult doSearch(LDAPConnection ldapConnection, String base, String filter) { + try { + return ldapConnection.search(base, SearchScope.SUB, filter); + } catch (LDAPSearchException e) { + logger.error("Problem Searching LDAP", e); + + return null; + } + } + + private boolean isAuthenticated(LDAPConnection ldapConnection, String userDn, String password) { + try { + // Binding will stop any LDAP-Injection Attacks since the searched-for user needs to bind to that DN + ldapConnection.bind(userDn, password); + return true; + } catch (LDAPException e) { + logger.error("Error authenticating user", e); + return false; + } + } + + + @Override + public List getAllUsernames() { + synchronizeLdapUsers(); + return super.getAllUsernames(); + } + + @Override + public List getAllUsers() { + synchronizeLdapUsers(); + return super.getAllUsers(); + } + + /** + * Returns a simple username without any domain prefixes. + * + * @param username + * @return a simple username + */ + protected String getSimpleUsername(String username) { + int lastSlash = username.lastIndexOf('\\'); + if (lastSlash > -1) { + username = username.substring(lastSlash + 1); + } + + return username; + } + + // From: https://www.owasp.org/index.php/Preventing_LDAP_Injection_in_Java + public static final String escapeLDAPSearchFilter(String filter) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < filter.length(); i++) { + char curChar = filter.charAt(i); + switch (curChar) { + case '\\': + sb.append("\\5c"); + break; + case '*': + sb.append("\\2a"); + break; + case '(': + sb.append("\\28"); + break; + case ')': + sb.append("\\29"); + break; + case '\u0000': + sb.append("\\00"); + break; + default: + sb.append(curChar); + } + } + return sb.toString(); + } +} -- cgit v1.2.3