Merge pull request #805 from andir/ldap

LDAP support continued (with missing packages & tests)
This commit is contained in:
Graham Christensen 2020-09-11 12:10:23 -04:00 committed by GitHub
commit 8f683aa7f8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 284 additions and 4 deletions

View file

@ -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: "(&amp;(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: "(&amp;(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
View file

@ -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;
}; };

View file

@ -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' => {

View file

@ -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 "";
accessDenied($c, "Bad username or password.") if ($c->authenticate({username => $username, password => $password}, 'ldap')) {
if !$c->authenticate({username => $username, password => $password}); doLDAPLogin($self, $c, $username);
} elsif ($c->authenticate({username => $username, password => $password})) {}
else {
accessDenied($c, "Bad username or 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) = @_;