2011-11-12 21:42:19 +04:00
package Apache::Authn::Redmine ;
= head1 Apache::Authn:: Redmine
Redmine - a mod_perl module to authenticate webdav subversion users
against redmine database
= head1 SYNOPSIS
This module allow anonymous users to browse public project and
registred users to browse and commit their project . Authentication is
done against the redmine database or the LDAP configured in redmine .
This method is far simpler than the one with pam_ * and works with all
database without an hassle but you need to have apache / mod_perl on the
svn server .
= head1 INSTALLATION
For this to automagically work , you need to have a recent reposman . rb
( after r860 ) and if you already use reposman , read the last section to
migrate .
Sorry ruby users but you need some perl modules , at least mod_perl2 ,
DBI and DBD:: mysql ( or the DBD driver for you database as it should
work on allmost all databases ) .
On debian / ubuntu you must do :
2011-11-14 14:12:40 +04:00
aptitude install libapache - dbi - perl libapache2 - mod - perl2 libdbd - mysql - perl
2011-11-12 21:42:19 +04:00
If your Redmine users use LDAP authentication , you will also need
Authen::Simple:: LDAP ( and IO::Socket:: SSL if LDAPS is used ) :
2011-11-14 14:12:40 +04:00
aptitude install libauthen - simple - ldap - perl libio - socket - ssl - perl
2011-11-12 21:42:19 +04:00
= head1 CONFIGURATION
2011-11-14 14:12:40 +04:00
## This module has to be in your perl path
## eg: /usr/lib/perl5/Apache/Authn/Redmine.pm
PerlLoadModule Apache::Authn:: Redmine
< Location / svn >
DAV svn
SVNParentPath "/var/svn"
AuthType Basic
AuthName redmine
Require valid - user
PerlAuthenHandler Apache::Authn::Redmine:: authen_handler
PerlAuthzHandler Apache::Authn::Redmine:: authz_handler
## for mysql
RedmineDSN "DBI:mysql:database=databasename;host=my.db.server"
## for postgres
# RedmineDSN "DBI:Pg:dbname=databasename;host=my.db.server"
RedmineDbUser "redmine"
RedmineDbPass "password"
2012-04-04 18:20:35 +04:00
2011-11-14 14:23:46 +04:00
## Authorization where clause (fulltext search would be slow and database dependant).
## Default: none
2011-11-14 14:12:40 +04:00
# RedmineDbWhereClause "and members.role_id IN (1,2)"
2012-04-04 18:20:35 +04:00
2012-04-12 13:10:12 +04:00
## SCM transport protocol, used to detecte write requests
2012-04-16 14:35:55 +04:00
## Valid values: Subversion, Git
## Default: Subversion
# RedmineRepositoryType Subversion
2012-04-12 13:10:12 +04:00
2011-11-14 14:23:46 +04:00
## Credentials cache size
## Default: 0 (disabled)
2011-11-14 14:12:40 +04:00
# RedmineCacheCredsMax 50
2012-04-04 18:20:35 +04:00
2011-11-14 14:23:46 +04:00
## Credentials cache expiration delay in seconds
## Set to 0 to disable expiration.
## Default: 5 minutes (300)
# RedmineCacheCredsMaxAge 60
2012-04-04 18:20:35 +04:00
2011-11-14 14:23:46 +04:00
## Check authorizations against a specific project.
## Default: none (extract project from location)
# RedmineProject myproject
2012-04-04 18:20:35 +04:00
2011-11-14 14:23:46 +04:00
## Permissions to check for "read" access.
## You can add several permissions, user is granted access if *at least* one them exists.
## Default: :browse_repository
# RedmineReadPermissions :browse_repository
2012-04-04 18:20:35 +04:00
2011-11-14 14:23:46 +04:00
## Permissions to check for "write" access.
## You can add several permissions, user is granted access if *at least* one them exists.
## Default: :commit_access
# RedmineWritePermissions :commit_access
2012-04-04 18:20:35 +04:00
2011-11-14 14:23:46 +04:00
## Deny anonymous access.
## Affects both authentication and authorization
## Default: Off
# RedmineDenyAnonymous On
2012-04-04 18:20:35 +04:00
2011-11-14 14:23:46 +04:00
## Deny non-member access to projects.
## Default: Off
# RedmineDenyNonMember On
2012-04-04 18:20:35 +04:00
2016-04-05 14:49:47 +03:00
## Allow authentication by API key
## Default: Off
# RedmineKeyAuthentication On
## Username for authentication by API key
## Default: api-key
# RedmineKeyUsername key
2011-11-14 14:23:46 +04:00
## Administrators have super-powers
## Default: On
# RedmineSuperAdmin Off
2012-04-04 18:20:35 +04:00
2011-11-14 14:12:40 +04:00
</Location>
2011-11-12 21:42:19 +04:00
To be able to browse repository inside redmine , you must add something
like that :
2011-11-14 14:12:40 +04:00
< Location / svn - private >
DAV svn
SVNParentPath "/var/svn"
Order deny , allow
Deny from all
# only allow reading orders
< Limit GET PROPFIND OPTIONS REPORT >
Allow from redmine . server . ip
</Limit>
</Location>
2011-11-12 21:42:19 +04:00
and you will have to use this reposman . rb command line to create repository :
2011-11-14 14:12:40 +04:00
reposman . rb - - redmine my . redmine . server - - svn - dir /var/s vn - - owner www - data - u http: //s vn . server /svn-private/
2011-11-12 21:42:19 +04:00
= head1 MIGRATION FROM OLDER RELEASES
If you use an older reposman . rb ( r860 or before ) , you need to change
rights on repositories to allow the apache user to read and write
S < them : >
2011-11-14 14:12:40 +04:00
sudo chown - R www - data /var/s vn / *
sudo chmod - R u + w /var/s vn / *
2011-11-12 21:42:19 +04:00
And you need to upgrade at least reposman . rb ( after r860 ) .
= cut
use strict ;
use warnings FATAL = > 'all' , NONFATAL = > 'redefine' ;
use DBI ;
2012-04-04 18:20:47 +04:00
use Digest::SHA ;
2011-11-12 21:42:19 +04:00
# optional module for LDAP authentication
my $ CanUseLDAPAuth = eval ( "use Authen::Simple::LDAP; 1" ) ;
# Reload ourself (disable in production)
2013-05-16 13:08:07 +04:00
# use Apache2::Reload;
2011-11-12 21:42:19 +04:00
use Apache2::Module ;
use Apache2::Access ;
use Apache2::ServerRec qw( ) ;
use Apache2::RequestRec qw( ) ;
use Apache2::RequestUtil qw( ) ;
2012-04-04 19:09:05 +04:00
use Apache2::CmdParms qw( ) ;
2011-11-12 21:42:19 +04:00
use Apache2::Const qw( :common :override :cmd_how ) ;
use Apache2::Log ;
use APR::Pool ( ) ;
use APR::Table ( ) ;
# use Apache2::Directive qw();
my @ directives = (
2011-11-13 14:53:29 +04:00
{
name = > 'RedmineDSN' ,
req_override = > OR_AUTHCFG ,
args_how = > TAKE1 ,
errmsg = > 'Redmine database DSN in format used by Perl DBI. eg: "DBI:Pg:dbname=databasename;host=my.db.server"' ,
} ,
{
name = > 'RedmineDbUser' ,
req_override = > OR_AUTHCFG ,
args_how = > TAKE1 ,
errmsg = > 'Redmine database user' ,
} ,
{
name = > 'RedmineDbPass' ,
req_override = > OR_AUTHCFG ,
args_how = > TAKE1 ,
errmsg = > 'Redmine database password' ,
} ,
{
name = > 'RedmineDbWhereClause' ,
req_override = > OR_AUTHCFG ,
args_how = > TAKE1 ,
errmsg = > 'Additionnal where clause used when checking for user permissions' ,
} ,
{
name = > 'RedmineProject' ,
req_override = > OR_AUTHCFG ,
args_how = > TAKE1 ,
errmsg = > 'Identifier (short name) of a Redmine project. If undefined, extract the project identifier from the location.' ,
} ,
2011-11-13 13:45:04 +04:00
{
name = > 'RedmineReadPermissions' ,
req_override = > OR_AUTHCFG ,
args_how = > ITERATE ,
2011-11-13 14:53:29 +04:00
errmsg = > 'Permissions to check for read access. Defaults to :browse_repository.' ,
2011-11-13 13:45:04 +04:00
} ,
{
name = > 'RedmineWritePermissions' ,
req_override = > OR_AUTHCFG ,
args_how = > ITERATE ,
2011-11-13 14:53:29 +04:00
errmsg = > 'Permissions to check for write access. Defaults to :commit_access.' ,
} ,
{
name = > 'RedmineCacheCredsMax' ,
req_override = > OR_AUTHCFG ,
args_how = > TAKE1 ,
errmsg = > 'Maximum number of credentials to cache. Set to 0 to disable credential caching.' ,
} ,
{
name = > 'RedmineCacheCredsMaxAge' ,
req_override = > OR_AUTHCFG ,
args_how = > TAKE1 ,
errmsg = > 'Maximum age of cached credentials. Defaults to 300. Set to 0 to disable credential expiration.' ,
2011-11-13 13:45:04 +04:00
} ,
2011-11-13 14:54:39 +04:00
{
name = > 'RedmineDenyAnonymous' ,
req_override = > OR_AUTHCFG ,
args_how = > FLAG ,
errmsg = > 'Deny anonymous access. Defaults to no.' ,
} ,
{
name = > 'RedmineDenyNonMember' ,
req_override = > OR_AUTHCFG ,
args_how = > FLAG ,
errmsg = > 'Do not check non-member permissions. Defaults to no.' ,
} ,
{
name = > 'RedmineSuperAdmin' ,
req_override = > OR_AUTHCFG ,
args_how = > FLAG ,
errmsg = > 'Grant all permissions to administrators. Defaults to yes.' ,
} ,
2012-04-12 13:07:05 +04:00
{
2012-04-16 14:35:55 +04:00
name = > 'RedmineRepositoryType' ,
2012-04-12 13:07:05 +04:00
req_override = > OR_AUTHCFG ,
args_how = > TAKE1 ,
2012-04-16 14:35:55 +04:00
errmsg = > 'Indicate the type of Repository (Subversion or Git). This is used to properly detected write requests. Defaults to Subversion.' ,
2012-04-12 13:07:05 +04:00
} ,
2016-04-05 14:49:47 +03:00
{
name = > 'RedmineKeyAuthentication' ,
req_override = > OR_AUTHCFG ,
args_how = > FLAG ,
errmsg = > 'Allow authentication by API key. Defaults to no.'
} ,
{
name = > 'RedmineKeyUsername' ,
req_override = > OR_AUTHCFG ,
args_how = > TAKE1 ,
errmsg = > 'USername to use for authenticiation with API KEY. Defaults to api-key.'
} ,
2011-11-12 21:42:19 +04:00
) ;
2011-11-13 14:54:39 +04:00
# Initialize defaults configuration
sub DIR_CREATE {
my ( $ class , $ parms ) = @ _ ;
2012-04-04 19:09:05 +04:00
my $ identifier_re = "^" . quotemeta ( $ parms - > path ) . "/?([^/]+)" if $ parms - > path ;
2011-11-13 14:54:39 +04:00
return bless {
PermissionQuery = > trim ( "
SELECT permissions FROM users , members , member_roles , roles
WHERE users . login = ?
AND users . id = members . user_id
AND users . status = 1
AND members . project_id = ?
AND members . id = member_roles . member_id
AND member_roles . role_id = roles . id
" ) ,
2012-04-16 14:35:55 +04:00
RepositoryType = > 'Subversion' ,
2012-04-04 19:09:05 +04:00
IdentifierRegex = > $ identifier_re ? qr{ $identifier_re } : undef ,
2011-11-13 14:54:39 +04:00
CacheCredsMax = > 0 ,
CacheCredsCount = > 0 ,
CacheCredsMaxAge = > 300 ,
DenyAnonymous = > 0 ,
DenyNonMember = > 0 ,
SuperAdmin = > 1 ,
2016-04-05 14:49:47 +03:00
KeyAuthentication = > 0 ,
KeyUsername = > 'api-key' ,
2011-11-13 14:54:39 +04:00
} , $ class ;
2011-11-12 21:42:19 +04:00
}
2011-11-13 14:54:39 +04:00
# Simple setters
sub RedmineDSN { set_val ( 'DSN' , @ _ ) ; }
2011-11-13 13:45:04 +04:00
sub RedmineDbUser { set_val ( 'DbUser' , @ _ ) ; }
sub RedmineDbPass { set_val ( 'DbPass' , @ _ ) ; }
sub RedmineProject { set_val ( 'Project' , @ _ ) ; }
2011-11-13 14:54:39 +04:00
sub RedmineReadPermissions { push_val ( 'ReadPermissions' , @ _ ) ; }
sub RedmineWritePermissions { push_val ( 'WritePermissions' , @ _ ) ; }
2012-04-04 19:09:52 +04:00
sub RedmineCacheCredsMax { set_val ( 'CacheCredsMax' , @ _ ) ; }
2011-11-13 14:54:39 +04:00
sub RedmineCacheCredsMaxAge { set_val ( 'CacheCredsMaxAge' , @ _ ) ; }
sub RedmineDenyAnonymous { set_val ( 'DenyAnonymous' , @ _ ) ; }
sub RedmineDenyNonMember { set_val ( 'DenyNonMember' , @ _ ) ; }
sub RedmineSuperAdmin { set_val ( 'SuperAdmin' , @ _ ) ; }
2016-04-05 14:49:47 +03:00
sub RedmineKeyAuthentication { set_val ( 'KeyAuthentication' , @ _ ) ; }
sub RedmineKeyUsername { set_val ( 'KeyUsername' , @ _ ) ; }
2011-11-13 13:45:04 +04:00
sub RedmineDbWhereClause {
2011-11-14 14:12:40 +04:00
my ( $ cfg , $ parms , $ arg ) = @ _ ;
if ( $ arg ) {
$ cfg - > { PermissionQuery } = trim ( $ cfg - > { PermissionQuery } . "$arg " ) ;
2011-11-13 14:54:39 +04:00
}
2011-11-12 21:42:19 +04:00
}
2012-04-16 14:35:55 +04:00
sub RedmineRepositoryType {
2012-04-12 13:07:05 +04:00
my ( $ cfg , $ parms , $ arg ) = @ _ ;
$ arg = trim ( $ arg ) ;
2012-04-16 14:35:55 +04:00
if ( $ arg eq 'Subversion' || $ arg eq 'Git' ) {
2013-04-17 14:39:13 +04:00
$ cfg - > { RepositoryType } = 'Repository::' . $ arg ;
2012-04-12 13:07:05 +04:00
} else {
2012-04-16 14:35:55 +04:00
die "Invalid RedmineRepositoryType value: $arg, choose either Subversion or Git !" ;
2012-04-12 13:07:05 +04:00
}
}
2011-11-13 14:54:39 +04:00
sub set_val {
2011-11-14 14:12:40 +04:00
my ( $ key , $ cfg , $ parms , $ arg ) = @ _ ;
$ cfg - > { $ key } = $ arg ;
2011-11-13 13:45:04 +04:00
}
2011-11-13 14:54:39 +04:00
sub push_val {
2011-11-14 14:12:40 +04:00
my ( $ key , $ cfg , $ parms , $ arg ) = @ _ ;
push @ { $ cfg - > { $ key } } , $ arg ;
2011-11-13 13:45:04 +04:00
}
2011-11-12 21:42:19 +04:00
sub trim {
2011-11-14 14:12:40 +04:00
my $ string = shift ;
$ string =~ s/\s{2,}/ /g ;
return $ string ;
2011-11-12 21:42:19 +04:00
}
Apache2::Module:: add ( __PACKAGE__ , \ @ directives ) ;
2011-11-13 13:45:04 +04:00
my % read_only_methods = map { $ _ = > 1 } qw/GET PROPFIND REPORT OPTIONS/ ;
my @ default_read_permissions = ( ':browse_repository' ) ;
my @ default_write_permissions = ( ':commit_access' ) ;
2011-11-12 21:42:19 +04:00
sub authen_handler {
2011-11-14 14:12:40 +04:00
my $ r = shift ;
2011-11-13 13:45:04 +04:00
2011-11-14 14:12:40 +04:00
unless ( $ r - > some_auth_required ) {
$ r - > log_reason ( "No authentication has been configured" ) ;
return FORBIDDEN ;
}
2011-11-12 21:42:19 +04:00
my ( $ res , $ password ) = $ r - > get_basic_auth_pw ( ) ;
my $ reason ;
2011-11-13 13:45:04 +04:00
2011-11-12 21:42:19 +04:00
if ( $ res == OK ) {
# Got user and password
# Used cached credentials if possible
my $ cache_key = get_cache_key ( $ r , $ password ) ;
2012-04-04 19:07:11 +04:00
if ( defined $ cache_key && cache_get ( $ r , $ cache_key ) ) {
2011-11-12 21:42:19 +04:00
$ r - > log - > debug ( "reusing cached credentials for user '" , $ r - > user , "'" ) ;
$ r - > set_handlers ( PerlAuthzHandler = > undef ) ;
2011-11-13 13:45:04 +04:00
2011-11-12 21:42:19 +04:00
} else {
# Else check them
2011-11-14 14:49:34 +04:00
my $ dbh = connect_database ( $ r )
or return SERVER_ERROR ;
2012-04-04 18:20:35 +04:00
2011-11-12 21:42:19 +04:00
( $ res , $ reason ) = check_login ( $ r , $ dbh , $ password ) ;
$ dbh - > disconnect ( ) ;
2011-11-13 13:45:04 +04:00
2011-11-12 21:42:19 +04:00
# Store the cache key for latter use
2012-04-04 19:07:11 +04:00
$ r - > pnotes ( "RedmineCacheKey" = > $ cache_key ) if $ res == OK && defined $ cache_key ;
2011-11-12 21:42:19 +04:00
}
2011-11-13 13:45:04 +04:00
2011-11-12 21:42:19 +04:00
} elsif ( $ res == AUTH_REQUIRED ) {
2011-11-14 14:49:34 +04:00
my $ dbh = connect_database ( $ r )
or return SERVER_ERROR ;
2012-04-04 18:20:35 +04:00
2011-11-13 14:54:39 +04:00
my $ cfg = get_config ( $ r ) ;
if ( ! $ cfg - > { AllowAnonymous } || is_authentication_forced ( $ dbh ) ) {
2011-11-12 21:42:19 +04:00
# We really want an user
$ reason = 'anonymous access disabled' ;
} else {
# Anonymous is allowed
$ res = OK ;
}
$ dbh - > disconnect ( ) ;
2011-11-13 13:45:04 +04:00
}
2011-11-12 21:42:19 +04:00
$ r - > log_reason ( $ reason ) if defined ( $ reason ) ;
2011-11-13 13:45:04 +04:00
$ r - > note_basic_auth_failure unless $ res == OK ;
2011-11-12 21:42:19 +04:00
2011-11-14 14:12:40 +04:00
return $ res ;
2011-11-12 21:42:19 +04:00
}
sub check_login {
my ( $ r , $ dbh , $ password ) = @ _ ;
my $ user = $ r - > user ;
2016-04-05 14:49:47 +03:00
my $ status ;
2011-11-13 13:45:04 +04:00
2016-04-05 14:49:47 +03:00
my $ cfg = get_config ( $ r ) ;
2011-11-14 14:12:40 +04:00
2016-04-05 14:49:47 +03:00
if ( $ cfg - > { KeyAuthentication } && $ user eq $ cfg - > { KeyUsername } ) {
# API key auth
( $ user , $ status ) = $ dbh - > selectrow_array ( 'SELECT u.login, u.status FROM users u INNER JOIN tokens t ON (t.user_id = u.id) WHERE t.action = \'api\' AND t.value = ?' , undef , $ password )
or return ( AUTH_REQUIRED , "unknown api-key '$password'" ) ;
$ r - > user ( $ user ) ;
2011-11-13 13:45:04 +04:00
} else {
2016-04-05 14:49:47 +03:00
# Login+password auth
my ( $ hashed_password , $ auth_source_id , $ salt , $ res , $ reason ) ;
( $ hashed_password , $ status , $ auth_source_id , $ salt ) = $ dbh - > selectrow_array ( 'SELECT hashed_password, status, auth_source_id, salt FROM users WHERE login = ?' , undef , $ user )
or return ( AUTH_REQUIRED , "unknown user '$user'" ) ;
if ( $ auth_source_id ) {
( $ res , $ reason ) = check_ldap_login ( $ dbh , $ auth_source_id , $ user , $ password ) ;
} else {
( $ res , $ reason ) = check_db_login ( $ user , $ password , $ hashed_password , $ salt ) ;
}
# Bail out if authentication failed
return ( $ res , $ reason ) unless $ res == OK ;
2011-11-12 21:42:19 +04:00
}
2011-11-13 13:45:04 +04:00
# Password is ok, check if account if locked
2011-11-13 13:38:18 +04:00
return ( FORBIDDEN , "inactive account: '$user'" ) unless $ status == 1 ;
2011-11-12 21:42:19 +04:00
$ r - > log - > debug ( "successfully authenticated as active redmine user '$user'" ) ;
2011-11-13 13:45:04 +04:00
# Everything's ok
2011-11-12 21:42:19 +04:00
return OK ;
}
2016-04-05 14:49:47 +03:00
sub check_ldap_login {
# Ensure Authen::Simple::LDAP is available
return ( SERVER_ERROR , "Redmine LDAP authentication requires Authen::Simple::LDAP" )
unless $ CanUseLDAPAuth ;
my ( $ dbh , $ auth_source_id , $ user , $ password ) = @ _ ;
# Get LDAP server informations
my ( $ host , $ port , $ tls , $ account , $ account_password , $ base_dn , $ attr_login ) = $ dbh - > selectrow_array (
"SELECT host,port,tls,account,account_password,base_dn,attr_login from auth_sources WHERE id = ?" ,
undef ,
$ auth_source_id
)
or return ( SERVER_ERROR , "Undefined authentication source for '$user'" ) ;
# Connect to the LDAP server
my $ ldap = Authen::Simple::LDAP - > new (
host = > is_true ( $ tls ) ? "ldaps://$host:$port" : $ host ,
port = > $ port ,
basedn = > $ base_dn ,
binddn = > $ account || "" ,
bindpw = > $ account_password || "" ,
filter = > '(' . $ attr_login . '=%s)'
) ;
# Finally check user login
return ( AUTH_REQUIRED , "LDAP authentication failed (user: '$user', server: '$host')" )
unless $ ldap - > authenticate ( $ user , $ password ) ;
# LDAP auth is ok
return OK ;
}
sub check_db_login {
2018-07-18 14:53:51 +03:00
my ( $ user , $ password , $ hashed_password , $ salt ) = @ _ ;
2016-04-05 14:49:47 +03:00
# Database authentication
my $ pass_digest = Digest::SHA:: sha1_hex ( $ password ) ;
return ( AUTH_REQUIRED , "wrong password for '$user'" )
unless $ hashed_password eq Digest::SHA:: sha1_hex ( $ salt . $ pass_digest ) ;
# Database password is ok
return OK ;
}
2011-11-12 21:42:19 +04:00
# check if authentication is forced
sub is_authentication_forced {
my $ dbh = shift ;
2011-11-14 14:12:40 +04:00
return is_true ( $ dbh - > selectrow_array ( "SELECT value FROM settings WHERE settings.name = 'login_required'" ) ) ;
2011-11-12 21:42:19 +04:00
}
sub authz_handler {
2011-11-14 14:12:40 +04:00
my $ r = shift ;
2011-11-12 21:42:19 +04:00
2011-11-14 14:12:40 +04:00
unless ( $ r - > some_auth_required ) {
$ r - > log_reason ( "No authentication has been configured" ) ;
return FORBIDDEN ;
}
2011-11-12 21:42:19 +04:00
2011-11-14 14:49:59 +04:00
my $ dbh = connect_database ( $ r )
or return SERVER_ERROR ;
2012-04-04 18:20:35 +04:00
2011-11-14 14:49:59 +04:00
my $ cfg = get_config ( $ r ) ;
2012-04-04 18:20:35 +04:00
2012-04-26 18:44:03 +04:00
my ( $ identifier , $ project_id , $ is_public , $ status ) ;
if ( $ identifier = $ cfg - > { Project } ) {
( $ project_id , $ is_public , $ status ) = $ dbh - > selectrow_array (
" SELECT p . id , p . is_public , p . status
FROM projects p JOIN repositories r ON ( p . id = r . project_id )
2012-04-26 19:08:35 +04:00
WHERE p . identifier = ? AND r . is_default AND r . type = ? " ,
2012-04-16 14:35:55 +04:00
undef , $ identifier , $ cfg - > { RepositoryType }
2012-04-26 18:44:03 +04:00
) ;
2013-05-16 13:09:48 +04:00
unless ( defined $ project_id ) {
$ r - > log_reason ( "No matching project for ${identifier}" ) ;
return NOT_FOUND ;
}
2012-04-26 18:44:03 +04:00
} elsif ( my $ repo_id = get_repository_identifier ( $ r ) ) {
2018-07-18 14:53:51 +03:00
my @ pr_id = split ( /\./ , $ repo_id ) ;
2012-04-26 18:44:03 +04:00
( $ identifier , $ project_id , $ is_public , $ status ) = $ dbh - > selectrow_array (
" SELECT p . identifier , p . id , p . is_public , p . status
FROM projects p JOIN repositories r ON ( p . id = r . project_id )
2012-04-26 19:08:35 +04:00
WHERE ( ( r . is_default AND p . identifier = ? ) OR r . identifier = ? ) AND r . type = ? " ,
2018-07-18 14:53:51 +03:00
undef , $ pr_id [ 0 ] , $ repo_id , $ cfg - > { RepositoryType }
2012-04-26 18:44:03 +04:00
) ;
2013-05-16 13:09:48 +04:00
unless ( defined $ project_id ) {
$ r - > log_reason ( "No matching project for ${repo_id}" ) ;
return NOT_FOUND ;
}
2012-04-26 18:44:03 +04:00
2013-05-16 13:09:48 +04:00
} else {
# Cannot get a project out of the URL, we probably are on the parent path (e.g. /svn/)
return FORBIDDEN ;
}
2012-04-26 18:44:03 +04:00
2011-11-12 21:42:19 +04:00
$ is_public = is_true ( $ is_public ) ;
my ( $ res , $ reason ) = FORBIDDEN ;
2011-11-13 13:45:04 +04:00
2011-11-14 14:49:59 +04:00
if ( $ status != 1 && ! is_read_request ( $ r ) ) {
2011-11-14 14:12:40 +04:00
# Write operation on archived project is forbidden
$ reason = "write operations on inactive project '$identifier' are forbidden" ;
2011-11-12 21:42:19 +04:00
} elsif ( ! $ r - > user ) {
2011-11-14 14:12:40 +04:00
# Anonymous access
2011-11-12 21:42:19 +04:00
$ res = AUTH_REQUIRED ;
$ reason = "anonymous access to '$identifier' denied" ;
2011-11-13 13:45:04 +04:00
2011-11-13 14:54:39 +04:00
if ( $ is_public && ! $ cfg - > { DenyAnonymous } ) {
2011-11-12 21:42:19 +04:00
# Check anonymous permissions
2011-11-13 14:54:39 +04:00
my $ permissions = $ dbh - > selectrow_array ( "SELECT permissions FROM roles WHERE builtin = 2" ) ;
$ res = OK if $ permissions && check_permissions ( $ r , $ permissions ) ;
2011-11-12 21:42:19 +04:00
}
2011-11-13 13:45:04 +04:00
2011-11-14 14:12:40 +04:00
# Force login if failed
2011-11-12 21:42:19 +04:00
$ r - > note_auth_failure unless $ res == OK ;
2011-11-13 13:45:04 +04:00
2011-11-14 14:12:40 +04:00
} else {
# Logged in user
my $ user = $ r - > user ;
2011-11-13 13:45:04 +04:00
2011-11-14 14:12:40 +04:00
if ( $ cfg - > { SuperAdmin } && is_true ( $ dbh - > selectrow_array ( "SELECT admin FROM users WHERE login = ?" , undef , $ user ) ) ) {
# Adminstrators have all the rights
$ res = OK ;
2011-11-13 14:54:39 +04:00
2011-11-14 14:12:40 +04:00
} else {
# Really check user permissions
2011-11-13 14:54:39 +04:00
my @ permissions = ( ) ;
# Membership permissions
my $ membership = $ dbh - > selectcol_arrayref ( $ cfg - > { PermissionQuery } , undef , $ user , $ project_id ) ;
push @ permissions , @ { $ membership } if $ membership ;
2011-11-13 13:45:04 +04:00
# Add non-member permissions for public projects
2011-11-13 14:54:39 +04:00
if ( $ is_public && ! $ cfg - > { DenyNonMember } ) {
my $ non_member = $ dbh - > selectrow_array ( "SELECT permissions FROM roles WHERE builtin = 1" ) ;
push @ permissions , $ non_member if $ non_member ;
2011-11-13 13:45:04 +04:00
}
2011-11-13 14:54:39 +04:00
# Look for the permissions
$ res = OK if check_permissions ( $ r , @ permissions ) ;
2011-11-14 14:12:40 +04:00
}
2011-11-13 13:45:04 +04:00
2011-11-13 14:54:39 +04:00
if ( $ res == OK ) {
# Put successful credentials in cache
if ( my $ cache_key = $ r - > pnotes ( "RedmineCacheKey" ) ) {
cache_set ( $ r , $ cache_key ) ;
}
2011-11-12 21:42:19 +04:00
} else {
2011-11-13 14:54:39 +04:00
$ reason = "insufficient permissions (user: '$user', project: '$identifier')" ;
2011-11-12 21:42:19 +04:00
}
2011-11-14 14:12:40 +04:00
}
2011-11-12 21:42:19 +04:00
2011-11-13 14:54:39 +04:00
# Log what we have done
if ( $ res == OK ) {
$ r - > log - > debug ( "access granted: user '" , ( $ r - > user || 'anonymous' ) , "', project '$identifier', method: '" , $ r - > method , "'" ) if $ res == OK ;
} elsif ( defined $ reason ) {
2011-11-14 14:12:40 +04:00
$ r - > log_reason ( $ reason ) ;
2011-11-13 14:54:39 +04:00
}
2012-04-04 18:20:35 +04:00
2011-11-14 14:33:09 +04:00
$ dbh - > disconnect ( ) ;
2011-11-13 13:45:04 +04:00
2011-11-14 14:12:40 +04:00
return $ res ;
2011-11-12 21:42:19 +04:00
}
2012-04-26 18:44:03 +04:00
# get the repository identifier from the URI
sub get_repository_identifier {
2011-11-14 14:49:59 +04:00
my ( $ r ) = @ _ ;
2011-11-13 13:45:04 +04:00
my $ cfg = get_config ( $ r ) ;
2012-04-26 18:44:03 +04:00
my ( $ identifier ) = ( $ r - > uri =~ $ cfg - > { IdentifierRegex } ) if defined $ cfg - > { IdentifierRegex } ;
2011-11-14 14:12:40 +04:00
return $ identifier ;
2011-11-12 21:42:19 +04:00
}
2011-11-13 13:45:04 +04:00
# tell if the given request is a read operation
sub is_read_request {
my $ r = shift ;
2012-04-12 13:07:05 +04:00
my $ cfg = get_config ( $ r ) ;
2012-04-16 14:35:55 +04:00
if ( $ cfg - > { RepositoryType } eq "Subversion" ) {
2012-04-12 13:07:05 +04:00
return defined $ read_only_methods { $ r - > method } ;
} else {
2013-05-27 15:15:23 +04:00
return $ r - > unparsed_uri !~ m @[=/]git-receive-pack@ ;
2012-04-12 13:07:05 +04:00
}
2011-11-13 13:45:04 +04:00
}
# check if one of the required permissions is in the passed list
sub check_permissions {
my $ r = shift ;
my $ permissions = join ( ' ' , @ _ )
or return 0 ;
2011-11-13 14:54:39 +04:00
my $ cfg = get_config ( $ r ) ;
2011-11-13 13:45:04 +04:00
my @ required ;
if ( is_read_request ( $ r ) ) {
@ required = $ cfg - > { ReadPermissions } || @ default_read_permissions ;
} else {
@ required = $ cfg - > { WritePermissions } || @ default_write_permissions ;
}
foreach ( @ required ) {
return 1 if $ permissions =~ m {\Q$_\E} ;
}
return 0 ;
}
2011-11-12 21:42:19 +04:00
# return module configuration for current directory
sub get_config {
my $ r = shift ;
2011-11-13 13:45:04 +04:00
return Apache2::Module:: get_config ( __PACKAGE__ , $ r - > server , $ r - > per_dir_config ) ;
2011-11-12 21:42:19 +04:00
}
# get a connection to the redmine database
sub connect_database {
2011-11-13 13:45:04 +04:00
my $ r = shift ;
2011-11-12 21:42:19 +04:00
my $ cfg = get_config ( $ r ) ;
2011-11-14 14:49:34 +04:00
my $ dbh = DBI - > connect ( $ cfg - > { DSN } , $ cfg - > { DbUser } , $ cfg - > { DbPass } )
or $ r - > log - > error ( "Connection to database failed: $DBI::errstr." ) ;
2012-04-04 18:20:35 +04:00
2011-11-14 14:49:34 +04:00
return $ dbh ;
2011-11-12 21:42:19 +04:00
}
# tell if a value returned from SQL is "true"
sub is_true {
my $ value = shift ;
2011-11-14 14:12:40 +04:00
return defined ( $ value ) && ( $ value == 1 || $ value eq "t" ) ;
2011-11-12 21:42:19 +04:00
}
# build credential cache key
sub get_cache_key {
my ( $ r , $ password ) = @ _ ;
2012-04-26 18:44:03 +04:00
my $ identifier = get_config ( $ r ) - > { Project } || get_repository_identifier ( $ r )
2012-04-04 19:07:11 +04:00
or return ;
2012-04-26 18:44:03 +04:00
return Digest::SHA:: sha1_hex ( join ( ':' , $ identifier , $ r - > user , $ password , is_read_request ( $ r ) ? 'read' : 'write' ) ) ;
2011-11-12 21:42:19 +04:00
}
# check if credentials exist in cache
sub cache_get {
my ( $ r , $ key ) = @ _ ;
2011-11-13 13:45:04 +04:00
2011-11-12 21:42:19 +04:00
my $ cfg = get_config ( $ r ) ;
2012-04-04 19:09:52 +04:00
return unless $ cfg - > { CacheCredsMax } && $ cfg - > { CacheCreds } ;
2011-11-13 13:45:04 +04:00
2011-11-13 14:54:39 +04:00
my $ time = $ cfg - > { CacheCreds } - > get ( $ key )
2011-11-13 13:45:04 +04:00
or return 0 ;
if ( $ cfg - > { CacheCredsMaxAge } && ( $ r - > request_time - $ time ) > $ cfg - > { CacheCredsMaxAge } ) {
2011-11-13 14:54:39 +04:00
$ cfg - > { CacheCreds } - > unset ( $ key ) ;
2011-11-13 13:45:04 +04:00
$ cfg - > { CacheCredsCount } - - ;
2011-11-12 21:42:19 +04:00
return 0 ;
}
2011-11-13 13:45:04 +04:00
return 1 ;
2011-11-12 21:42:19 +04:00
}
# put credentials in cache
sub cache_set {
my ( $ r , $ key ) = @ _ ;
2011-11-13 13:45:04 +04:00
2011-11-12 21:42:19 +04:00
my $ cfg = get_config ( $ r ) ;
2011-11-13 14:54:39 +04:00
return unless $ cfg - > { CacheCredsMax } ;
2011-11-13 13:45:04 +04:00
2012-04-04 19:09:52 +04:00
unless ( $ cfg - > { CacheCreds } ) {
$ cfg - > { CachePool } = APR::Pool - > new ;
$ cfg - > { CacheCreds } = APR::Table:: make ( $ cfg - > { CachePool } , $ cfg - > { CacheCredsMax } ) ;
}
2011-11-13 13:45:04 +04:00
if ( $ cfg - > { CacheCredsCount } >= $ cfg - > { CacheCredsMax } ) {
2011-11-13 14:54:39 +04:00
$ cfg - > { CacheCreds } - > clear ;
2011-11-13 13:45:04 +04:00
$ cfg - > { CacheCredsCount } = 0 ;
2011-11-12 21:42:19 +04:00
}
2011-11-13 14:54:39 +04:00
$ cfg - > { CacheCreds } - > set ( $ key , $ r - > request_time ) ;
2011-11-13 13:45:04 +04:00
$ cfg - > { CacheCredsCount } + + ;
2011-11-12 21:42:19 +04:00
}
1 ;
2016-04-05 14:49:47 +03:00
# vim: set noexpandtab ts=4