Another wonderful version.
parent
eeb2877ee4
commit
e1577bc897
171
Redmine.pm
171
Redmine.pm
|
@ -173,52 +173,86 @@ my @directives = (
|
||||||
args_how => TAKE1,
|
args_how => TAKE1,
|
||||||
errmsg => 'Maximum age of cached credentials. Defaults to 300. Set to 0 to disable credential expiration.',
|
errmsg => 'Maximum age of cached credentials. Defaults to 300. Set to 0 to disable credential expiration.',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
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.',
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
sub RedmineDSN {
|
# Initialize defaults configuration
|
||||||
my ($cfg, $parms, $arg) = @_;
|
sub DIR_CREATE {
|
||||||
$cfg->{DSN} = $arg;
|
my($class, $parms) = @_;
|
||||||
$cfg->{Query} = trim("
|
return bless {
|
||||||
SELECT permissions FROM users, members, member_roles, roles
|
PermissionQuery => trim("
|
||||||
WHERE users.login = ?
|
SELECT permissions FROM users, members, member_roles, roles
|
||||||
AND users.id = members.user_id
|
WHERE users.login = ?
|
||||||
AND users.status = 1
|
AND users.id = members.user_id
|
||||||
AND members.project_id = ?
|
AND users.status = 1
|
||||||
AND members.id = member_roles.member_id
|
AND members.project_id = ?
|
||||||
AND member_roles.role_id = roles.id
|
AND members.id = member_roles.member_id
|
||||||
");
|
AND member_roles.role_id = roles.id
|
||||||
|
"),
|
||||||
|
CacheCredsMax => 0,
|
||||||
|
CacheCredsCount => 0,
|
||||||
|
CacheCredsMaxAge => 300,
|
||||||
|
DenyAnonymous => 0,
|
||||||
|
DenyNonMember => 0,
|
||||||
|
SuperAdmin => 1,
|
||||||
|
}, $class;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Simple setters
|
||||||
|
sub RedmineDSN { set_val('DSN', @_); }
|
||||||
sub RedmineDbUser { set_val('DbUser', @_); }
|
sub RedmineDbUser { set_val('DbUser', @_); }
|
||||||
sub RedmineDbPass { set_val('DbPass', @_); }
|
sub RedmineDbPass { set_val('DbPass', @_); }
|
||||||
sub RedmineCacheCredsMaxAge { set_val('CacheCredsMaxAge', @_); }
|
|
||||||
sub RedmineProject { set_val('Project', @_); }
|
sub RedmineProject { set_val('Project', @_); }
|
||||||
|
sub RedmineReadPermissions { push_val('ReadPermissions', @_); }
|
||||||
|
sub RedmineWritePermissions { push_val('WritePermissions', @_); }
|
||||||
|
sub RedmineCacheCredsMaxAge { set_val('CacheCredsMaxAge', @_); }
|
||||||
|
sub RedmineDenyAnonymous { set_val('DenyAnonymous', @_); }
|
||||||
|
sub RedmineDenyNonMember { set_val('DenyNonMember', @_); }
|
||||||
|
sub RedmineSuperAdmin { set_val('SuperAdmin', @_); }
|
||||||
|
|
||||||
sub RedmineDbWhereClause {
|
sub RedmineDbWhereClause {
|
||||||
my ($cfg, $parms, $arg) = @_;
|
my ($cfg, $parms, $arg) = @_;
|
||||||
$cfg->{Query} = trim($cfg->{Query}.($arg || "")." ");
|
if($arg) {
|
||||||
|
$cfg->{PermissionQuery} = trim($cfg->{PermissionQuery}."$arg ");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sub RedmineCacheCredsMax {
|
sub RedmineCacheCredsMax {
|
||||||
my ($cfg, $parms, $arg) = @_;
|
my ($cfg, $parms, $arg) = @_;
|
||||||
if ($arg) {
|
if ($arg) {
|
||||||
$cfg->{CachePool} = APR::Pool->new;
|
unless($cfg->{CachePool}) {
|
||||||
$cfg->{CacheCreds} = APR::Table::make($cfg->{CachePool}, $arg);
|
$cfg->{CachePool} = APR::Pool->new;
|
||||||
$cfg->{CacheCredsCount} = 0;
|
$cfg->{CacheCreds} = APR::Table::make($cfg->{CachePool}, $arg);
|
||||||
|
}
|
||||||
$cfg->{CacheCredsMax} = $arg;
|
$cfg->{CacheCredsMax} = $arg;
|
||||||
$cfg->{CacheCredsMaxAge} ||= 300;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sub set_val {
|
||||||
sub RedmineReadPermissions {
|
my ($key, $cfg, $parms, $arg) = @_;
|
||||||
my ( $cfg, $parms, $arg ) = @_;
|
$cfg->{$key} = $arg;
|
||||||
push @{ $cfg->{ReadPermissions} }, $arg;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sub RedmineWritePermissions {
|
sub push_val {
|
||||||
my ( $cfg, $parms, $arg ) = @_;
|
my ($key, $cfg, $parms, $arg) = @_;
|
||||||
push @{ $cfg->{WritePermissions} }, $arg;
|
push @{ $cfg->{$key} }, $arg;
|
||||||
}
|
}
|
||||||
|
|
||||||
sub trim {
|
sub trim {
|
||||||
|
@ -227,11 +261,6 @@ sub trim {
|
||||||
return $string;
|
return $string;
|
||||||
}
|
}
|
||||||
|
|
||||||
sub set_val {
|
|
||||||
my ($key, $self, $parms, $arg) = @_;
|
|
||||||
$self->{$key} = $arg;
|
|
||||||
}
|
|
||||||
|
|
||||||
Apache2::Module::add(__PACKAGE__, \@directives);
|
Apache2::Module::add(__PACKAGE__, \@directives);
|
||||||
|
|
||||||
my %read_only_methods = map { $_ => 1 } qw/GET PROPFIND REPORT OPTIONS/;
|
my %read_only_methods = map { $_ => 1 } qw/GET PROPFIND REPORT OPTIONS/;
|
||||||
|
@ -270,7 +299,9 @@ sub authen_handler {
|
||||||
|
|
||||||
} elsif($res == AUTH_REQUIRED) {
|
} elsif($res == AUTH_REQUIRED) {
|
||||||
my $dbh = connect_database($r);
|
my $dbh = connect_database($r);
|
||||||
if(is_authentication_forced($dbh)) {
|
my $cfg = get_config($r);
|
||||||
|
|
||||||
|
if(!$cfg->{AllowAnonymous} || is_authentication_forced($dbh)) {
|
||||||
# We really want an user
|
# We really want an user
|
||||||
$reason = 'anonymous access disabled';
|
$reason = 'anonymous access disabled';
|
||||||
} else {
|
} else {
|
||||||
|
@ -292,7 +323,7 @@ sub check_login {
|
||||||
my ($r, $dbh, $password) = @_;
|
my ($r, $dbh, $password) = @_;
|
||||||
my $user = $r->user;
|
my $user = $r->user;
|
||||||
|
|
||||||
my ($hashed_password, $status, $auth_source_id, $salt) = $dbh->selectrow_arrayref('SELECT hashed_password, status, auth_source_id, salt FROM users WHERE login = ?', $user);
|
my ($hashed_password, $status, $auth_source_id, $salt) = $dbh->selectrow_array('SELECT hashed_password, status, auth_source_id, salt FROM users WHERE login = ?', undef, $user);
|
||||||
|
|
||||||
# Not found
|
# Not found
|
||||||
return (AUTH_REQUIRED, "unknown user '$user'") unless defined($hashed_password);
|
return (AUTH_REQUIRED, "unknown user '$user'") unless defined($hashed_password);
|
||||||
|
@ -349,7 +380,7 @@ sub check_login {
|
||||||
# check if authentication is forced
|
# check if authentication is forced
|
||||||
sub is_authentication_forced {
|
sub is_authentication_forced {
|
||||||
my $dbh = shift;
|
my $dbh = shift;
|
||||||
return is_true($dbh->selectrow_arrayref("SELECT value FROM settings WHERE settings.name = 'login_required'"));
|
return is_true($dbh->selectrow_array("SELECT value FROM settings WHERE settings.name = 'login_required'"));
|
||||||
}
|
}
|
||||||
|
|
||||||
sub authz_handler {
|
sub authz_handler {
|
||||||
|
@ -361,6 +392,7 @@ sub authz_handler {
|
||||||
}
|
}
|
||||||
|
|
||||||
my $dbh = connect_database($r);
|
my $dbh = connect_database($r);
|
||||||
|
my $cfg = get_config($r);
|
||||||
|
|
||||||
my ($identifier, $project_id, $is_public, $status) = get_project_data($r, $dbh);
|
my ($identifier, $project_id, $is_public, $status) = get_project_data($r, $dbh);
|
||||||
$is_public = is_true($is_public);
|
$is_public = is_true($is_public);
|
||||||
|
@ -381,10 +413,10 @@ sub authz_handler {
|
||||||
$res = AUTH_REQUIRED;
|
$res = AUTH_REQUIRED;
|
||||||
$reason = "anonymous access to '$identifier' denied";
|
$reason = "anonymous access to '$identifier' denied";
|
||||||
|
|
||||||
if($is_public) {
|
if($is_public && !$cfg->{DenyAnonymous}) {
|
||||||
# Check anonymous permissions
|
# Check anonymous permissions
|
||||||
my ($permissions) = $dbh->selectrow_arrayref("SELECT permissions FROM roles WHERE builtin = 2");
|
my $permissions = $dbh->selectrow_array("SELECT permissions FROM roles WHERE builtin = 2");
|
||||||
$res = OK if check_permissions($r, $permissions);
|
$res = OK if $permissions && check_permissions($r, $permissions);
|
||||||
}
|
}
|
||||||
|
|
||||||
# Force login if failed
|
# Force login if failed
|
||||||
|
@ -392,35 +424,47 @@ sub authz_handler {
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
# Logged in user
|
# Logged in user
|
||||||
my @permissions = ();
|
|
||||||
my $user = $r->user;
|
my $user = $r->user;
|
||||||
|
|
||||||
# Membership permissions
|
if($cfg->{SuperAdmin} && is_true($dbh->selectrow_array("SELECT admin FROM users WHERE login = ?", undef, $user))) {
|
||||||
if(my @membership = $dbh->selectcol_arrayref($cfg->{Query}, $user, $project_id)) {
|
# Adminstrators have all the rights
|
||||||
push @permissions, @membership;
|
$res = OK;
|
||||||
}
|
|
||||||
|
} else {
|
||||||
|
# Really check user permissions
|
||||||
|
my @permissions = ();
|
||||||
|
|
||||||
|
# Membership permissions
|
||||||
|
my $membership = $dbh->selectcol_arrayref($cfg->{PermissionQuery}, undef, $user, $project_id);
|
||||||
|
push @permissions, @{$membership} if $membership;
|
||||||
|
|
||||||
if($is_public) {
|
|
||||||
# Add non-member permissions for public projects
|
# Add non-member permissions for public projects
|
||||||
if(my ($non_member) = $dbh->selectrow_arrayref("SELECT permissions FROM roles WHERE builtin = 1")) {
|
if($is_public && !$cfg->{DenyNonMember}) {
|
||||||
push @permissions, $non_member;
|
my $non_member = $dbh->selectrow_array("SELECT permissions FROM roles WHERE builtin = 1");
|
||||||
|
push @permissions, $non_member if $non_member;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if(check_permissions($r, @permissions)) {
|
# Look for the permissions
|
||||||
$res = OK;
|
$res = OK if check_permissions($r, @permissions);
|
||||||
|
}
|
||||||
|
|
||||||
my $cache_key = $r->pnotes("RedmineCacheKey");
|
if($res == OK) {
|
||||||
cache_set($r, $cache_key) if defined $cache_key;
|
# Put successful credentials in cache
|
||||||
|
if(my $cache_key = $r->pnotes("RedmineCacheKey")) {
|
||||||
|
cache_set($r, $cache_key);
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
$reason = "insufficient permissions (user: '$user', project: '$identifier', required: '$required')";
|
$reason = "insufficient permissions (user: '$user', project: '$identifier')";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$r->log->debug("access granted: user '", ($r->user || 'anonymous'), "', project '$identifier', method: '", $r->method, "'") if $res == OK;
|
# Log what we have done
|
||||||
|
if($res == OK) {
|
||||||
$r->log_reason($reason) if $res != OK && defined $reason;
|
$r->log->debug("access granted: user '", ($r->user || 'anonymous'), "', project '$identifier', method: '", $r->method, "'") if $res == OK;
|
||||||
|
} elsif(defined $reason) {
|
||||||
|
$r->log_reason($reason);
|
||||||
|
}
|
||||||
|
|
||||||
return $res;
|
return $res;
|
||||||
}
|
}
|
||||||
|
@ -453,7 +497,7 @@ sub check_permissions {
|
||||||
my $permissions = join(' ', @_)
|
my $permissions = join(' ', @_)
|
||||||
or return 0;
|
or return 0;
|
||||||
|
|
||||||
$cfg = get_config($r);
|
my $cfg = get_config($r);
|
||||||
my @required;
|
my @required;
|
||||||
if(is_read_request($r)) {
|
if(is_read_request($r)) {
|
||||||
@required = $cfg->{ReadPermissions} || @default_read_permissions;
|
@required = $cfg->{ReadPermissions} || @default_read_permissions;
|
||||||
|
@ -474,7 +518,7 @@ sub get_project_data {
|
||||||
my $dbh = shift;
|
my $dbh = shift;
|
||||||
|
|
||||||
my $identifier = get_project_identifier($r);
|
my $identifier = get_project_identifier($r);
|
||||||
return $identifier, $dbh->selectrow_arrayref("SELECT id, is_public, status FROM projects WHERE identifier = ?", $identifier);
|
return $identifier, $dbh->selectrow_array("SELECT id, is_public, status FROM projects WHERE identifier = ?", undef, $identifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
# return module configuration for current directory
|
# return module configuration for current directory
|
||||||
|
@ -501,7 +545,7 @@ sub is_true {
|
||||||
# build credential cache key
|
# build credential cache key
|
||||||
sub get_cache_key {
|
sub get_cache_key {
|
||||||
my ($r, $password) = @_;
|
my ($r, $password) = @_;
|
||||||
return Digest::SHA1::sha1_hex(join(':', get_project_identifier($r), $r->user, $password, is_read_request($r) ? 'read' : 'write');
|
return Digest::SHA1::sha1_hex(join(':', get_project_identifier($r), $r->user, $password, is_read_request($r) ? 'read' : 'write'));
|
||||||
}
|
}
|
||||||
|
|
||||||
# check if credentials exist in cache
|
# check if credentials exist in cache
|
||||||
|
@ -509,14 +553,13 @@ sub cache_get {
|
||||||
my($r, $key) = @_;
|
my($r, $key) = @_;
|
||||||
|
|
||||||
my $cfg = get_config($r);
|
my $cfg = get_config($r);
|
||||||
my $cache = $cfg->{CacheCreds};
|
return unless $cfg->{CacheCredsMax};
|
||||||
return unless $cache;
|
|
||||||
|
|
||||||
my $time = $cache->get($key)
|
my $time = $cfg->{CacheCreds}->get($key)
|
||||||
or return 0;
|
or return 0;
|
||||||
|
|
||||||
if($cfg->{CacheCredsMaxAge} && ($r->request_time - $time) > $cfg->{CacheCredsMaxAge}) {
|
if($cfg->{CacheCredsMaxAge} && ($r->request_time - $time) > $cfg->{CacheCredsMaxAge}) {
|
||||||
$cache->unset($key);
|
$cfg->{CacheCreds}->unset($key);
|
||||||
$cfg->{CacheCredsCount}--;
|
$cfg->{CacheCredsCount}--;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
@ -528,16 +571,16 @@ sub cache_set {
|
||||||
my($r, $key) = @_;
|
my($r, $key) = @_;
|
||||||
|
|
||||||
my $cfg = get_config($r);
|
my $cfg = get_config($r);
|
||||||
my $cache = $cfg->{CacheCreds};
|
return unless $cfg->{CacheCredsMax};
|
||||||
return unless $cache;
|
|
||||||
|
|
||||||
if($cfg->{CacheCredsCount} >= $cfg->{CacheCredsMax}) {
|
if($cfg->{CacheCredsCount} >= $cfg->{CacheCredsMax}) {
|
||||||
$cache->clear;
|
$cfg->{CacheCreds}->clear;
|
||||||
$cfg->{CacheCredsCount} = 0;
|
$cfg->{CacheCredsCount} = 0;
|
||||||
}
|
}
|
||||||
$cache->set($key, $r->request_time);
|
$cfg->{CacheCreds}->set($key, $r->request_time);
|
||||||
$cfg->{CacheCredsCount}++;
|
$cfg->{CacheCredsCount}++;
|
||||||
}
|
}
|
||||||
|
|
||||||
1;
|
1;
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue