Merge pull request #1151 from DeterminateSystems/ldap-tests-inc-mapping

ldap: support configurable roles with backwards compatibility
This commit is contained in:
Graham Christensen 2022-02-14 09:30:54 -05:00 committed by GitHub
commit 1124230d9f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 725 additions and 119 deletions

View file

@ -105,50 +105,116 @@ in the hydra configuration file, as below:
Using LDAP as authentication backend (optional) Using LDAP as authentication backend (optional)
----------------------------------------------- -----------------------------------------------
Instead of using Hydra\'s built-in user management you can optionally Instead of using Hydra's built-in user management you can optionally
use LDAP to manage roles and users. use LDAP to manage roles and users.
The `hydra-server` accepts the environment variable This is configured by defining the `<ldap>` block in the configuration file.
*HYDRA\_LDAP\_CONFIG*. The value of the variable should point to a valid In this block it's possible to configure the authentication plugin in the
YAML file containing the Catalyst LDAP configuration. The format of the `<config>` block. All options are directly passed to `Catalyst::Authentication::Store::LDAP`.
configuration file is describe in the The documentation for the available settings can be found [here]
[*Catalyst::Authentication::Store::LDAP* (https://metacpan.org/pod/Catalyst::Authentication::Store::LDAP#CONFIGURATION-OPTIONS).
documentation](https://metacpan.org/pod/Catalyst::Authentication::Store::LDAP#CONFIGURATION-OPTIONS).
An example is given below.
Roles can be assigned to users based on their LDAP group membership Note that the bind password (if needed) should be supplied as an included file to
(*use\_roles: 1* in the below example). For a user to have the role prevent it from leaking to the Nix store.
*admin* assigned to them they should be in the group *hydra\_admin*. In
general any LDAP group of the form *hydra\_some\_role* (notice the
*hydra\_* prefix) will work.
credential: Roles can be assigned to users based on their LDAP group membership. For this
class: Password to work *use\_roles = 1* needs to be defined for the authentication plugin.
password_field: password LDAP groups can then be mapped to Hydra roles using the `<role_mapping>` block.
password_type: self_check
store: Example configuration:
class: LDAP ```
ldap_server: localhost <ldap>
ldap_server_options.timeout: 30 <config>
binddn: "cn=root,dc=example" <credential>
bindpw: notapassword class = Password
start_tls: 0 password_field = password
start_tls_options: password_type = self_check
verify: none </credential>
user_basedn: "ou=users,dc=example" <store>
user_filter: "(&(objectClass=inetOrgPerson)(cn=%s))" class = LDAP
user_scope: one ldap_server = localhost
user_field: cn <ldap_server_options>
user_search_options: timeout = 30
deref: always </ldap_server_options>
use_roles: 1 binddn = "cn=root,dc=example"
role_basedn: "ou=groups,dc=example" include ldap-password.conf
role_filter: "(&(objectClass=groupOfNames)(member=%s))" start_tls = 0
role_scope: one <start_tls_options>
role_field: cn verify = none
role_value: dn </start_tls_options>
role_search_options: user_basedn = "ou=users,dc=example"
deref: always user_filter = "(&(objectClass=inetOrgPerson)(cn=%s))"
user_scope = one
user_field = cn
<user_search_options>
deref = always
</user_search_options>
# Important for role mappings to work:
use_roles = 1
role_basedn = "ou=groups,dc=example"
role_filter = "(&(objectClass=groupOfNames)(member=%s))"
role_scope = one
role_field = cn
role_value = dn
<role_search_options>
deref = always
</role_search_options>
</config>
<role_mapping>
# Make all users in the hydra_admin group Hydra admins
hydra_admin = admin
# Allow all users in the dev group to restart jobs and cancel builds
dev = restart-jobs
dev = cancel-builds
</role_mapping>
</ldap>
```
Then, place the password to your LDAP server in `/var/lib/hydra/ldap-password.conf`:
```
bindpw = the-ldap-password
```
### Debugging LDAP
Set the `debug` parameter under `ldap.config.ldap_server_options.debug`:
```
<ldap>
<config>
<store>
<ldap_server_options>
debug = 2
</ldap_server_options>
</store>
</config>
</ldap>
```
### Legacy LDAP Configuration
Hydra used to load the LDAP configuration from a YAML file in the
`HYDRA_LDAP_CONFIG` environment variable. This behavior is deperecated
and will be removed.
When Hydra uses the deprecated YAML file, Hydra applies the following
default role mapping:
```
<ldap>
<role_mapping>
hydra_admin = admin
hydra_bump-to-front = bump-to-front
hydra_cancel-build = cancel-build
hydra_create-projects = create-projects
hydra_restart-jobs = restart-jobs
</role_mapping>
</ldap>
```
Note that configuring both the LDAP parameters in the hydra.conf and via
the environment variable is a fatal error.
Embedding Extra HTML Embedding Extra HTML
-------------------- --------------------

View file

@ -521,8 +521,8 @@
TextDiff TextDiff
TextTable TextTable
UUID4Tiny UUID4Tiny
XMLSimple
YAML YAML
XMLSimple
]; ];
}; };

View file

@ -6,6 +6,7 @@ use parent 'Catalyst';
use Moose; use Moose;
use Hydra::Plugin; use Hydra::Plugin;
use Hydra::Model::DB; use Hydra::Model::DB;
use Hydra::Config qw(getLDAPConfigAmbient);
use Catalyst::Runtime '5.70'; use Catalyst::Runtime '5.70';
use Catalyst qw/ConfigLoader use Catalyst qw/ConfigLoader
Static::Simple Static::Simple
@ -19,7 +20,6 @@ use Catalyst qw/ConfigLoader
PrometheusTiny/, PrometheusTiny/,
'-Log=warn,fatal,error'; '-Log=warn,fatal,error';
use CatalystX::RoleApplicator; use CatalystX::RoleApplicator;
use YAML qw(LoadFile);
use Path::Class 'file'; use Path::Class 'file';
our $VERSION = '0.01'; our $VERSION = '0.01';
@ -43,9 +43,7 @@ __PACKAGE__->config(
role_field => "role", role_field => "role",
}, },
}, },
ldap => $ENV{'HYDRA_LDAP_CONFIG'} ? LoadFile( ldap => getLDAPConfigAmbient()->{'config'}
file($ENV{'HYDRA_LDAP_CONFIG'})
) : undef
}, },
'Plugin::ConfigLoader' => { 'Plugin::ConfigLoader' => {
driver => { driver => {

View file

@ -2,7 +2,165 @@ package Hydra::Config;
use strict; use strict;
use warnings; use warnings;
use Config::General;
use List::SomeUtils qw(none);
use YAML qw(LoadFile);
our @ISA = qw(Exporter);
our @EXPORT = qw(
getHydraConfig
getLDAPConfig
getLDAPConfigAmbient
);
our %configGeneralOpts = (-UseApacheInclude => 1, -IncludeAgain => 1, -IncludeRelative => 1); our %configGeneralOpts = (-UseApacheInclude => 1, -IncludeAgain => 1, -IncludeRelative => 1);
my $hydraConfigCache;
sub getHydraConfig {
return $hydraConfigCache if defined $hydraConfigCache;
my $conf;
if ($ENV{"HYDRA_CONFIG"}) {
$conf = $ENV{"HYDRA_CONFIG"};
} else {
require Hydra::Model::DB;
$conf = Hydra::Model::DB::getHydraPath() . "/hydra.conf"
};
if (-f $conf) {
$hydraConfigCache = loadConfig($conf);
} else {
$hydraConfigCache = {};
}
return $hydraConfigCache;
}
sub loadConfig {
my ($sourceFile) = @_;
my %opts = (%configGeneralOpts, -ConfigFile => $sourceFile);
return { Config::General->new(%opts)->getall };
}
sub is_ldap_in_legacy_mode {
my ($config, %env) = @_;
my $legacy_defined = defined $env{"HYDRA_LDAP_CONFIG"};
if (defined $config->{"ldap"}) {
if ($legacy_defined) {
die "The legacy environment variable HYDRA_LDAP_CONFIG is set, but config is also specified in hydra.conf. Please unset the environment variable.";
}
return 0;
} elsif ($legacy_defined) {
warn "Hydra is configured to use LDAP via the HYDRA_LDAP_CONFIG, a deprecated method. Please see the docs about configuring LDAP in the hydra.conf.";
return 1;
} else {
return 0;
}
}
sub getLDAPConfigAmbient {
return getLDAPConfig(getHydraConfig(), %ENV);
}
sub getLDAPConfig {
my ($config, %env) = @_;
my $ldap_config;
if (is_ldap_in_legacy_mode($config, %env)) {
$ldap_config = get_legacy_ldap_config($env{"HYDRA_LDAP_CONFIG"});
} else {
$ldap_config = $config->{"ldap"};
}
$ldap_config->{"role_mapping"} = normalize_ldap_role_mappings($ldap_config->{"role_mapping"});
return $ldap_config;
}
sub get_legacy_ldap_config {
my ($ldap_yaml_file) = @_;
return {
config => LoadFile($ldap_yaml_file),
role_mapping => {
"hydra_admin" => [ "admin" ],
"hydra_bump-to-front" => [ "bump-to-front" ],
"hydra_cancel-build" => [ "cancel-build" ],
"hydra_create-projects" => [ "create-projects" ],
"hydra_restart-jobs" => [ "restart-jobs" ],
},
};
}
sub normalize_ldap_role_mappings {
my ($input_map) = @_;
my $mapping = {};
my @errors;
for my $group (keys %{$input_map}) {
my $input = $input_map->{$group};
if (ref $input eq "ARRAY") {
$mapping->{$group} = $input;
} elsif (ref $input eq "") {
$mapping->{$group} = [ $input ];
} else {
push @errors, "On group '$group': the value is of type ${\ref $input}. Only strings and lists are acceptable.";
$mapping->{$group} = [ ];
}
eval {
validate_roles($mapping->{$group});
};
if ($@) {
push @errors, "On group '$group': $@";
}
}
if (@errors) {
die "Failed to normalize LDAP role mappings:\n" . (join "\n", @errors);
}
return $mapping;
}
sub validate_roles {
my ($roles) = @_;
my @invalid;
my $valid = valid_roles();
for my $role (@$roles) {
if (none { $_ eq $role } @$valid) {
push @invalid, "'$role'";
}
}
if (@invalid) {
die "Invalid roles: ${\join ', ', @invalid}. Valid roles are: ${\join ', ', @$valid}.";
}
return 1;
}
sub valid_roles {
return [
"admin",
"bump-to-front",
"cancel-build",
"create-projects",
"restart-jobs",
];
}
1; 1;

View file

@ -7,6 +7,7 @@ use base 'Hydra::Base::Controller::REST';
use File::Slurper qw(read_text); use File::Slurper qw(read_text);
use Crypt::RandPasswd; use Crypt::RandPasswd;
use Digest::SHA1 qw(sha1_hex); use Digest::SHA1 qw(sha1_hex);
use Hydra::Config qw(getLDAPConfigAmbient);
use Hydra::Helper::Nix; use Hydra::Helper::Nix;
use Hydra::Helper::CatalystUtils; use Hydra::Helper::CatalystUtils;
use Hydra::Helper::Email; use Hydra::Helper::Email;
@ -56,10 +57,10 @@ sub logout_POST {
sub doLDAPLogin { sub doLDAPLogin {
my ($self, $c, $username) = @_; my ($self, $c, $username) = @_;
my $user = $c->find_user({ username => $username }); my $user = $c->find_user({ username => $username });
my $LDAPUser = $c->find_user({ username => $username }, 'ldap'); my $LDAPUser = $c->find_user({ username => $username }, 'ldap');
my @LDAPRoles = grep { (substr $_, 0, 6) eq "hydra_" } $LDAPUser->roles; my @LDAPRoles = $LDAPUser->roles;
my $role_mapping = getLDAPConfigAmbient()->{"role_mapping"};
if (!$user) { if (!$user) {
$c->model('DB::Users')->create( $c->model('DB::Users')->create(
@ -79,8 +80,13 @@ sub doLDAPLogin {
}); });
} }
$user->userroles->delete; $user->userroles->delete;
if (@LDAPRoles) { foreach my $ldap_role (@LDAPRoles) {
$user->userroles->create({ role => (substr $_, 6) }) for @LDAPRoles; if (defined($role_mapping->{$ldap_role})) {
my $roles = $role_mapping->{$ldap_role};
for my $mapped_role (@$roles) {
$user->userroles->create({ role => $mapped_role });
}
}
} }
$c->set_authenticated($user); $c->set_authenticated($user);
} }

View file

@ -5,7 +5,6 @@ use warnings;
use Exporter; use Exporter;
use File::Path; use File::Path;
use File::Basename; use File::Basename;
use Config::General;
use Hydra::Config; use Hydra::Config;
use Hydra::Helper::CatalystUtils; use Hydra::Helper::CatalystUtils;
use Hydra::Model::DB; use Hydra::Model::DB;
@ -49,24 +48,6 @@ sub getHydraHome {
return $dir; return $dir;
} }
my $hydraConfig;
sub getHydraConfig {
return $hydraConfig if defined $hydraConfig;
my $conf = $ENV{"HYDRA_CONFIG"} || (Hydra::Model::DB::getHydraPath . "/hydra.conf");
my %opts = (%Hydra::Config::configGeneralOpts, -ConfigFile => $conf);
if (-f $conf) {
my %h = Config::General->new(%opts)->getall;
$hydraConfig = \%h;
} else {
$hydraConfig = {};
}
return $hydraConfig;
}
# Return hash of statsd configuration of the following shape: # Return hash of statsd configuration of the following shape:
# ( # (
# host => string, # host => string,

View file

@ -8,6 +8,7 @@ use Data::Dump qw(dump);
use Digest::SHA qw(sha256_hex); use Digest::SHA qw(sha256_hex);
use Encode; use Encode;
use File::Slurper qw(read_text); use File::Slurper qw(read_text);
use Hydra::Config;
use Hydra::Helper::AddBuilds; use Hydra::Helper::AddBuilds;
use Hydra::Helper::CatalystUtils; use Hydra::Helper::CatalystUtils;
use Hydra::Helper::Email; use Hydra::Helper::Email;

View file

@ -6,6 +6,7 @@ use utf8;
use Getopt::Long; use Getopt::Long;
use Time::HiRes qw( gettimeofday tv_interval ); use Time::HiRes qw( gettimeofday tv_interval );
use HTTP::Server::PSGI; use HTTP::Server::PSGI;
use Hydra::Config;
use Hydra::Event; use Hydra::Event;
use Hydra::Event::BuildFinished; use Hydra::Event::BuildFinished;
use Hydra::Helper::AddBuilds; use Hydra::Helper::AddBuilds;

View file

@ -9,6 +9,7 @@ use Net::Amazon::S3;
use Net::Amazon::S3::Client; use Net::Amazon::S3::Client;
use Nix::Config; use Nix::Config;
use Nix::Store; use Nix::Store;
use Hydra::Config;
use Hydra::Model::DB; use Hydra::Model::DB;
use Hydra::Helper::Nix; use Hydra::Helper::Nix;

View file

@ -3,6 +3,7 @@
use strict; use strict;
use warnings; use warnings;
use utf8; use utf8;
use Hydra::Config;
use Hydra::Helper::Nix; use Hydra::Helper::Nix;
use Net::Statsd; use Net::Statsd;
use File::Slurper qw(read_text); use File::Slurper qw(read_text);

View file

@ -6,6 +6,7 @@ use File::Path;
use File::stat; use File::stat;
use File::Basename; use File::Basename;
use Nix::Store; use Nix::Store;
use Hydra::Config;
use Hydra::Schema; use Hydra::Schema;
use Hydra::Helper::Nix; use Hydra::Helper::Nix;
use Hydra::Model::DB; use Hydra::Model::DB;

View file

@ -1,6 +1,7 @@
use strict; use strict;
use warnings; use warnings;
use Setup; use Setup;
use Hydra::Config;
my %ctx = test_init(hydra_config => q| my %ctx = test_init(hydra_config => q|
<hydra_notify> <hydra_notify>
@ -14,7 +15,7 @@ my %ctx = test_init(hydra_config => q|
require Hydra::Helper::Nix; require Hydra::Helper::Nix;
use Test2::V0; use Test2::V0;
is(Hydra::Helper::Nix::getHydraNotifyPrometheusConfig(Hydra::Helper::Nix::getHydraConfig()), { is(Hydra::Helper::Nix::getHydraNotifyPrometheusConfig(getHydraConfig()), {
'listen_address' => "127.0.0.1", 'listen_address' => "127.0.0.1",
'port' => 9199 'port' => 9199
}, "Reading specific configuration from the hydra.conf works"); }, "Reading specific configuration from the hydra.conf works");

View file

@ -1,6 +1,8 @@
use strict; use strict;
use warnings; use warnings;
use Setup; use Setup;
use Hydra::Config;
use Test2::V0;
my %ctx = test_init( my %ctx = test_init(
use_external_destination_store => 0, use_external_destination_store => 0,
@ -17,10 +19,7 @@ write_file($ctx{'tmpdir'} . "/bar.conf", q|
bar = baz bar = baz
|); |);
require Hydra::Helper::Nix; is(getHydraConfig(), {
use Test2::V0;
is(Hydra::Helper::Nix::getHydraConfig(), {
foo => { bar => "baz" } foo => { bar => "baz" }
}, "Nested includes work."); }, "Nested includes work.");

View file

@ -0,0 +1,242 @@
use strict;
use warnings;
use Setup;
use Hydra::Config;
use Test2::V0;
my $tmpdir = File::Temp->newdir();
my $cfgfile = "$tmpdir/conf";
my $scratchCfgFile = "$tmpdir/hydra.scratch.conf";
my $ldapInHydraConfFile = "$tmpdir/hydra.empty.conf";
write_file($ldapInHydraConfFile, <<CONF);
<ldap>
<config>
<credential>
class = Password
</credential>
</config>
<role_mapping>
hydra_admin = admin
hydra_one_group_many_roles = create-projects
hydra_one_group_many_roles = cancel-build
</role_mapping>
</ldap>
CONF
my $ldapInHydraConf = Hydra::Config::loadConfig($ldapInHydraConfFile);
my $emptyHydraConfFile = "$tmpdir/hydra.empty.conf";
write_file($emptyHydraConfFile, "");
my $emptyHydraConf = Hydra::Config::loadConfig($emptyHydraConfFile);
my $ldapYamlFile = "$tmpdir/ldap.yaml";
write_file($ldapYamlFile, <<YAML);
credential:
class: Password
YAML
subtest "getLDAPConfig" => sub {
subtest "No ldap section and an env var gets us legacy data" => sub {
like(
warning {
is(
Hydra::Config::getLDAPConfig(
$emptyHydraConf,
( HYDRA_LDAP_CONFIG => $ldapYamlFile )
),
{
config => {
credential => {
class => "Password",
},
},
role_mapping => {
"hydra_admin" => [ "admin" ],
"hydra_bump-to-front" => [ "bump-to-front" ],
"hydra_cancel-build" => [ "cancel-build" ],
"hydra_create-projects" => [ "create-projects" ],
"hydra_restart-jobs" => [ "restart-jobs" ],
}
},
"The empty file and set env var make legacy mode active."
);
},
qr/configured to use LDAP via the HYDRA_LDAP_CONFIG/,
"Having the environment variable set warns."
);
};
subtest "An ldap section and no env var gets us normalized data" => sub {
is(
warns {
is(
Hydra::Config::getLDAPConfig(
$ldapInHydraConf,
()
),
{
config => {
credential => {
class => "Password",
},
},
role_mapping => {
"hydra_admin" => [ "admin" ],
"hydra_one_group_many_roles" => [ "create-projects", "cancel-build" ],
}
},
"The empty file and set env var make legacy mode active."
);
},
0,
"No warnings are issued for non-legacy LDAP support."
);
};
};
subtest "is_ldap_in_legacy_mode" => sub {
subtest "With the environment variable set and an empty hydra.conf" => sub {
like(
warning {
is(
Hydra::Config::is_ldap_in_legacy_mode(
$emptyHydraConf,
( HYDRA_LDAP_CONFIG => $ldapYamlFile )
),
1,
"The empty file and set env var make legacy mode active."
);
},
qr/configured to use LDAP via the HYDRA_LDAP_CONFIG/,
"Having the environment variable set warns."
);
};
subtest "With the environment variable set and LDAP specified in hydra.conf" => sub {
like(
dies {
Hydra::Config::is_ldap_in_legacy_mode(
$ldapInHydraConf,
( HYDRA_LDAP_CONFIG => $ldapYamlFile )
);
},
qr/HYDRA_LDAP_CONFIG is set, but config is also specified in hydra\.conf/,
"Having the environment variable set dies to avoid misconfiguration."
);
};
subtest "Without the environment variable set and an empty hydra.conf" => sub {
is(
warns {
is(
Hydra::Config::is_ldap_in_legacy_mode(
$emptyHydraConf,
()
),
0,
"The empty file and unset env var means non-legacy."
);
},
0,
"We should receive zero warnings."
);
};
subtest "Without the environment variable set and LDAP specified in hydra.conf" => sub {
is(
warns {
is(
Hydra::Config::is_ldap_in_legacy_mode(
$ldapInHydraConf,
()
),
0,
"The empty file and unset env var means non-legacy."
);
},
0,
"We should receive zero warnings."
);
};
};
subtest "get_legacy_ldap_config" => sub {
is(
Hydra::Config::get_legacy_ldap_config($ldapYamlFile),
{
config => {
credential => {
class => "Password",
},
},
role_mapping => {
"hydra_admin" => [ "admin" ],
"hydra_bump-to-front" => [ "bump-to-front" ],
"hydra_cancel-build" => [ "cancel-build" ],
"hydra_create-projects" => [ "create-projects" ],
"hydra_restart-jobs" => [ "restart-jobs" ],
}
},
"Legacy, default role maps are applied."
);
};
subtest "validate_roles" => sub {
ok(Hydra::Config::validate_roles([]), "An empty list is valid");
ok(Hydra::Config::validate_roles(Hydra::Config::valid_roles()), "All current roles are valid.");
like(
dies { Hydra::Config::validate_roles([""]) },
qr/Invalid roles: ''./,
"Invalid roles are failing"
);
like(
dies { Hydra::Config::validate_roles(["foo", "bar"]) },
qr/Invalid roles: 'foo', 'bar'./,
"All the invalid roles are present in the error"
);
};
subtest "normalize_ldap_role_mappings" => sub {
is(
Hydra::Config::normalize_ldap_role_mappings({}),
{},
"An empty input map is an empty output map."
);
is(
Hydra::Config::normalize_ldap_role_mappings({
hydra_admin => "admin",
hydra_one_group_many_roles => [ "create-projects", "bump-to-front" ],
}),
{
hydra_admin => [ "admin" ],
hydra_one_group_many_roles => [ "create-projects", "bump-to-front" ],
},
"Lists and plain strings normalize to lists"
);
like(
dies{
Hydra::Config::normalize_ldap_role_mappings({
"group" => "invalid-role",
}),
},
qr/Failed to normalize.*Invalid roles.*invalid-role/s,
"Invalid roles fail to normalize."
);
like(
dies{
Hydra::Config::normalize_ldap_role_mappings({
"group" => { "nested" => "data" },
}),
},
qr/On group 'group':.* Only strings/,
"Invalid nesting fail to normalize."
);
};
done_testing;

View file

@ -1,6 +1,7 @@
use strict; use strict;
use warnings; use warnings;
use Setup; use Setup;
use Hydra::Config;
my %ctx = test_init(hydra_config => q| my %ctx = test_init(hydra_config => q|
<statsd> <statsd>
@ -12,7 +13,7 @@ my %ctx = test_init(hydra_config => q|
require Hydra::Helper::Nix; require Hydra::Helper::Nix;
use Test2::V0; use Test2::V0;
is(Hydra::Helper::Nix::getStatsdConfig(Hydra::Helper::Nix::getHydraConfig()), { is(Hydra::Helper::Nix::getStatsdConfig(getHydraConfig()), {
'host' => "foo.bar", 'host' => "foo.bar",
'port' => 18125 'port' => 18125
}, "Reading specific configuration from the hydra.conf works"); }, "Reading specific configuration from the hydra.conf works");

View file

@ -0,0 +1,107 @@
use strict;
use warnings;
use Setup;
use LDAPContext;
use Test2::V0;
use Catalyst::Test ();
use HTTP::Request::Common;
use JSON::MaybeXS;
my $ldap = LDAPContext->new(
root_password => "the-root-password",
);
my $users = {
unrelated => $ldap->add_user("unrelated_user"),
admin => $ldap->add_user("admin_user"),
not_admin => $ldap->add_user("not_admin_user"),
many_roles => $ldap->add_user("many_roles"),
};
$ldap->add_group("hydra_admin", $users->{"admin"}->{"username"});
$ldap->add_group("hydra-admin", $users->{"not_admin"}->{"username"});
$ldap->add_group("hydra_create-projects", $users->{"many_roles"}->{"username"});
$ldap->add_group("hydra_restart-jobs", $users->{"many_roles"}->{"username"});
$ldap->add_group("hydra_bump-to-front", $users->{"many_roles"}->{"username"});
$ldap->add_group("hydra_cancel-build", $users->{"many_roles"}->{"username"});
my $hydra_ldap_config = "${\$ldap->tmpdir()}/hydra_ldap_config.yaml";
LDAPContext::write_file($hydra_ldap_config, <<YAML);
credential:
class: Password
password_field: password
password_type: self_check
store:
class: LDAP
ldap_server: "${\$ldap->server_url()}"
ldap_server_options:
timeout: 30
debug: 0
binddn: "cn=root,dc=example"
bindpw: the-root-password
start_tls: 0
start_tls_options:
verify: none
user_basedn: "ou=users,dc=example"
user_filter: "(&(objectClass=inetOrgPerson)(cn=%s))"
user_scope: one
user_field: cn
user_search_options:
deref: always
use_roles: 1
role_basedn: "ou=groups,dc=example"
role_filter: "(&(objectClass=groupOfNames)(member=%s))"
role_scope: one
role_field: cn
role_value: dn
role_search_options:
deref: always
YAML
$ENV{'HYDRA_LDAP_CONFIG'} = $hydra_ldap_config;
my $ctx = test_context();
Catalyst::Test->import('Hydra');
subtest "Valid login attempts" => sub {
my %users_to_roles = (
unrelated => [],
admin => ["admin"],
not_admin => [],
many_roles => [ "create-projects", "restart-jobs", "bump-to-front", "cancel-build" ],
);
for my $username (keys %users_to_roles) {
my $user = $users->{$username};
my $roles = $users_to_roles{$username};
subtest "Verifying $username" => sub {
my $req = request(POST '/login',
Referer => 'http://localhost/',
Accept => 'application/json',
Content => {
username => $user->{"username"},
password => $user->{"password"}
}
);
is($req->code, 302, "The login redirects");
my $data = decode_json($req->content());
is($data->{"username"}, $user->{"username"}, "Username matches");
is($data->{"emailaddress"}, $user->{"email"}, "Email matches");
is([sort @{$data->{"userroles"}}], [sort @$roles], "Roles match");
};
}
};
# Logging in with an invalid user is rejected
is(request(POST '/login',
Referer => 'http://localhost/',
Content => {
username => 'alice',
password => 'foobar'
}
)->code, 403, "Logging in with invalid credentials does not work");
done_testing;

View file

@ -13,60 +13,87 @@ my $users = {
admin => $ldap->add_user("admin_user"), admin => $ldap->add_user("admin_user"),
not_admin => $ldap->add_user("not_admin_user"), not_admin => $ldap->add_user("not_admin_user"),
many_roles => $ldap->add_user("many_roles"), many_roles => $ldap->add_user("many_roles"),
many_roles_one_group => $ldap->add_user("many_roles_one_group"),
}; };
$ldap->add_group("hydra_admin", $users->{"admin"}->{"username"}); $ldap->add_group("hydra_admin", $users->{"admin"}->{"username"});
$ldap->add_group("hydra-admin", $users->{"not_admin"}->{"username"}); $ldap->add_group("hydra-admin", $users->{"not_admin"}->{"username"});
$ldap->add_group("hydra_one_group_many_roles", $users->{"many_roles_one_group"}->{"username"});
$ldap->add_group("hydra_create-projects", $users->{"many_roles"}->{"username"}); $ldap->add_group("hydra_create-projects", $users->{"many_roles"}->{"username"});
$ldap->add_group("hydra_restart-jobs", $users->{"many_roles"}->{"username"}); $ldap->add_group("hydra_restart-jobs", $users->{"many_roles"}->{"username"});
$ldap->add_group("hydra_bump-to-front", $users->{"many_roles"}->{"username"}); $ldap->add_group("hydra_bump-to-front", $users->{"many_roles"}->{"username"});
$ldap->add_group("hydra_cancel-build", $users->{"many_roles"}->{"username"}); $ldap->add_group("hydra_cancel-build", $users->{"many_roles"}->{"username"});
my $hydra_ldap_config = "${\$ldap->tmpdir()}/hydra_ldap_config.yaml";
LDAPContext::write_file($hydra_ldap_config, <<YAML);
credential:
class: Password
password_field: password
password_type: self_check
store:
class: LDAP
ldap_server: "${\$ldap->server_url()}"
ldap_server_options:
timeout: 30
debug: 0
binddn: "cn=root,dc=example"
bindpw: notapassword
start_tls: 0
start_tls_options:
verify: none
user_basedn: "ou=users,dc=example"
user_filter: "(&(objectClass=inetOrgPerson)(cn=%s))"
user_scope: one
user_field: cn
user_search_options:
deref: always
use_roles: 1
role_basedn: "ou=groups,dc=example"
role_filter: "(&(objectClass=groupOfNames)(member=%s))"
role_scope: one
role_field: cn
role_value: dn
role_search_options:
deref: always
YAML
$ENV{'HYDRA_LDAP_CONFIG'} = $hydra_ldap_config; my $ctx = test_context(
my $ctx = test_context(); before_init => sub {
my ($ctx) = @_;
write_file($ctx->{"tmpdir"} . "/password.conf", "bindpw = ${\$ldap->{'root_password'}}");
},
hydra_config => <<CFG
<ldap>
<config>
<credential>
class = Password
password_field = password
password_type = self_check
</credential>
<store>
class = LDAP
ldap_server = ${\$ldap->server_url()}
<ldap_server_options>
timeout = 30
debug = 0
</ldap_server_options>
binddn = "cn=root,dc=example"
include password.conf
start_tls = 0
<start_tls_options>
verify = none
</start_tls_options>
user_basedn = "ou=users,dc=example"
user_filter = "(&(objectClass=inetOrgPerson)(cn=%s))"
user_scope = one
user_field = cn
<user_search_options>
deref = always
</user_search_options>
use_roles = 1
role_basedn = "ou=groups,dc=example"
role_filter = "(&(objectClass=groupOfNames)(member=%s))"
role_scope = one
role_field = cn
role_value = dn
<role_search_options>
deref = always
</role_search_options>
</store>
</config>
<role_mapping>
hydra_admin = admin
hydra_create-projects = create-projects
hydra_cancel-build = cancel-build
hydra_bump-to-front = bump-to-front
hydra_restart-jobs = restart-jobs
hydra_one_group_many_roles = create-projects
hydra_one_group_many_roles = cancel-build
hydra_one_group_many_roles = bump-to-front
</role_mapping>
</ldap>
CFG
);
Catalyst::Test->import('Hydra'); Catalyst::Test->import('Hydra');
subtest "Valid login attempts" => sub { subtest "Valid login attempts" => sub {
my %users_to_roles = ( my %users_to_roles = (
unrelated => [], unrelated => [],
admin => ["admin"], admin => ["admin"],
not_admin => [], not_admin => [],
many_roles => [ "create-projects", "restart-jobs", "bump-to-front", "cancel-build" ], many_roles => [ "create-projects", "restart-jobs", "bump-to-front", "cancel-build" ],
many_roles_one_group => [ "create-projects", "bump-to-front", "cancel-build" ],
); );
for my $username (keys %users_to_roles) { for my $username (keys %users_to_roles) {
my $user = $users->{$username}; my $user = $users->{$username};

View file

@ -6,6 +6,7 @@ use File::Path qw(make_path);
use File::Basename; use File::Basename;
use Cwd qw(abs_path getcwd); use Cwd qw(abs_path getcwd);
use CliRunners; use CliRunners;
use Hydra::Helper::Exec;
# Set up the environment for running tests. # Set up the environment for running tests.
# #
@ -16,6 +17,9 @@ use CliRunners;
# * use_external_destination_store: Boolean indicating whether hydra should # * use_external_destination_store: Boolean indicating whether hydra should
# use a destination store different from the evaluation store. # use a destination store different from the evaluation store.
# True by default. # True by default.
# * before_init: a sub which is called after the database is up, but before
# hydra-init is executed. It receives the HydraTestContext object as
# its argument.
# #
# This clears several environment variables and sets them to ephemeral # This clears several environment variables and sets them to ephemeral
# values: a temporary database, temporary Nix store, temporary Hydra # values: a temporary database, temporary Nix store, temporary Hydra
@ -63,24 +67,28 @@ sub new {
extra_initdb_args => "--locale C.UTF-8" extra_initdb_args => "--locale C.UTF-8"
); );
$ENV{'HYDRA_DBI'} = $pgsql->dsn; $ENV{'HYDRA_DBI'} = $pgsql->dsn;
system("hydra-init") == 0 or die;
my $self = { my $self = bless {
_db => undef, _db => undef,
db_handle => $pgsql, db_handle => $pgsql,
tmpdir => $dir, tmpdir => $dir,
nix_state_dir => "$dir/nix/var/nix", nix_state_dir => "$dir/nix/var/nix",
testdir => abs_path(dirname(__FILE__) . "/.."), testdir => abs_path(dirname(__FILE__) . "/.."),
jobsdir => abs_path(dirname(__FILE__) . "/../jobs") jobsdir => abs_path(dirname(__FILE__) . "/../jobs")
}; }, $class;
return bless $self, $class; if ($opts{'before_init'}) {
$opts{'before_init'}->($self);
}
expectOkay(5, ("hydra-init"));
return $self;
} }
sub db { sub db {
my ($self, $setup) = @_; my ($self, $setup) = @_;
if (!defined $self->{_db}) { if (!defined $self->{_db}) {
require Hydra::Schema; require Hydra::Schema;
require Hydra::Model::DB; require Hydra::Model::DB;

View file

@ -12,9 +12,15 @@ use Hydra::Helper::Exec;
# It creates a top level organization and structure, and provides # It creates a top level organization and structure, and provides
# add_user and add_group. # add_user and add_group.
# #
# Hash Parameters:
#
# * root_password: The clear text password required for connecting to the LDAP server
#
# The server is automatically terminated when the class is dropped. # The server is automatically terminated when the class is dropped.
sub new { sub new {
my ($class) = @_; my ($class, %opts) = @_;
my $rootPassword = $opts{'root_password'} // rand_chars();
my $root = File::Temp->newdir(); my $root = File::Temp->newdir();
mkdir $root; mkdir $root;
@ -30,12 +36,13 @@ sub new {
my $socket = "$root/slapd.socket"; my $socket = "$root/slapd.socket";
my $self = { my $self = {
_tmpdir => $root,
_db_dir => $db_dir, _db_dir => $db_dir,
_openldap_source => $ENV{"OPENLDAP_ROOT"}, _openldap_source => $ENV{"OPENLDAP_ROOT"},
_pid_file => $pid_file, _pid_file => $pid_file,
_slapd_dir => $slapd_dir, _slapd_dir => $slapd_dir,
_socket => $socket, _socket => $socket,
_tmpdir => $root,
root_password => $rootPassword,
}; };
my $blessed = bless $self, $class; my $blessed = bless $self, $class;
@ -128,7 +135,7 @@ objectClass: olcMdbConfig
olcDatabase: {1}mdb olcDatabase: {1}mdb
olcDbDirectory: ${\$self->{"_db_dir"}} olcDbDirectory: ${\$self->{"_db_dir"}}
olcRootDN: cn=root,dc=example olcRootDN: cn=root,dc=example
olcRootPW: notapassword olcRootPW: ${\$self->{"root_password"}}
olcSuffix: dc=example olcSuffix: dc=example
EOF EOF
} }