distrib/gitblit.properties | ●●●●● patch | view | raw | blame | history | |
src/com/gitblit/ConfigUserService.java | ●●●●● patch | view | raw | blame | history | |
src/com/gitblit/FileUserService.java | ●●●●● patch | view | raw | blame | history | |
src/com/gitblit/GitblitUserService.java | ●●●●● patch | view | raw | blame | history | |
src/com/gitblit/IUserService.java | ●●●●● patch | view | raw | blame | history | |
src/com/gitblit/LdapUserService.java | ●●●●● patch | view | raw | blame | history |
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 # <http://download.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html> # # SINCE 0.8.0 web.timeFormat = HH:mm # Short date format # <http://download.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html> # # 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 # <http://download.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html> # # 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. # # <https://issues.apache.org/jira/browse/WICKET-1303> # <http://tomcat.apache.org/security-6.html#Fixed_in_Apache_Tomcat_6.0.10> # 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!!!<a href="http://somehost/bug/$3">Bug-Id: $3</a> # SINCE 0.5.0 regex.global.changeid = \\b(Change-Id:\\s*)([A-Za-z0-9]*)\\b!!!<a href="http://somehost/changeid/$2">Change-Id: $2</a> # Example per-repository regex substitutions overrides global # SINCE 0.5.0 regex.myrepository.bug = \\b(Bug:)(\\s*[#]?|-){0,1}(\\d+)\\b!!!<a href="http://elsewhere/bug/$3">Bug-Id: $3</a> # # 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 '<long> <TimeUnit>' where <TimeUnit> 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 # <http://download.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html> # # SINCE 0.8.0 web.timeFormat = HH:mm # Short date format # <http://download.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html> # # 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 # <http://download.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html> # # 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. # # <https://issues.apache.org/jira/browse/WICKET-1303> # <http://tomcat.apache.org/security-6.html#Fixed_in_Apache_Tomcat_6.0.10> # 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!!!<a href="http://somehost/bug/$3">Bug-Id: $3</a> # SINCE 0.5.0 regex.global.changeid = \\b(Change-Id:\\s*)([A-Za-z0-9]*)\\b!!!<a href="http://somehost/changeid/$2">Change-Id: $2</a> # Example per-repository regex substitutions overrides global # SINCE 0.5.0 regex.myrepository.bug = \\b(Bug:)(\\s*[#]?|-){0,1}(\\d+)\\b!!!<a href="http://elsewhere/bug/$3">Bug-Id: $3</a> # # 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 '<long> <TimeUnit>' where <TimeUnit> 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 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<String, UserModel> users = new ConcurrentHashMap<String, UserModel>(); private final Map<String, UserModel> cookies = new ConcurrentHashMap<String, UserModel>(); private final Map<String, TeamModel> teams = new ConcurrentHashMap<String, TeamModel>(); 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<UserModel> 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<String> getAllTeamNames() { read(); List<String> list = new ArrayList<String>(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<TeamModel> getAllTeams() { read(); List<TeamModel> list = new ArrayList<TeamModel>(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<String> getTeamnamesForRepositoryRole(String role) { List<String> list = new ArrayList<String>(); try { read(); for (Map.Entry<String, TeamModel> 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<String> teamnames) { try { Set<String> specifiedTeams = new HashSet<String>(); 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<TeamModel> 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<String> getAllUsernames() { read(); List<String> list = new ArrayList<String>(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<UserModel> getAllUsers() { read(); List<UserModel> list = new ArrayList<UserModel>(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<String> getUsernamesForRepositoryRole(String role) { List<String> list = new ArrayList<String>(); try { read(); for (Map.Entry<String, UserModel> 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<String> usernames) { try { Set<String> specifiedUsers = new HashSet<String>(); 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<String> roles = new ArrayList<String>(); 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<String> permissions = new ArrayList<String>(); for (Map.Entry<String, AccessPermission> 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<String> roles = new ArrayList<String>(); 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<String>( model.repositories)); } } else { // discrete repository permissions List<String> permissions = new ArrayList<String>(); for (Map.Entry<String, AccessPermission> 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<String>(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<String>( 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<String> 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<String> roles = new HashSet<String>(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<String> repositories = new HashSet<String>(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<String> teamnames = config.getSubsections(TEAM); for (String teamname : teamnames) { TeamModel team = new TeamModel(teamname); Set<String> roles = new HashSet<String>(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<String, UserModel> users = new ConcurrentHashMap<String, UserModel>(); private final Map<String, UserModel> cookies = new ConcurrentHashMap<String, UserModel>(); private final Map<String, TeamModel> teams = new ConcurrentHashMap<String, TeamModel>(); 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<UserModel> 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<String> getAllTeamNames() { read(); List<String> list = new ArrayList<String>(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<TeamModel> getAllTeams() { read(); List<TeamModel> list = new ArrayList<TeamModel>(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<String> getTeamnamesForRepositoryRole(String role) { List<String> list = new ArrayList<String>(); try { read(); for (Map.Entry<String, TeamModel> 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<String> teamnames) { try { Set<String> specifiedTeams = new HashSet<String>(); 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<TeamModel> 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<String> getAllUsernames() { read(); List<String> list = new ArrayList<String>(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<UserModel> getAllUsers() { read(); List<UserModel> list = new ArrayList<UserModel>(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<String> getUsernamesForRepositoryRole(String role) { List<String> list = new ArrayList<String>(); try { read(); for (Map.Entry<String, UserModel> 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<String> usernames) { try { Set<String> specifiedUsers = new HashSet<String>(); 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<String> roles = new ArrayList<String>(); 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<String> permissions = new ArrayList<String>(); for (Map.Entry<String, AccessPermission> 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<String> roles = new ArrayList<String>(); 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<String>( model.repositories)); } } else { // discrete repository permissions List<String> permissions = new ArrayList<String>(); for (Map.Entry<String, AccessPermission> 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<String>(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<String>( 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<String> 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<String> roles = new HashSet<String>(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<String> repositories = new HashSet<String>(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<String> teamnames = config.getSubsections(TEAM); for (String teamname : teamnames) { TeamModel team = new TeamModel(teamname); Set<String> roles = new HashSet<String>(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() + ")"; } } 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<String, String> cookies = new ConcurrentHashMap<String, String>(); private final Map<String, TeamModel> teams = new ConcurrentHashMap<String, TeamModel>(); 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<UserModel> 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<String> roles; if (model.permissions == null) { roles = new ArrayList<String>(); } else { // discrete repository permissions roles = new ArrayList<String>(); for (Map.Entry<String, AccessPermission> 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<String> getAllUsernames() { Properties allUsers = read(); List<String> list = new ArrayList<String>(); 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<UserModel> getAllUsers() { read(); List<UserModel> list = new ArrayList<UserModel>(); 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<String> getUsernamesForRepositoryRole(String role) { List<String> list = new ArrayList<String>(); 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<String> usernames) { try { Set<String> specifiedUsers = new HashSet<String>(usernames); Set<String> needsAddRole = new HashSet<String>(specifiedUsers); Set<String> needsRemoveRole = new HashSet<String>(); // 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<String> needsRenameRole = new HashSet<String>(); // 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<String> needsDeleteRole = new HashSet<String>(); // 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<String> repositories = new ArrayList<String>(); List<String> users = new ArrayList<String>(); List<String> mailingLists = new ArrayList<String>(); List<String> preReceive = new ArrayList<String>(); List<String> postReceive = new ArrayList<String>(); 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<String> getAllTeamNames() { List<String> list = new ArrayList<String>(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<TeamModel> getAllTeams() { List<TeamModel> list = new ArrayList<TeamModel>(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<String> getTeamnamesForRepositoryRole(String role) { List<String> list = new ArrayList<String>(); 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<String> teamnames) { try { Set<String> specifiedTeams = new HashSet<String>(teamnames); Set<String> needsAddRole = new HashSet<String>(specifiedTeams); Set<String> needsRemoveRole = new HashSet<String>(); // 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<TeamModel> 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<String> roles; if (model.permissions == null) { // legacy, use repository list if (model.repositories != null) { roles = new ArrayList<String>(model.repositories); } else { roles = new ArrayList<String>(); } } else { // discrete repository permissions roles = new ArrayList<String>(); for (Map.Entry<String, AccessPermission> 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<String, String> cookies = new ConcurrentHashMap<String, String>(); private final Map<String, TeamModel> teams = new ConcurrentHashMap<String, TeamModel>(); 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<UserModel> 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<String> roles; if (model.permissions == null) { roles = new ArrayList<String>(); } else { // discrete repository permissions roles = new ArrayList<String>(); for (Map.Entry<String, AccessPermission> 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<String> getAllUsernames() { Properties allUsers = read(); List<String> list = new ArrayList<String>(); 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<UserModel> getAllUsers() { read(); List<UserModel> list = new ArrayList<UserModel>(); 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<String> getUsernamesForRepositoryRole(String role) { List<String> list = new ArrayList<String>(); 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<String> usernames) { try { Set<String> specifiedUsers = new HashSet<String>(usernames); Set<String> needsAddRole = new HashSet<String>(specifiedUsers); Set<String> needsRemoveRole = new HashSet<String>(); // 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<String> needsRenameRole = new HashSet<String>(); // 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<String> needsDeleteRole = new HashSet<String>(); // 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<String> repositories = new ArrayList<String>(); List<String> users = new ArrayList<String>(); List<String> mailingLists = new ArrayList<String>(); List<String> preReceive = new ArrayList<String>(); List<String> postReceive = new ArrayList<String>(); 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<String> getAllTeamNames() { List<String> list = new ArrayList<String>(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<TeamModel> getAllTeams() { List<TeamModel> list = new ArrayList<TeamModel>(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<String> getTeamnamesForRepositoryRole(String role) { List<String> list = new ArrayList<String>(); 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<String> teamnames) { try { Set<String> specifiedTeams = new HashSet<String>(teamnames); Set<String> needsAddRole = new HashSet<String>(specifiedTeams); Set<String> needsRemoveRole = new HashSet<String>(); // 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<TeamModel> 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<String> roles; if (model.permissions == null) { // legacy, use repository list if (model.repositories != null) { roles = new ArrayList<String>(model.repositories); } else { roles = new ArrayList<String>(); } } else { // discrete repository permissions roles = new ArrayList<String>(); for (Map.Entry<String, AccessPermission> 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; } } 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<UserModel> 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<String> getAllUsernames() { return serviceImpl.getAllUsernames(); } @Override public List<UserModel> getAllUsers() { return serviceImpl.getAllUsers(); } @Override public List<String> getAllTeamNames() { return serviceImpl.getAllTeamNames(); } @Override public List<TeamModel> getAllTeams() { return serviceImpl.getAllTeams(); } @Override public List<String> getTeamnamesForRepositoryRole(String role) { return serviceImpl.getTeamnamesForRepositoryRole(role); } @Override @Deprecated public boolean setTeamnamesForRepositoryRole(String role, List<String> 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<TeamModel> 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<String> getUsernamesForRepositoryRole(String role) { return serviceImpl.getUsernamesForRepositoryRole(role); } @Override @Deprecated public boolean setUsernamesForRepositoryRole(String role, List<String> 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<UserModel> 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<String> getAllUsernames() { return serviceImpl.getAllUsernames(); } @Override public List<UserModel> getAllUsers() { return serviceImpl.getAllUsers(); } @Override public List<String> getAllTeamNames() { return serviceImpl.getAllTeamNames(); } @Override public List<TeamModel> getAllTeams() { return serviceImpl.getAllTeams(); } @Override public List<String> getTeamnamesForRepositoryRole(String role) { return serviceImpl.getTeamnamesForRepositoryRole(role); } @Override @Deprecated public boolean setTeamnamesForRepositoryRole(String role, List<String> 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<TeamModel> 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<String> getUsernamesForRepositoryRole(String role) { return serviceImpl.getUsernamesForRepositoryRole(role); } @Override @Deprecated public boolean setUsernamesForRepositoryRole(String role, List<String> 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); } } 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<UserModel> 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<String> getAllUsernames(); /** * Returns the list of all users available to the login service. * * @return list of all users * @since 0.8.0 */ List<UserModel> getAllUsers(); /** * Returns the list of all teams available to the login service. * * @return list of all teams * @since 0.8.0 */ List<String> getAllTeamNames(); /** * Returns the list of all teams available to the login service. * * @return list of all teams * @since 0.8.0 */ List<TeamModel> 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<String> 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<String> 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<TeamModel> 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<String> 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<String> 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<UserModel> 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<String> getAllUsernames(); /** * Returns the list of all users available to the login service. * * @return list of all users * @since 0.8.0 */ List<UserModel> getAllUsers(); /** * Returns the list of all teams available to the login service. * * @return list of all teams * @since 0.8.0 */ List<String> getAllTeamNames(); /** * Returns the list of all teams available to the login service. * * @return list of all teams * @since 0.8.0 */ List<TeamModel> 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<String> 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<String> 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<TeamModel> 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<String> 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<String> 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(); } 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 '<long> <TimeUnit>' where <TimeUnit> 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<String, UserModel> ldapUsers = new HashMap<String, UserModel>(); 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<String, TeamModel> userTeams = new HashMap<String, TeamModel>(); 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)://<server>:<port>", 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<String> 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<String> getAllUsernames() { synchronizeLdapUsers(); return super.getAllUsernames(); } @Override public List<UserModel> 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 '<long> <TimeUnit>' where <TimeUnit> 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<String, UserModel> ldapUsers = new HashMap<String, UserModel>(); 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<String, TeamModel> userTeams = new HashMap<String, TeamModel>(); 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)://<server>:<port>", 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<String> 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<String> getAllUsernames() { synchronizeLdapUsers(); return super.getAllUsernames(); } @Override public List<UserModel> 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(); } }