Compare commits
5 Commits
master
...
api-key-au
Author | SHA1 | Date |
---|---|---|
inpos | 7842558576 | |
inpos | c3b2b5a22f | |
inpos | 3b5b094b3e | |
Бородин Роман | 6f85952dc3 | |
Guillaume Perréal | 47e5703f64 |
12
README.pod
12
README.pod
|
@ -97,10 +97,18 @@ Authen::Simple::LDAP (and IO::Socket::SSL if LDAPS is used):
|
||||||
## Default: Off
|
## Default: Off
|
||||||
# RedmineDenyNonMember On
|
# RedmineDenyNonMember On
|
||||||
|
|
||||||
|
## Allow authentication by API key
|
||||||
|
## Default: Off
|
||||||
|
# RedmineKeyAuthentication On
|
||||||
|
|
||||||
|
## Username for authentication by API key
|
||||||
|
## Default: api-key
|
||||||
|
# RedmineKeyUsername key
|
||||||
|
|
||||||
## Administrators have super-powers
|
## Administrators have super-powers
|
||||||
## Default: On
|
## Default: On
|
||||||
# RedmineSuperAdmin Off
|
# RedmineSuperAdmin Off
|
||||||
|
|
||||||
## Sets firstname, lastname, email address to environment variables.
|
## Sets firstname, lastname, email address to environment variables.
|
||||||
## Default: Off
|
## Default: Off
|
||||||
# RedmineSetUserAttributes Off
|
# RedmineSetUserAttributes Off
|
||||||
|
@ -136,6 +144,4 @@ S<them :>
|
||||||
|
|
||||||
And you need to upgrade at least reposman.rb (after r860).
|
And you need to upgrade at least reposman.rb (after r860).
|
||||||
|
|
||||||
|
|
||||||
=cut
|
=cut
|
||||||
|
|
||||||
|
|
177
Redmine.pm
177
Redmine.pm
|
@ -63,7 +63,7 @@ Authen::Simple::LDAP (and IO::Socket::SSL if LDAPS is used):
|
||||||
# RedmineDbWhereClause "and members.role_id IN (1,2)"
|
# RedmineDbWhereClause "and members.role_id IN (1,2)"
|
||||||
|
|
||||||
## SCM transport protocol, used to detecte write requests
|
## SCM transport protocol, used to detecte write requests
|
||||||
## Valid values: Subversion, Git, None
|
## Valid values: Subversion, Git
|
||||||
## Default: Subversion
|
## Default: Subversion
|
||||||
# RedmineRepositoryType Subversion
|
# RedmineRepositoryType Subversion
|
||||||
|
|
||||||
|
@ -99,6 +99,14 @@ Authen::Simple::LDAP (and IO::Socket::SSL if LDAPS is used):
|
||||||
## Default: Off
|
## Default: Off
|
||||||
# RedmineDenyNonMember On
|
# RedmineDenyNonMember On
|
||||||
|
|
||||||
|
## Allow authentication by API key
|
||||||
|
## Default: Off
|
||||||
|
# RedmineKeyAuthentication On
|
||||||
|
|
||||||
|
## Username for authentication by API key
|
||||||
|
## Default: api-key
|
||||||
|
# RedmineKeyUsername key
|
||||||
|
|
||||||
## Administrators have super-powers
|
## Administrators have super-powers
|
||||||
## Default: On
|
## Default: On
|
||||||
# RedmineSuperAdmin Off
|
# RedmineSuperAdmin Off
|
||||||
|
@ -237,7 +245,19 @@ my @directives = (
|
||||||
name => 'RedmineRepositoryType',
|
name => 'RedmineRepositoryType',
|
||||||
req_override => OR_AUTHCFG,
|
req_override => OR_AUTHCFG,
|
||||||
args_how => TAKE1,
|
args_how => TAKE1,
|
||||||
errmsg => 'Indicate the type of Repository (Subversion or Git or None). This is used to properly detected write requests. Defaults to Subversion.',
|
errmsg => 'Indicate the type of Repository (Subversion or Git). This is used to properly detected write requests. Defaults to Subversion.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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.'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name => 'RedmineSetUserAttributes',
|
name => 'RedmineSetUserAttributes',
|
||||||
|
@ -269,6 +289,8 @@ sub DIR_CREATE {
|
||||||
DenyAnonymous => 0,
|
DenyAnonymous => 0,
|
||||||
DenyNonMember => 0,
|
DenyNonMember => 0,
|
||||||
SuperAdmin => 1,
|
SuperAdmin => 1,
|
||||||
|
KeyAuthentication => 0,
|
||||||
|
KeyUsername => 'api-key',
|
||||||
SetUserAttributes => 0,
|
SetUserAttributes => 0,
|
||||||
AttributesCacheCredsCount => 0,
|
AttributesCacheCredsCount => 0,
|
||||||
}, $class;
|
}, $class;
|
||||||
|
@ -286,6 +308,8 @@ sub RedmineCacheCredsMaxAge { set_val('CacheCredsMaxAge', @_); }
|
||||||
sub RedmineDenyAnonymous { set_val('DenyAnonymous', @_); }
|
sub RedmineDenyAnonymous { set_val('DenyAnonymous', @_); }
|
||||||
sub RedmineDenyNonMember { set_val('DenyNonMember', @_); }
|
sub RedmineDenyNonMember { set_val('DenyNonMember', @_); }
|
||||||
sub RedmineSuperAdmin { set_val('SuperAdmin', @_); }
|
sub RedmineSuperAdmin { set_val('SuperAdmin', @_); }
|
||||||
|
sub RedmineKeyAuthentication { set_val('KeyAuthentication', @_); }
|
||||||
|
sub RedmineKeyUsername { set_val('KeyUsername', @_); }
|
||||||
sub RedmineSetUserAttributes { set_val('SetUserAttributes', @_); }
|
sub RedmineSetUserAttributes { set_val('SetUserAttributes', @_); }
|
||||||
|
|
||||||
sub RedmineDbWhereClause {
|
sub RedmineDbWhereClause {
|
||||||
|
@ -300,10 +324,8 @@ sub RedmineRepositoryType {
|
||||||
$arg = trim($arg);
|
$arg = trim($arg);
|
||||||
if($arg eq 'Subversion' || $arg eq 'Git') {
|
if($arg eq 'Subversion' || $arg eq 'Git') {
|
||||||
$cfg->{RepositoryType} = 'Repository::'.$arg;
|
$cfg->{RepositoryType} = 'Repository::'.$arg;
|
||||||
} elsif($arg eq 'None') {
|
|
||||||
$cfg->{RepositoryType} = $arg;
|
|
||||||
} else {
|
} else {
|
||||||
die "Invalid RedmineRepositoryType value: $arg, choose either Subversion or Git or None !";
|
die "Invalid RedmineRepositoryType value: $arg, choose either Subversion or Git !";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -350,6 +372,7 @@ sub authen_handler {
|
||||||
if(defined $cache_key && !$cfg->{SetUserAttributes} && cache_get($r, $cache_key)) {
|
if(defined $cache_key && !$cfg->{SetUserAttributes} && cache_get($r, $cache_key)) {
|
||||||
$r->log->debug("reusing cached credentials for user '", $r->user, "'");
|
$r->log->debug("reusing cached credentials for user '", $r->user, "'");
|
||||||
$r->set_handlers(PerlAuthzHandler => undef);
|
$r->set_handlers(PerlAuthzHandler => undef);
|
||||||
|
attributes_cache_get($r, $cache_key);
|
||||||
|
|
||||||
} elsif(defined $cache_key && $cfg->{SetUserAttributes} && cache_get($r, $cache_key) && attributes_cache_get($r, $cache_key)) {
|
} elsif(defined $cache_key && $cfg->{SetUserAttributes} && cache_get($r, $cache_key) && attributes_cache_get($r, $cache_key)) {
|
||||||
$r->log->debug("reusing cached credentials for user '", $r->user, "' including attributes");
|
$r->log->debug("reusing cached credentials for user '", $r->user, "' including attributes");
|
||||||
|
@ -360,7 +383,7 @@ sub authen_handler {
|
||||||
my $dbh = connect_database($r)
|
my $dbh = connect_database($r)
|
||||||
or return SERVER_ERROR;
|
or return SERVER_ERROR;
|
||||||
|
|
||||||
($res, $reason) = check_login($r, $dbh, $password, $cfg);
|
($res, $reason) = check_login($r, $dbh, $password);
|
||||||
$dbh->disconnect();
|
$dbh->disconnect();
|
||||||
|
|
||||||
# Store the cache key for latter use
|
# Store the cache key for latter use
|
||||||
|
@ -390,53 +413,40 @@ sub authen_handler {
|
||||||
return $res;
|
return $res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
sub check_login {
|
sub check_login {
|
||||||
my ($r, $dbh, $password, $cfg) = @_;
|
my ($r, $dbh, $password) = @_;
|
||||||
my $user = $r->user;
|
my $user = $r->user;
|
||||||
|
my $status;
|
||||||
|
|
||||||
my ($hashed_password, $status, $auth_source_id, $salt, $id, $firstname, $lastname, $email_address) =
|
my $cfg = get_config($r);
|
||||||
$dbh->selectrow_array('SELECT users.hashed_password, users.status, users.auth_source_id, users.salt, users.id, users.firstname, users.lastname, email_addresses.address
|
|
||||||
FROM users
|
|
||||||
LEFT JOIN email_addresses on (email_addresses.user_id=users.id and email_addresses.is_default=1)
|
|
||||||
WHERE users.login = ?', undef, $user)
|
|
||||||
or return (AUTH_REQUIRED, "unknown user '$user'");
|
|
||||||
|
|
||||||
# Check password
|
my ($hashed_password, $auth_source_id, $salt, $id, $firstname, $lastname, $email_address);
|
||||||
if($auth_source_id) {
|
|
||||||
# LDAP authentication
|
|
||||||
|
|
||||||
# Ensure Authen::Simple::LDAP is available
|
if ($cfg->{KeyAuthentication} && $user eq $cfg->{KeyUsername}) {
|
||||||
return (SERVER_ERROR, "Redmine LDAP authentication requires Authen::Simple::LDAP")
|
# API key auth
|
||||||
unless $CanUseLDAPAuth;
|
($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'");
|
||||||
# Get LDAP server informations
|
$r->user($user);
|
||||||
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);
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
# Database authentication
|
# Login+password auth
|
||||||
my $pass_digest = Digest::SHA::sha1_hex($password);
|
($hashed_password, $status, $auth_source_id, $salt, $id, $firstname, $lastname, $email_address) =
|
||||||
return (AUTH_REQUIRED, "wrong password for '$user'")
|
$dbh->selectrow_array('SELECT users.hashed_password, users.status, users.auth_source_id, users.salt, users.id, users.firstname, users.lastname, email_addresses.address
|
||||||
unless $hashed_password eq Digest::SHA::sha1_hex($salt.$pass_digest);
|
FROM users
|
||||||
|
LEFT JOIN email_addresses on (email_addresses.user_id=users.id and email_addresses.is_default = true)
|
||||||
|
WHERE users.login = ?', undef, $user)
|
||||||
|
or return (AUTH_REQUIRED, "unknown user '$user'");
|
||||||
|
|
||||||
|
my ($res, $reason);
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Password is ok, check if account if locked
|
# Password is ok, check if account if locked
|
||||||
|
@ -450,6 +460,7 @@ sub check_login {
|
||||||
}
|
}
|
||||||
$r->subprocess_env->set("REDMINE_FIRSTNAME" => $firstname);
|
$r->subprocess_env->set("REDMINE_FIRSTNAME" => $firstname);
|
||||||
$r->subprocess_env->set("REDMINE_LASTNAME" => $lastname);
|
$r->subprocess_env->set("REDMINE_LASTNAME" => $lastname);
|
||||||
|
|
||||||
$r->log->debug("successfully authenticated as active redmine user '$user'");
|
$r->log->debug("successfully authenticated as active redmine user '$user'");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -457,6 +468,52 @@ sub check_login {
|
||||||
return OK;
|
return OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
my ($user, $password, $hashed_password, $salt) = @_ ;
|
||||||
|
|
||||||
|
# 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# check if authentication is forced
|
# check if authentication is forced
|
||||||
sub is_authentication_forced {
|
sub is_authentication_forced {
|
||||||
my $dbh = shift;
|
my $dbh = shift;
|
||||||
|
@ -478,23 +535,7 @@ sub authz_handler {
|
||||||
|
|
||||||
my ($identifier, $project_id, $is_public, $status);
|
my ($identifier, $project_id, $is_public, $status);
|
||||||
|
|
||||||
if($cfg->{RepositoryType} eq 'None') {
|
if($identifier = $cfg->{Project}) {
|
||||||
$identifier = $cfg->{Project};
|
|
||||||
if(!$cfg->{Project}) {
|
|
||||||
return FORBIDDEN;
|
|
||||||
}
|
|
||||||
($project_id, $is_public, $status) = $dbh->selectrow_array(
|
|
||||||
"SELECT p.id, p.is_public, p.status
|
|
||||||
FROM projects p
|
|
||||||
WHERE p.identifier = ?",
|
|
||||||
undef, $identifier
|
|
||||||
);
|
|
||||||
unless(defined $project_id) {
|
|
||||||
$r->log_reason("No matching project for ${identifier}");
|
|
||||||
return NOT_FOUND;
|
|
||||||
}
|
|
||||||
|
|
||||||
} elsif($identifier = $cfg->{Project}) {
|
|
||||||
($project_id, $is_public, $status) = $dbh->selectrow_array(
|
($project_id, $is_public, $status) = $dbh->selectrow_array(
|
||||||
"SELECT p.id, p.is_public, p.status
|
"SELECT p.id, p.is_public, p.status
|
||||||
FROM projects p JOIN repositories r ON (p.id = r.project_id)
|
FROM projects p JOIN repositories r ON (p.id = r.project_id)
|
||||||
|
@ -507,11 +548,12 @@ sub authz_handler {
|
||||||
}
|
}
|
||||||
|
|
||||||
} elsif(my $repo_id = get_repository_identifier($r)) {
|
} elsif(my $repo_id = get_repository_identifier($r)) {
|
||||||
|
my @pr_id = split(/\./, $repo_id);
|
||||||
($identifier, $project_id, $is_public, $status) = $dbh->selectrow_array(
|
($identifier, $project_id, $is_public, $status) = $dbh->selectrow_array(
|
||||||
"SELECT p.identifier, p.id, p.is_public, p.status
|
"SELECT p.identifier, p.id, p.is_public, p.status
|
||||||
FROM projects p JOIN repositories r ON (p.id = r.project_id)
|
FROM projects p JOIN repositories r ON (p.id = r.project_id)
|
||||||
WHERE ((r.is_default AND p.identifier = ?) OR r.identifier = ?) AND r.type = ?",
|
WHERE ((r.is_default AND p.identifier = ?) OR r.identifier = ?) AND r.type = ?",
|
||||||
undef, $repo_id, $repo_id, $cfg->{RepositoryType}
|
undef, $pr_id[0], $repo_id, $cfg->{RepositoryType}
|
||||||
);
|
);
|
||||||
unless(defined $project_id) {
|
unless(defined $project_id) {
|
||||||
$r->log_reason("No matching project for ${repo_id}");
|
$r->log_reason("No matching project for ${repo_id}");
|
||||||
|
@ -697,8 +739,9 @@ sub attributes_cache_get {
|
||||||
return unless $cfg->{CacheCredsMax} && $cfg->{AttributesCacheCreds};
|
return unless $cfg->{CacheCredsMax} && $cfg->{AttributesCacheCreds};
|
||||||
|
|
||||||
my $cache_text = $cfg->{AttributesCacheCreds}->get($key)
|
my $cache_text = $cfg->{AttributesCacheCreds}->get($key)
|
||||||
or return 0;
|
or return 0;
|
||||||
|
|
||||||
|
$r->log->error("cache_text:$cache_text");
|
||||||
my($time, $email_address, $firstname, $lastname) = split(":", $cache_text);
|
my($time, $email_address, $firstname, $lastname) = split(":", $cache_text);
|
||||||
if($cfg->{CacheCredsMaxAge} && ($r->request_time - $time) > $cfg->{CacheCredsMaxAge}) {
|
if($cfg->{CacheCredsMaxAge} && ($r->request_time - $time) > $cfg->{CacheCredsMaxAge}) {
|
||||||
$cfg->{AttributesCacheCreds}->unset($key);
|
$cfg->{AttributesCacheCreds}->unset($key);
|
||||||
|
@ -741,7 +784,7 @@ sub attributes_cache_set {
|
||||||
|
|
||||||
unless($cfg->{AttributesCacheCreds}) {
|
unless($cfg->{AttributesCacheCreds}) {
|
||||||
$cfg->{AttributesCachePool} = APR::Pool->new;
|
$cfg->{AttributesCachePool} = APR::Pool->new;
|
||||||
$cfg->{AttributesCacheCreds} = APR::Table::make($cfg->{AttributesCachePool}, $cfg->{CacheCredsMax});
|
$cfg->{AttributesCacheCreds} = APR::Table::make($cfg->{AttributesCachePool}, $cfg->{CacheCredsMax});
|
||||||
}
|
}
|
||||||
|
|
||||||
if($cfg->{AttributesCacheCredsCount} >= $cfg->{CacheCredsMax}) {
|
if($cfg->{AttributesCacheCredsCount} >= $cfg->{CacheCredsMax}) {
|
||||||
|
@ -759,4 +802,4 @@ sub attributes_cache_set {
|
||||||
|
|
||||||
1;
|
1;
|
||||||
|
|
||||||
|
# vim: set noexpandtab ts=4
|
||||||
|
|
Loading…
Reference in New Issue