From 28646e1c5fb9d71fab027e25324f969750ce7e69 Mon Sep 17 00:00:00 2001 From: ajs124 Date: Sun, 13 Oct 2019 02:06:36 +0200 Subject: [PATCH 1/7] Initial attempt at adding LDAP login support --- src/lib/Hydra.pm | 6 ++++- src/lib/Hydra/Controller/User.pm | 39 ++++++++++++++++++++++++++++++-- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/lib/Hydra.pm b/src/lib/Hydra.pm index f4583eed..3c8184d9 100644 --- a/src/lib/Hydra.pm +++ b/src/lib/Hydra.pm @@ -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 => LoadFile( + file($ENV{'HYDRA_LDAP_CONFIG'}) + ) }, }, 'Plugin::Static::Simple' => { diff --git a/src/lib/Hydra/Controller/User.pm b/src/lib/Hydra/Controller/User.pm index 18cc7b05..e1351be4 100644 --- a/src/lib/Hydra/Controller/User.pm +++ b/src/lib/Hydra/Controller/User.pm @@ -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 ""; - accessDenied($c, "Bad username or password.") - if !$c->authenticate({username => $username, password => $password}); + 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.") + } 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) = @_; From b9ff7b2671760a06411b0e94c184ca1ec567a31f Mon Sep 17 00:00:00 2001 From: edef Date: Tue, 29 Oct 2019 13:12:48 +0000 Subject: [PATCH 2/7] include perlPackages.YAML in buildInputs --- flake.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/flake.nix b/flake.nix index ab830f19..77bf6670 100644 --- a/flake.nix +++ b/flake.nix @@ -88,6 +88,7 @@ TextDiff TextTable XMLSimple + YAML final.nix final.nix.perl-bindings git From c00b42dced5ea3051786fa67293804407d2b7d39 Mon Sep 17 00:00:00 2001 From: edef Date: Tue, 29 Oct 2019 13:12:49 +0000 Subject: [PATCH 3/7] don't try to load HYDRA_LDAP_CONFIG if none is provided --- src/lib/Hydra.pm | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib/Hydra.pm b/src/lib/Hydra.pm index 3c8184d9..7894a8ba 100644 --- a/src/lib/Hydra.pm +++ b/src/lib/Hydra.pm @@ -45,9 +45,9 @@ __PACKAGE__->config( role_field => "role", }, }, - ldap => LoadFile( - file($ENV{'HYDRA_LDAP_CONFIG'}) - ) + ldap => $ENV{'HYDRA_LDAP_CONFIG'} ? LoadFile( + file($ENV{'HYDRA_LDAP_CONFIG'}) + ) : undef }, }, 'Plugin::Static::Simple' => { From b8c19337b6b65e68ee8078d55859e0034da13b21 Mon Sep 17 00:00:00 2001 From: Andreas Rammhold Date: Wed, 9 Sep 2020 13:30:43 +0200 Subject: [PATCH 4/7] LDAP: add the required packages to the perlPackage via the overlay Nixpkgs doesn't currently provide these required packages. In order to use this feature without waiting for a newer release of NixOS/Nixpkgs thes have been packages inline. --- flake.nix | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index 77bf6670..94dcd292 100644 --- a/flake.nix +++ b/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 From cfc01e25180a3a4828d5aab1f9b4c6ec91fc9869 Mon Sep 17 00:00:00 2001 From: Andreas Rammhold Date: Wed, 9 Sep 2020 18:26:46 +0200 Subject: [PATCH 5/7] LDAP: add VM test to flake.nix In this newly added test an OpenLDAP server will provide one user (called `user`) and it will be attempted to login as that said user. Also logging in with any other password must fail. --- flake.nix | 86 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/flake.nix b/flake.nix index 94dcd292..3daf1a46 100644 --- a/flake.nix +++ b/flake.nix @@ -351,6 +351,92 @@ ''; }; + 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: cn=user,ou=users,dc=example + objectClass: organizationalPerson + objectClass: inetOrgPerson + sn: user + cn: user + mail: user@example + userPassword: foobar + ''; + }; + 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: 0 + role_basedn: "ou=groups,ou=OxObjects,dc=yourcompany,dc=com" + role_filter: "(&(objectClass=posixGroup)(memberUid=%s))" + role_scope: one + role_field: uid + role_value: dn + role_search_options: + deref: always + ''; + networking.firewall.enable = false; + }; + testScript = '' + machine.wait_for_unit("openldap.service") + machine.wait_for_job("hydra-init") + machine.wait_for_open_port("3000") + machine.succeed( + "curl --fail http://localhost:3000/login -H 'Accept: application/json' -H 'Referer: http://localhost:3000' --data 'username=user&password=foobar'" + ) + machine.fail( + "curl --fail http://localhost:3000/login -H 'Accept: application/json' -H 'Referer: http://localhost:3000' --data 'username=user&password=wrongpassword'" + ) + ''; + }; + container = nixosConfigurations.container.config.system.build.toplevel; }; From f229da352ebde742517a0c8f70df58d6b4b2eb88 Mon Sep 17 00:00:00 2001 From: Andreas Rammhold Date: Wed, 9 Sep 2020 22:47:36 +0200 Subject: [PATCH 6/7] LDAP add test for roles and multiple users --- flake.nix | 49 ++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/flake.nix b/flake.nix index 3daf1a46..842e4bcc 100644 --- a/flake.nix +++ b/flake.nix @@ -381,6 +381,18 @@ 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 @@ -388,6 +400,14 @@ 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"; @@ -413,11 +433,11 @@ user_field: cn user_search_options: deref: always - use_roles: 0 - role_basedn: "ou=groups,ou=OxObjects,dc=yourcompany,dc=com" - role_filter: "(&(objectClass=posixGroup)(memberUid=%s))" + use_roles: 1 + role_basedn: "ou=groups,dc=example" + role_filter: "(&(objectClass=groupOfNames)(member=%s))" role_scope: one - role_field: uid + role_field: cn role_value: dn role_search_options: deref: always @@ -425,15 +445,34 @@ 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") - machine.succeed( + 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"] ''; }; From b5d7ed2e99c6a32ed3d49fda9dfc7a4e7f13dc8f Mon Sep 17 00:00:00 2001 From: Andreas Rammhold Date: Thu, 10 Sep 2020 17:09:12 +0200 Subject: [PATCH 7/7] LDAP: add brief section in the documentation --- doc/manual/installation.xml | 56 +++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/doc/manual/installation.xml b/doc/manual/installation.xml index cb94dbbe..c9bb0291 100644 --- a/doc/manual/installation.xml +++ b/doc/manual/installation.xml @@ -272,6 +272,62 @@ server { +
+ Using LDAP as authentication backend (optional) + + Instead of using Hydra's built-in user management you can optionally use LDAP to manage roles and users. + + + + The hydra-server accepts the environment + variable HYDRA_LDAP_CONFIG. 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 + + Catalyst::Authentication::Store::LDAP documentation. + An example is given below. + + + + Roles can be assigned to users based on their LDAP group membership + (use_roles: 1 in the below example). + For a user to have the role 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: + 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 + +