Add REST Auth/UserDB/Password backend (closes: #1174)

This commit is contained in:
Xavier Guimard 2017-02-23 21:46:00 +00:00
parent 18c9215724
commit d7025a2251
17 changed files with 385 additions and 8 deletions

View File

@ -44,6 +44,7 @@ our $authParameters = {
proxyParams => [qw(proxyAuthService proxySessionService remoteCookieName proxyAuthnLevel proxyUseSoap)],
radiusParams => [qw(radiusAuthnLevel radiusSecret radiusServer)],
remoteParams => [qw(remotePortal remoteCookieName remoteGlobalStorage remoteGlobalStorageOptions)],
restParams => [qw(restAuthUrl restUserDBUrl restPwdConfirmUrl restPwdModifyUrl)],
slaveParams => [qw(slaveAuthnLevel slaveExportedVars slaveUserHeader slaveMasterIP slaveHeaderName slaveHeaderContent)],
sslParams => [qw(SSLAuthnLevel SSLVar)],
twitterParams => [qw(twitterAuthnLevel twitterKey twitterSecret twitterAppName)],

View File

@ -328,6 +328,10 @@ sub attributes {
'k' => 'Radius',
'v' => 'Radius'
},
{
'k' => 'REST',
'v' => 'REST'
},
{
'k' => 'Remote',
'v' => 'Remote'
@ -382,6 +386,10 @@ sub attributes {
'k' => 'LDAP',
'v' => 'LDAP'
},
{
'k' => 'REST',
'v' => 'REST'
},
{
'k' => 'Null',
'v' => 'None'
@ -432,6 +440,10 @@ sub attributes {
'k' => 'LDAP',
'v' => 'LDAP'
},
{
'k' => 'REST',
'v' => 'REST'
},
{
'k' => 'Null',
'v' => 'None'
@ -478,6 +490,10 @@ sub attributes {
'k' => 'Radius',
'v' => 'Radius'
},
{
'k' => 'REST',
'v' => 'REST'
},
{
'k' => 'SSL',
'v' => 'SSL'
@ -676,6 +692,10 @@ qr/(?:(?:https?):\/\/(?:(?:(?:(?:(?:(?:[a-zA-Z0-9][-a-zA-Z0-9]*)?[a-zA-Z0-9])[.]
'k' => 'Radius',
'v' => 'Radius'
},
{
'k' => 'REST',
'v' => 'REST'
},
{
'k' => 'SSL',
'v' => 'SSL'
@ -1799,6 +1819,10 @@ qr/^(?:(?:(?:(?:[a-zA-Z0-9][-a-zA-Z0-9]*)?[a-zA-Z0-9])[.])*(?:[a-zA-Z][-a-zA-Z0-
'k' => 'LDAP',
'v' => 'LDAP'
},
{
'k' => 'REST',
'v' => 'REST'
},
{
'k' => 'Null',
'v' => 'None'
@ -2077,14 +2101,26 @@ qr/(?:(?:https?):\/\/(?:(?:(?:(?:(?:(?:[a-zA-Z0-9][-a-zA-Z0-9]*)?[a-zA-Z0-9])[.]
'default' => 1,
'type' => 'bool'
},
'restAuthUrl' => {
'type' => 'url'
},
'restConfigServer' => {
'default' => 0,
'type' => 'bool'
},
'restPwdConfirmUrl' => {
'type' => 'url'
},
'restPwdModifyUrl' => {
'type' => 'url'
},
'restSessionServer' => {
'default' => 0,
'type' => 'bool'
},
'restUserDBUrl' => {
'type' => 'url'
},
'samlAttributeAuthorityDescriptorAttributeServiceSOAP' => {
'default' =>
'urn:oasis:names:tc:SAML:2.0:bindings:SOAP;#PORTAL#/saml/AA/SOAP;',
@ -2825,6 +2861,10 @@ qr/^(?:(?:(?:(?:(?:(?:[a-zA-Z0-9][-a-zA-Z0-9]*)?[a-zA-Z0-9])[.])*(?:[a-zA-Z][-a-
'k' => 'LDAP',
'v' => 'LDAP'
},
{
'k' => 'REST',
'v' => 'REST'
},
{
'k' => 'Null',
'v' => 'None'

View File

@ -1713,6 +1713,7 @@ sub attributes {
{ k => 'Google', v => 'Google' },
{ k => 'LDAP', v => 'LDAP' },
{ k => 'Radius', v => 'Radius' },
{ k => 'REST', v => 'REST' },
{ k => 'SSL', v => 'SSL' },
{ k => 'Twitter', v => 'Twitter' },
{ k => 'WebID', v => 'WebID' },
@ -1739,6 +1740,7 @@ sub attributes {
{ k => 'AD', v => 'Active Directory' },
{ k => 'DBI', v => 'Database (DBI)' },
{ k => 'LDAP', v => 'LDAP' },
{ k => 'REST', v => 'REST' },
{ k => 'Null', v => 'None' },
],
default => 'Same',
@ -1752,6 +1754,7 @@ sub attributes {
{ k => 'DBI', v => 'Database (DBI)' },
{ k => 'Demo', v => 'Demonstration' },
{ k => 'LDAP', v => 'LDAP' },
{ k => 'REST', v => 'REST' },
{ k => 'Null', v => 'None' },
],
default => 'Demo',
@ -1978,6 +1981,12 @@ m{^(?:ldapi://[^/]*/?|\w[\w\-\.]*(?::\d{1,5})?|ldap(?:s|\+tls)?://\w[\w\-\.]*(?:
radiusSecret => { type => 'text', },
radiusServer => { type => 'text', },
# REST
restAuthUrl => { type => 'url' },
restUserDBUrl => { type => 'url' },
restPwdConfirmUrl => { type => 'url' },
restPwdModifyUrl => { type => 'url' },
# Remote
remotePortal => { type => 'text', },
remoteGlobalStorage => {
@ -2167,6 +2176,7 @@ m{^(?:ldapi://[^/]*/?|\w[\w\-\.]*(?::\d{1,5})?|ldap(?:s|\+tls)?://\w[\w\-\.]*(?:
{ k => 'OpenIDConnect', v => 'OpenID Connect' },
{ k => 'Proxy', v => 'Proxy' },
{ k => 'Radius', v => 'Radius' },
{ k => 'REST', v => 'REST' },
{ k => 'Remote', v => 'Remote' },
{ k => 'SAML', v => 'SAML v2' },
{ k => 'Slave', v => 'Slave' },
@ -2182,6 +2192,7 @@ m{^(?:ldapi://[^/]*/?|\w[\w\-\.]*(?::\d{1,5})?|ldap(?:s|\+tls)?://\w[\w\-\.]*(?:
{ k => 'Facebook', v => 'Facebook' },
{ k => 'Google', v => 'Google' },
{ k => 'LDAP', v => 'LDAP' },
{ k => 'REST', v => 'REST' },
{ k => 'Null', v => 'None' },
{ k => 'OpenID', v => 'OpenID' },
{ k => 'OpenIDConnect', v => 'OpenID Connect' },
@ -2196,6 +2207,7 @@ m{^(?:ldapi://[^/]*/?|\w[\w\-\.]*(?::\d{1,5})?|ldap(?:s|\+tls)?://\w[\w\-\.]*(?:
{ k => 'DBI', v => 'Database (DBI)' },
{ k => 'Demo', v => 'Demo' },
{ k => 'LDAP', v => 'LDAP' },
{ k => 'REST', v => 'REST' },
{ k => 'Null', v => 'None' }
]
],
@ -2219,6 +2231,7 @@ m{^(?:ldapi://[^/]*/?|\w[\w\-\.]*(?::\d{1,5})?|ldap(?:s|\+tls)?://\w[\w\-\.]*(?:
{ k => 'Google', v => 'Google' },
{ k => 'LDAP', v => 'LDAP' },
{ k => 'Radius', v => 'Radius' },
{ k => 'REST', v => 'REST' },
{ k => 'SSL', v => 'SSL' },
{ k => 'Twitter', v => 'Twitter' },
{ k => 'WebID', v => 'WebID' },

View File

@ -296,6 +296,15 @@ sub tree {
'radiusServer'
]
},
{
title => 'restParams',
help => 'authrest.html',
form => 'simpleInputContainer',
nodes => [
'restAuthUrl', 'restUserDBUrl',
'restPwdConfirmUrl', 'restPwdModifyUrl'
]
},
{
title => 'remoteParams',
help => 'authremote.html',

View File

@ -568,10 +568,15 @@
"remotePortal": "Portal URL",
"replaceByFile": "Replace by file",
"requireToken": "Require token for forms",
"restAuthUrl": "Authentication URL",
"restConfigServer": "REST configuration server",
"restSessionServer": "REST session server",
"restore": "Restore",
"restoreConf": "Restore configuration",
"restParams": "REST parameters",
"restPwdConfirmUrl": "Password confirmation URL",
"restPwdModifyUrl": "Password change URL",
"restSessionServer": "REST session server",
"restUserDBUrl": "User data URL",
"returnUrl": "Return URL",
"rule": "Rule",
"rules": "Rules",

View File

@ -568,10 +568,15 @@
"remotePortal": "URL du portail",
"replaceByFile": "Remplacer par le fichier",
"requireToken": "Exige un jeton pour les formulaires",
"restAuthUrl": "URL d'authentification",
"restConfigServer": "Serveur de configurations REST",
"restSessionServer": "Serveur de sessions REST",
"restore": "Restaurer",
"restoreConf": "Restaurer la configuration",
"restParams": "Paramètres REST",
"restPwdConfirmUrl": "URL de confirmation de mot-de-passe",
"restPwdModifyUrl": "URL de modification de mot-de-passe",
"restSessionServer": "Serveur de sessions REST",
"restUserDBUrl": "URL de données utilisateurs",
"returnUrl": "URL de retour",
"rule": "Règle",
"rules": "Règles",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -24,6 +24,7 @@ lib/Lemonldap/NG/Portal/Auth/OpenIDConnect.pm
lib/Lemonldap/NG/Portal/Auth/Proxy.pm
lib/Lemonldap/NG/Portal/Auth/Radius.pm
lib/Lemonldap/NG/Portal/Auth/Remote.pm
lib/Lemonldap/NG/Portal/Auth/REST.pm
lib/Lemonldap/NG/Portal/Auth/SAML.pm
lib/Lemonldap/NG/Portal/Auth/Slave.pm
lib/Lemonldap/NG/Portal/Auth/SSL.pm
@ -51,6 +52,7 @@ lib/Lemonldap/NG/Portal/Lib/OpenID/SREG.pm
lib/Lemonldap/NG/Portal/Lib/OpenIDConnect.pm
lib/Lemonldap/NG/Portal/Lib/OtherSessions.pm
lib/Lemonldap/NG/Portal/Lib/Remote.pm
lib/Lemonldap/NG/Portal/Lib/REST.pm
lib/Lemonldap/NG/Portal/Lib/RESTProxy.pm
lib/Lemonldap/NG/Portal/Lib/SAML.pm
lib/Lemonldap/NG/Portal/Lib/Slave.pm
@ -74,6 +76,7 @@ lib/Lemonldap/NG/Portal/Password/Choice.pm
lib/Lemonldap/NG/Portal/Password/DBI.pm
lib/Lemonldap/NG/Portal/Password/Demo.pm
lib/Lemonldap/NG/Portal/Password/LDAP.pm
lib/Lemonldap/NG/Portal/Password/REST.pm
lib/Lemonldap/NG/Portal/Plugins/CDA.pm
lib/Lemonldap/NG/Portal/Plugins/ForceAuth.pm
lib/Lemonldap/NG/Portal/Plugins/GrantSession.pm
@ -103,6 +106,7 @@ lib/Lemonldap/NG/Portal/UserDB/OpenID.pm
lib/Lemonldap/NG/Portal/UserDB/OpenIDConnect.pm
lib/Lemonldap/NG/Portal/UserDB/Proxy.pm
lib/Lemonldap/NG/Portal/UserDB/Remote.pm
lib/Lemonldap/NG/Portal/UserDB/REST.pm
lib/Lemonldap/NG/Portal/UserDB/SAML.pm
lib/Lemonldap/NG/Portal/UserDB/Slave.pm
lib/Lemonldap/NG/Portal/UserDB/WebID.pm
@ -361,10 +365,11 @@ site/templates/pastel/yubikeyform.tpl
t/01-AuthDemo.t
t/02-Password-Demo.t
t/03-XSS-protection.t
t/19-Auth-Null.t
t/20-Auth-and-password-DBI.t
t/21-Auth-and-password-LDAP.t
t/22-Auth-and-password-AD.t
t/23-AuthNull.t
t/23-Auth-and-password-REST.t
t/24-AuthApache.t
t/25-AuthSlave.t
t/26-AuthRemote.t

View File

@ -0,0 +1,61 @@
package Lemonldap::NG::Portal::Auth::REST;
use strict;
use Mouse;
use Lemonldap::NG::Portal::Main::Constants qw(
PE_ERROR
PE_BADCREDENTIALS
PE_OK
);
our $VERSION = '2.0.0';
extends 'Lemonldap::NG::Portal::Auth::_WebForm',
'Lemonldap::NG::Portal::Lib::REST';
# INITIALIZATION
sub init {
my $self = shift;
# Add warning in log
unless ( $self->conf->{restAuthUrl} ) {
$self->logger->error('No Auth REST URL given');
return 0;
}
return $self->Lemonldap::NG::Portal::Auth::_WebForm::init();
}
sub authenticate {
my ( $self, $req ) = @_;
my $res = eval {
$self->restCall( $self->conf->{restAuthUrl},
{ user => $req->user, password => $req->datas->{password} } );
};
if ($@) {
$self->logger("Auth error: $@");
return PE_ERROR;
}
unless ( $res->{result} ) {
$self->userLogger->warn(
"Bad credentials for " . $req->user . ' (' . $req->address . ')' );
return PE_BADCREDENTIALS;
}
$req->datas->{restAuthInfo} = $res->{info};
return PE_OK;
}
sub setAuthSessionInfo {
my ( $self, $req ) = @_;
$self->SUPER::setAuthSessionInfo($req);
$req->sessionInfo->{$_} = $req->datas->{restAuthInfo}->{$_}
foreach ( keys %{ $req->datas->{restAuthInfo} } );
return PE_OK;
}
sub authLogout {
PE_OK;
}
1;

View File

@ -0,0 +1,29 @@
package Lemonldap::NG::Portal::Lib::REST;
use strict;
use Mouse;
use Lemonldap::NG::Common::UserAgent;
use JSON qw(from_json to_json);
has ua => (
is => 'rw',
default => sub {
return Lemonldap::NG::Common::UserAgent->new( $_[0]->{conf} );
}
);
sub restCall {
my($self,$url,$content) = @_;
my $hreq = HTTP::Request->new( POST => $url );
$hreq->header( 'Content-Type' => 'application/json' );
$hreq->content(to_json($content));
my $resp = $self->ua->request($hreq);
unless ( $resp->is_success ) {
die $resp->status_line;
}
my $res = eval { from_json( $resp->content ) };
die "Bad REST response: $@" if($@);
return $res;
}
1;

View File

@ -2,7 +2,10 @@ package Lemonldap::NG::Portal::Password::Demo;
use strict;
use Mouse;
use Lemonldap::NG::Portal::Main::Constants qw(PE_OK PE_ERROR);
use Lemonldap::NG::Portal::Main::Constants qw(
PE_ERROR
PE_PASSWORD_OK
);
extends 'Lemonldap::NG::Portal::Password::Base';
@ -22,7 +25,7 @@ sub modifyPassword {
my ( $self, $req, $pwd ) = @_;
# Nothing to do here, all new passwords are accepted
PE_OK;
PE_PASSWORD_OK;
}
1;

View File

@ -0,0 +1,52 @@
package Lemonldap::NG::Portal::Password::REST;
use strict;
use Mouse;
use Lemonldap::NG::Portal::Main::Constants qw(
PE_ERROR
PE_PASSWORD_OK
);
extends 'Lemonldap::NG::Portal::Password::Base',
'Lemonldap::NG::Portal::Lib::REST';
our $VERSION = '2.0.0';
sub init {
my ($self) = @_;
unless ($self->conf->{restPwdConfirmUrl}
and $self->conf->{restPwdModifyUrl} )
{
$self->logger->error('Missing REST password URL');
return 0;
}
return $self->SUPER::init;
}
sub confirm {
my ( $self, $req, $pwd ) = @_;
my $res = eval {
$self->restCall( $self->conf->{restPwdConfirmUrl},
{ user => $req->user, password => $pwd } );
};
if ($@) {
$self->logger("Pwd confirm error: $@");
return 0;
}
return ( $res->{result} ? 1 : 0 );
}
sub modifyPassword {
my ( $self, $req, $pwd ) = @_;
my $res = eval {
$self->restCall( $self->conf->{restPwdModifyUrl},
{ user => $req->user, password => $pwd } );
};
if ($@) {
$self->logger("Pwd confirm error: $@");
return PE_ERROR;
}
return ( $res->{result} ? PE_PASSWORD_OK : PE_ERROR );
}
1;

View File

@ -39,7 +39,6 @@ has demoAccounts => (
# INITIALIZATION
# Check AuthDemo use
sub init {
1;
}

View File

@ -0,0 +1,58 @@
package Lemonldap::NG::Portal::UserDB::REST;
use strict;
use Mouse;
use Lemonldap::NG::Portal::Main::Constants qw(
PE_ERROR
PE_OK
PE_USERNOTFOUND
);
extends 'Lemonldap::NG::Common::Module', 'Lemonldap::NG::Portal::Lib::REST';
our $VERSION = '2.0.0';
# INITIALIZATION
sub init {
my $self = shift;
# Add warning in log
unless ( $self->conf->{restUserDBUrl} ) {
$self->logger->error('No User REST URL given');
return 0;
}
return 1;
}
# RUNNING METHODS
sub getUser {
my ( $self, $req ) = @_;
my $res = eval {
$self->restCall( $self->conf->{restUserDBUrl}, { user => $req->user } );
};
if ($@) {
$self->logger->error("UserDB REST error: $@");
return PE_ERROR;
}
unless ( $res->{result} ) {
$self->userLogger->warn( 'User ' . $req->user . ' not found' );
return PE_USERNOTFOUND;
}
$req->datas->{restUserDBInfo} = $res->{info};
return PE_OK;
}
sub setSessionInfo {
my ( $self, $req ) = @_;
$req->sessionInfo->{$_} = $req->datas->{restUserDBInfo}->{$_}
foreach ( keys %{ $req->datas->{restUserDBInfo} } );
PE_OK;
}
sub setGroups {
PE_OK;
}
1;

View File

@ -0,0 +1,97 @@
use strict;
use IO::String;
use Test::More;
use LWP::UserAgent;
use JSON qw(to_json from_json);
BEGIN {
require 't/test-lib.pm';
}
my $res;
my $client = LLNG::Manager::Test->new(
{
ini => {
logLevel => 'error',
useSafeJail => 1,
authentication => 'REST',
userDB => 'Same',
passwordDB => 'REST',
restAuthUrl => 'http://ws/auth',
restUserDBUrl => 'http://ws/user',
restPwdConfirmUrl => 'http://ws/confirm',
restPwdModifyUrl => 'http://ws/modify',
}
}
);
ok(
$res = $client->_post(
'/', IO::String->new('user=dwho&password=dwho'),
length => 23,
accept => 'text/html',
),
'Auth query'
);
count(1);
expectRedirection( $res, 'http://auth.example.com/' );
my $id = expectCookie($res);
ok(
$res = $client->_post(
'/',
IO::String->new(
'oldpassword=dwho&newpassword=test&confirmpassword=test'),
cookie => "lemonldap=$id",
accept => 'application/json',
length => 54
),
'Change password'
);
count(1);
expectOK($res);
$client->logout($id);
clean_sessions();
done_testing( count() );
no warnings 'redefine';
sub LWP::UserAgent::request {
my ( $self, $req ) = @_;
ok( $req->uri =~ m#^http://ws/(auth|user|confirm|modify)#,
' ' . ucfirst($1) . ' REST request' )
or explain( $req->uri, 'http://ws/(auth|user)' );
my $type = $1;
count(1);
my $res = from_json( $req->content );
ok( $res->{user} eq 'dwho', ' User is dwho' );
count(1);
my $resp = HTTP::Response->new( 200, 'OK' );
if ( $type eq 'auth' ) {
ok( $res->{password} eq 'dwho', ' Password is dwho' )
or explain( $res, 'password: dwho' );
count(1);
$resp->content('{"result":true,"info":{"uid":"dwho"}}');
}
elsif ( $type eq 'modify' ) {
ok( $res->{password} eq 'test', ' Password is test' );
count(1);
$resp->content('{"result":true}');
}
elsif ( $type eq 'confirm' ) {
ok( $res->{password} eq 'dwho', ' Password is dwho' );
count(1);
$resp->content('{"result":true}');
}
elsif ( $type eq 'user' ) {
$resp->content('{"result":true,"info":{"cn":"dwho"}}');
}
else {
fail('Unknwon URL');
count(1);
}
return $resp;
}