Merge pull request #805 from andir/ldap
LDAP support continued (with missing packages & tests)
This commit is contained in:
commit
8f683aa7f8
|
@ -272,6 +272,62 @@ server {
|
|||
|
||||
</para>
|
||||
</section>
|
||||
<section>
|
||||
<title>Using LDAP as authentication backend (optional)</title>
|
||||
<para>
|
||||
Instead of using Hydra's built-in user management you can optionally use LDAP to manage roles and users.
|
||||
</para>
|
||||
|
||||
<para>
|
||||
The <command>hydra-server</command> accepts the environment
|
||||
variable <emphasis>HYDRA_LDAP_CONFIG</emphasis>. The value of
|
||||
the variable should point to a valid YAML file containing the
|
||||
Catalyst LDAP configuration. The format of the configuration
|
||||
file is describe in the
|
||||
<link xlink:href="https://metacpan.org/pod/Catalyst::Authentication::Store::LDAP#CONFIGURATION-OPTIONS">
|
||||
<emphasis>Catalyst::Authentication::Store::LDAP</emphasis> documentation</link>.
|
||||
An example is given below.
|
||||
</para>
|
||||
|
||||
<para>
|
||||
Roles can be assigned to users based on their LDAP group membership
|
||||
(<emphasis>use_roles: 1</emphasis> in the below example).
|
||||
For a user to have the role <emphasis>admin</emphasis> assigned to them
|
||||
they should be in the group <emphasis>hydra_admin</emphasis>. In general
|
||||
any LDAP group of the form <emphasis>hydra_some_role</emphasis>
|
||||
(notice the <emphasis>hydra_</emphasis> prefix) will work.
|
||||
</para>
|
||||
|
||||
<screen>
|
||||
credential:
|
||||
class: Password
|
||||
password_field: password
|
||||
password_type: self_check
|
||||
store:
|
||||
class: LDAP
|
||||
ldap_server: localhost
|
||||
ldap_server_options.timeout: 30
|
||||
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
|
||||
</screen>
|
||||
</section>
|
||||
</chapter>
|
||||
|
||||
<!--
|
||||
|
|
187
flake.nix
187
flake.nix
|
@ -35,14 +35,73 @@
|
|||
# A Nixpkgs overlay that provides a 'hydra' package.
|
||||
overlay = final: prev: {
|
||||
|
||||
hydra = with final; let
|
||||
# Add LDAP dependencies that aren't currently found within nixpkgs.
|
||||
perlPackages = prev.perlPackages // {
|
||||
NetLDAPServer = prev.perlPackages.buildPerlPackage {
|
||||
pname = "Net-LDAP-Server";
|
||||
version = "0.43";
|
||||
src = final.fetchurl {
|
||||
url = "mirror://cpan/authors/id/A/AA/AAR/Net-LDAP-Server-0.43.tar.gz";
|
||||
sha256 = "0qmh3cri3fpccmwz6bhwp78yskrb3qmalzvqn0a23hqbsfs4qv6x";
|
||||
};
|
||||
propagatedBuildInputs = with final.perlPackages; [ NetLDAP ConvertASN1 ];
|
||||
meta = {
|
||||
description = "LDAP server side protocol handling";
|
||||
license = with final.stdenv.lib.licenses; [ artistic1 ];
|
||||
};
|
||||
};
|
||||
|
||||
NetLDAPSID = prev.perlPackages.buildPerlPackage {
|
||||
pname = "Net-LDAP-SID";
|
||||
version = "0.0001";
|
||||
src = final.fetchurl {
|
||||
url = "mirror://cpan/authors/id/K/KA/KARMAN/Net-LDAP-SID-0.001.tar.gz";
|
||||
sha256 = "1mnnpkmj8kpb7qw50sm8h4sd8py37ssy2xi5hhxzr5whcx0cvhm8";
|
||||
};
|
||||
meta = {
|
||||
description= "Active Directory Security Identifier manipulation";
|
||||
license = with final.stdenv.lib.licenses; [ artistic2 ];
|
||||
};
|
||||
};
|
||||
|
||||
NetLDAPServerTest = prev.perlPackages.buildPerlPackage {
|
||||
pname = "Net-LDAP-Server-Test";
|
||||
version = "0.22";
|
||||
src = final.fetchurl {
|
||||
url = "mirror://cpan/authors/id/K/KA/KARMAN/Net-LDAP-Server-Test-0.22.tar.gz";
|
||||
sha256 = "13idip7jky92v4adw60jn2gcc3zf339gsdqlnc9nnvqzbxxp285i";
|
||||
};
|
||||
propagatedBuildInputs = with final.perlPackages; [ NetLDAP NetLDAPServer TestMore DataDump NetLDAPSID ];
|
||||
meta = {
|
||||
description= "test Net::LDAP code";
|
||||
license = with final.stdenv.lib.licenses; [ artistic1 ];
|
||||
};
|
||||
};
|
||||
|
||||
CatalystAuthenticationStoreLDAP = prev.perlPackages.buildPerlPackage {
|
||||
pname = "Catalyst-Authentication-Store-LDAP";
|
||||
version = "1.016";
|
||||
src = final.fetchurl {
|
||||
url = "mirror://cpan/authors/id/I/IL/ILMARI/Catalyst-Authentication-Store-LDAP-1.016.tar.gz";
|
||||
sha256 = "0cm399vxqqf05cjgs1j5v3sk4qc6nmws5nfhf52qvpbwc4m82mq8";
|
||||
};
|
||||
propagatedBuildInputs = with final.perlPackages; [ NetLDAP CatalystPluginAuthentication ClassAccessorFast ];
|
||||
buildInputs = with final.perlPackages; [ TestMore TestMockObject TestException NetLDAPServerTest ];
|
||||
meta = {
|
||||
description= "Authentication from an LDAP Directory";
|
||||
license = with final.stdenv.lib.licenses; [ artistic1 ];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
hydra = with final; let
|
||||
perlDeps = buildEnv {
|
||||
name = "hydra-perl-deps";
|
||||
paths = with perlPackages; lib.closePropagation
|
||||
[ ModulePluggable
|
||||
CatalystActionREST
|
||||
CatalystAuthenticationStoreDBIxClass
|
||||
CatalystAuthenticationStoreLDAP
|
||||
CatalystDevel
|
||||
CatalystDispatchTypeRegex
|
||||
CatalystPluginAccessLog
|
||||
|
@ -88,6 +147,7 @@
|
|||
TextDiff
|
||||
TextTable
|
||||
XMLSimple
|
||||
YAML
|
||||
final.nix
|
||||
final.nix.perl-bindings
|
||||
git
|
||||
|
@ -291,6 +351,131 @@
|
|||
'';
|
||||
};
|
||||
|
||||
tests.ldap.x86_64-linux =
|
||||
with import (nixpkgs + "/nixos/lib/testing-python.nix") { system = "x86_64-linux"; };
|
||||
makeTest {
|
||||
machine = { pkgs, ... }: {
|
||||
imports = [ hydraServer ];
|
||||
|
||||
services.openldap = {
|
||||
enable = true;
|
||||
suffix = "dc=example";
|
||||
rootdn = "cn=root,dc=example";
|
||||
rootpw = "notapassword";
|
||||
database = "bdb";
|
||||
dataDir = "/var/lib/openldap";
|
||||
extraDatabaseConfig = ''
|
||||
'';
|
||||
|
||||
declarativeContents = ''
|
||||
dn: dc=example
|
||||
dc: example
|
||||
o: Root
|
||||
objectClass: top
|
||||
objectClass: dcObject
|
||||
objectClass: organization
|
||||
|
||||
dn: ou=users,dc=example
|
||||
ou: users
|
||||
description: All users
|
||||
objectClass: top
|
||||
objectClass: organizationalUnit
|
||||
|
||||
dn: ou=groups,dc=example
|
||||
ou: groups
|
||||
description: All groups
|
||||
objectClass: top
|
||||
objectClass: organizationalUnit
|
||||
|
||||
dn: cn=hydra_admin,ou=groups,dc=example
|
||||
cn: hydra_admin
|
||||
description: Hydra Admin user group
|
||||
objectClass: groupOfNames
|
||||
member: cn=admin,ou=users,dc=example
|
||||
|
||||
dn: cn=user,ou=users,dc=example
|
||||
objectClass: organizationalPerson
|
||||
objectClass: inetOrgPerson
|
||||
sn: user
|
||||
cn: user
|
||||
mail: user@example
|
||||
userPassword: foobar
|
||||
|
||||
dn: cn=admin,ou=users,dc=example
|
||||
objectClass: organizationalPerson
|
||||
objectClass: inetOrgPerson
|
||||
sn: admin
|
||||
cn: admin
|
||||
mail: admin@example
|
||||
userPassword: password
|
||||
'';
|
||||
};
|
||||
systemd.services.hdyra-server.environment.CATALYST_DEBUG = "1";
|
||||
systemd.services.hydra-server.environment.HYDRA_LDAP_CONFIG = pkgs.writeText "config.yaml"
|
||||
# example config based on https://metacpan.org/source/ILMARI/Catalyst-Authentication-Store-LDAP-1.016/README#L103
|
||||
''
|
||||
credential:
|
||||
class: Password
|
||||
password_field: password
|
||||
password_type: self_check
|
||||
store:
|
||||
class: LDAP
|
||||
ldap_server: localhost
|
||||
ldap_server_options.timeout: 30
|
||||
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
|
||||
'';
|
||||
networking.firewall.enable = false;
|
||||
};
|
||||
testScript = ''
|
||||
import json
|
||||
|
||||
machine.wait_for_unit("openldap.service")
|
||||
machine.wait_for_job("hydra-init")
|
||||
machine.wait_for_open_port("3000")
|
||||
response = machine.succeed(
|
||||
"curl --fail http://localhost:3000/login -H 'Accept: application/json' -H 'Referer: http://localhost:3000' --data 'username=user&password=foobar'"
|
||||
)
|
||||
|
||||
response_json = json.loads(response)
|
||||
assert "user" == response_json["username"]
|
||||
assert "user@example" == response_json["emailaddress"]
|
||||
assert len(response_json["userroles"]) == 0
|
||||
|
||||
# logging on with wrong credentials shouldn't work
|
||||
machine.fail(
|
||||
"curl --fail http://localhost:3000/login -H 'Accept: application/json' -H 'Referer: http://localhost:3000' --data 'username=user&password=wrongpassword'"
|
||||
)
|
||||
|
||||
# the admin user should get the admin role from his group membership in `hydra_admin`
|
||||
response = machine.succeed(
|
||||
"curl --fail http://localhost:3000/login -H 'Accept: application/json' -H 'Referer: http://localhost:3000' --data 'username=admin&password=password'"
|
||||
)
|
||||
|
||||
response_json = json.loads(response)
|
||||
assert "admin" == response_json["username"]
|
||||
assert "admin@example" == response_json["emailaddress"]
|
||||
assert "admin" in response_json["userroles"]
|
||||
'';
|
||||
};
|
||||
|
||||
container = nixosConfigurations.container.config.system.build.toplevel;
|
||||
};
|
||||
|
||||
|
|
|
@ -20,7 +20,8 @@ use Catalyst qw/ConfigLoader
|
|||
Captcha/,
|
||||
'-Log=warn,fatal,error';
|
||||
use CatalystX::RoleApplicator;
|
||||
|
||||
use YAML qw(LoadFile);
|
||||
use Path::Class 'file';
|
||||
|
||||
our $VERSION = '0.01';
|
||||
|
||||
|
@ -44,6 +45,9 @@ __PACKAGE__->config(
|
|||
role_field => "role",
|
||||
},
|
||||
},
|
||||
ldap => $ENV{'HYDRA_LDAP_CONFIG'} ? LoadFile(
|
||||
file($ENV{'HYDRA_LDAP_CONFIG'})
|
||||
) : undef
|
||||
},
|
||||
},
|
||||
'Plugin::Static::Simple' => {
|
||||
|
|
|
@ -12,6 +12,7 @@ use Hydra::Helper::Email;
|
|||
use LWP::UserAgent;
|
||||
use JSON;
|
||||
use HTML::Entities;
|
||||
use Encode qw(decode);
|
||||
|
||||
|
||||
__PACKAGE__->config->{namespace} = '';
|
||||
|
@ -28,8 +29,12 @@ sub login_POST {
|
|||
error($c, "You must specify a user name.") if $username eq "";
|
||||
error($c, "You must specify a password.") if $password eq "";
|
||||
|
||||
if ($c->authenticate({username => $username, password => $password}, 'ldap')) {
|
||||
doLDAPLogin($self, $c, $username);
|
||||
} elsif ($c->authenticate({username => $username, password => $password})) {}
|
||||
else {
|
||||
accessDenied($c, "Bad username or password.")
|
||||
if !$c->authenticate({username => $username, password => $password});
|
||||
}
|
||||
|
||||
currentUser_GET($self, $c);
|
||||
}
|
||||
|
@ -44,6 +49,36 @@ sub logout_POST {
|
|||
$self->status_no_content($c);
|
||||
}
|
||||
|
||||
sub doLDAPLogin {
|
||||
my ($self, $c, $username) = @_;
|
||||
|
||||
my $user = $c->find_user({ username => $username });
|
||||
my $LDAPUser = $c->find_user({ username => $username }, 'ldap');
|
||||
my @LDAPRoles = grep { (substr $_, 0, 5) eq "hydra" } $LDAPUser->roles;
|
||||
|
||||
if (!$user) {
|
||||
$c->model('DB::Users')->create(
|
||||
{ username => $username
|
||||
, fullname => decode('UTF-8', $LDAPUser->cn)
|
||||
, password => "!"
|
||||
, emailaddress => $LDAPUser->mail
|
||||
, type => "LDAP"
|
||||
});
|
||||
$user = $c->find_user({ username => $username }) or die;
|
||||
} else {
|
||||
$user->update(
|
||||
{ fullname => decode('UTF-8', $LDAPUser->cn)
|
||||
, password => "!"
|
||||
, emailaddress => $LDAPUser->mail
|
||||
, type => "LDAP"
|
||||
});
|
||||
}
|
||||
$user->userroles->delete;
|
||||
if (@LDAPRoles) {
|
||||
$user->userroles->create({ role => (substr $_, 6) }) for @LDAPRoles;
|
||||
}
|
||||
$c->set_authenticated($user);
|
||||
}
|
||||
|
||||
sub doEmailLogin {
|
||||
my ($self, $c, $type, $email, $fullName) = @_;
|
||||
|
|
Loading…
Reference in a new issue