Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: NixOS/hydra
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: e707990e2d6a
Choose a base ref
...
head repository: NixOS/hydra
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 8f683aa7f84a
Choose a head ref
  • 8 commits
  • 4 files changed
  • 4 contributors

Commits on Sep 9, 2020

  1. Copy the full SHA
    28646e1 View commit details
  2. include perlPackages.YAML in buildInputs

    edef1c authored and andir committed Sep 9, 2020

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    b9ff7b2 View commit details
  3. Copy the full SHA
    c00b42d View commit details

Commits on Sep 10, 2020

  1. 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.
    andir committed Sep 10, 2020
    Copy the full SHA
    b8c1933 View commit details
  2. 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.
    andir committed Sep 10, 2020
    Copy the full SHA
    cfc01e2 View commit details
  3. Copy the full SHA
    f229da3 View commit details

Commits on Sep 11, 2020

  1. Copy the full SHA
    b5d7ed2 View commit details
  2. Merge pull request #805 from andir/ldap

    LDAP support continued (with missing packages & tests)
    grahamc authored Sep 11, 2020
    Copy the full SHA
    8f683aa View commit details
Showing with 284 additions and 4 deletions.
  1. +56 −0 doc/manual/installation.xml
  2. +186 −1 flake.nix
  3. +5 −1 src/lib/Hydra.pm
  4. +37 −2 src/lib/Hydra/Controller/User.pm
56 changes: 56 additions & 0 deletions doc/manual/installation.xml
Original file line number Diff line number Diff line change
@@ -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: "(&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>

<!--
187 changes: 186 additions & 1 deletion flake.nix
Original file line number Diff line number Diff line change
@@ -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;
};

6 changes: 5 additions & 1 deletion src/lib/Hydra.pm
Original file line number Diff line number Diff line change
@@ -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' => {
39 changes: 37 additions & 2 deletions src/lib/Hydra/Controller/User.pm
Original file line number Diff line number Diff line change
@@ -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) = @_;