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>
|
</para>
|
||||||
</section>
|
</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>
|
</chapter>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
|
|
187
flake.nix
187
flake.nix
|
@ -35,14 +35,73 @@
|
||||||
# A Nixpkgs overlay that provides a 'hydra' package.
|
# A Nixpkgs overlay that provides a 'hydra' package.
|
||||||
overlay = final: prev: {
|
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 {
|
perlDeps = buildEnv {
|
||||||
name = "hydra-perl-deps";
|
name = "hydra-perl-deps";
|
||||||
paths = with perlPackages; lib.closePropagation
|
paths = with perlPackages; lib.closePropagation
|
||||||
[ ModulePluggable
|
[ ModulePluggable
|
||||||
CatalystActionREST
|
CatalystActionREST
|
||||||
CatalystAuthenticationStoreDBIxClass
|
CatalystAuthenticationStoreDBIxClass
|
||||||
|
CatalystAuthenticationStoreLDAP
|
||||||
CatalystDevel
|
CatalystDevel
|
||||||
CatalystDispatchTypeRegex
|
CatalystDispatchTypeRegex
|
||||||
CatalystPluginAccessLog
|
CatalystPluginAccessLog
|
||||||
|
@ -88,6 +147,7 @@
|
||||||
TextDiff
|
TextDiff
|
||||||
TextTable
|
TextTable
|
||||||
XMLSimple
|
XMLSimple
|
||||||
|
YAML
|
||||||
final.nix
|
final.nix
|
||||||
final.nix.perl-bindings
|
final.nix.perl-bindings
|
||||||
git
|
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;
|
container = nixosConfigurations.container.config.system.build.toplevel;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,8 @@ use Catalyst qw/ConfigLoader
|
||||||
Captcha/,
|
Captcha/,
|
||||||
'-Log=warn,fatal,error';
|
'-Log=warn,fatal,error';
|
||||||
use CatalystX::RoleApplicator;
|
use CatalystX::RoleApplicator;
|
||||||
|
use YAML qw(LoadFile);
|
||||||
|
use Path::Class 'file';
|
||||||
|
|
||||||
our $VERSION = '0.01';
|
our $VERSION = '0.01';
|
||||||
|
|
||||||
|
@ -44,6 +45,9 @@ __PACKAGE__->config(
|
||||||
role_field => "role",
|
role_field => "role",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
ldap => $ENV{'HYDRA_LDAP_CONFIG'} ? LoadFile(
|
||||||
|
file($ENV{'HYDRA_LDAP_CONFIG'})
|
||||||
|
) : undef
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'Plugin::Static::Simple' => {
|
'Plugin::Static::Simple' => {
|
||||||
|
|
|
@ -12,6 +12,7 @@ use Hydra::Helper::Email;
|
||||||
use LWP::UserAgent;
|
use LWP::UserAgent;
|
||||||
use JSON;
|
use JSON;
|
||||||
use HTML::Entities;
|
use HTML::Entities;
|
||||||
|
use Encode qw(decode);
|
||||||
|
|
||||||
|
|
||||||
__PACKAGE__->config->{namespace} = '';
|
__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 user name.") if $username eq "";
|
||||||
error($c, "You must specify a password.") if $password 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.")
|
accessDenied($c, "Bad username or password.")
|
||||||
if !$c->authenticate({username => $username, password => $password});
|
}
|
||||||
|
|
||||||
currentUser_GET($self, $c);
|
currentUser_GET($self, $c);
|
||||||
}
|
}
|
||||||
|
@ -44,6 +49,36 @@ sub logout_POST {
|
||||||
$self->status_no_content($c);
|
$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 {
|
sub doEmailLogin {
|
||||||
my ($self, $c, $type, $email, $fullName) = @_;
|
my ($self, $c, $type, $email, $fullName) = @_;
|
||||||
|
|
Loading…
Reference in a new issue