Commit 395826f8 authored by Christophe Maudoux's avatar Christophe Maudoux 🐛

Rename U2F manager module to SFA

parent f808292b
......@@ -20,7 +20,7 @@ lib/Lemonldap/NG/Manager/Conf/Tests.pm
lib/Lemonldap/NG/Manager/Conf/Zero.pm
lib/Lemonldap/NG/Manager/Notifications.pm
lib/Lemonldap/NG/Manager/Sessions.pm
lib/Lemonldap/NG/Manager/U2F.pm
lib/Lemonldap/NG/Manager/SFA.pm
Makefile.PL
MANIFEST This list of files
META.yml
......@@ -34,7 +34,7 @@ site/coffee/llApp.coffee
site/coffee/manager.coffee
site/coffee/notifications.coffee
site/coffee/sessions.coffee
site/coffee/u2f.coffee
site/coffee/sfa.coffee
site/htdocs/manager.fcgi
site/htdocs/manager.psgi
site/htdocs/static/bwr/angular-animate/angular-animate.js
......@@ -150,8 +150,8 @@ site/htdocs/static/js/notifications.js
site/htdocs/static/js/notifications.min.js
site/htdocs/static/js/sessions.js
site/htdocs/static/js/sessions.min.js
site/htdocs/static/js/u2f.js
site/htdocs/static/js/u2f.min.js
site/htdocs/static/js/sfa.js
site/htdocs/static/js/sfa.min.js
site/htdocs/static/languages/ar.json
site/htdocs/static/languages/en.json
site/htdocs/static/languages/fr.json
......
......@@ -91,7 +91,7 @@ sub init {
# Find out more glyphicones at https://www.w3schools.com/icons/bootstrap_icons_glyphicons.asp
my $linksIcons =
{ 'conf' => 'cog', 'sessions' => 'duplicate', 'notifications' => 'bell', 'U2F' => 'wrench' };
{ 'conf' => 'cog', 'sessions' => 'duplicate', 'notifications' => 'bell', 'SFA' => 'wrench' };
$self->links( [] );
for ( my $i = 0 ; $i < @links ; $i++ ) {
......
package Lemonldap::NG::Manager::U2F;
use 5.10.0;
use utf8;
use strict;
use Mouse;
use MIME::Base64 qw(encode_base64 decode_base64);
#use Crypt::U2F::Server::Simple;
use Lemonldap::NG::Common::Session;
use Lemonldap::NG::Common::Conf::Constants;
use Lemonldap::NG::Common::PSGI::Constants;
use Lemonldap::NG::Common::Conf::ReConstants;
use feature 'state';
extends 'Lemonldap::NG::Common::Conf::AccessLib',
'Lemonldap::NG::Common::Session::REST';
our $VERSION = '2.0.0';
#############################
# I. INITIALIZATION METHODS #
#############################
use constant defaultRoute => 'u2f.html#/persistent';
sub addRoutes {
my ( $self, $conf ) = @_;
# Remote Procedure Call are defined in Lemonldap::NG::Common::Session::REST
# HTML template
$self->addRoute( 'u2f.html', undef, ['GET'] )
# READ
->addRoute(
u2f => { ':sessionType' => 'u2f' },
['GET']
)
# DELETE U2F KEY
->addRoute(
u2f => { ':sessionType' => { ':sessionId' => 'deleteU2FKey' } },
['DELETE']
)
# ADD U2F KEY
->addRoute(
u2f => { ':sessionType' => { ':sessionId' => 'registerU2FKey' } },
['PUT']
)
# VERIFY U2F KEY
->addRoute(
u2f => { ':sessionType' => { ':sessionId' => 'verifyU2FKey' } },
['POST']
);
$self->setTypes($conf);
$self->{ipField} ||= 'ipAddr';
$self->{multiValuesSeparator} ||= '; ';
$self->{hiddenAttributes} //= "_password";
}
############################
# II. REGISTRATION METHODS #
############################
sub registerU2FKey {
my ( $self, $req, $session, $skey ) = @_;
eval 'use Crypt::U2F::Server::Simple';
if ($@) {
$self->error("Can't load U2F library: $@");
return 0;
}
return $self->addU2FKey( $req, $session, $skey );
}
########################
# III. DISPLAY METHODS #
########################
sub u2f {
my ( $self, $req, $session, $skey ) = @_;
# Case 1: only one session is required
if ($session) {
return $self->session( $req, $session, $skey );
}
my $mod = $self->getMod($req)
or return $self->sendError( $req, undef, 400 );
my $params = $req->parameters();
my $type = delete $params->{sessionType};
$type = ucfirst($type);
my $res;
# Case 2: list of sessions
my $whatToTrace = Lemonldap::NG::Handler::PSGI::Main->tsv->{whatToTrace};
# 2.1 Get fields to require
my @fields = ( '_httpSessionType', $self->{ipField}, $whatToTrace, '_u2fKeyHandle' );
if ( my $groupBy = $params->{groupBy} ) {
$groupBy =~ s/^substr\((\w+)(?:,\d+(?:,\d+)?)?\)$/$1/;
$groupBy =~ s/^_whatToTrace$/$whatToTrace/o
or push @fields, $groupBy;
}
else {
push @fields, '_utime';
}
# 2.2 Restrict query if possible: search for filters (any query arg that is
# not a keyword)
my $moduleOptions = $mod->{options};
$moduleOptions->{backend} = $mod->{module};
my %filters = map {
my $s = $_;
$s =~ s/\b_whatToTrace\b/$whatToTrace/o;
/^(?:(?:group|order)By|doubleIp)$/
? ()
: ( $s => $params->{$_} );
} keys %$params;
$filters{_session_kind} = $type;
push @fields, keys(%filters);
{
my %seen;
@fields = grep { !$seen{$_}++ } @fields;
}
# For now, only one argument can be passed to
# Lemonldap::NG::Common::Apache::Session so just the first filter is
# used
my ($firstFilter) = sort {
$filters{$a} =~ m#^[\w:]+/\d+\*?$# ? 1
: $filters{$b} =~ m#^[\w:]+/\d+\*?$# ? -1
: $a eq '_session_kind' ? 1
: $b eq '_session_kind' ? -1
: $a cmp $b
} keys %filters;
# Check if a '*' is required
my $function = 'searchOn';
$function = 'searchOnExpr'
if ( grep { /\*/ and not m#^[\w:]+/\d+\*?$# }
( $filters{$firstFilter} ) );
$self->logger->debug(
"First filter: $firstFilter = $filters{$firstFilter} ($function)");
$res =
Lemonldap::NG::Common::Apache::Session->$function( $moduleOptions,
$firstFilter, $filters{$firstFilter}, @fields );
return $self->sendJSONresponse(
$req,
{
result => 1,
count => 0,
total => 0,
values => []
}
) unless ( $res and %$res );
delete $filters{$firstFilter}
unless ( grep { /\*/ and not m#^[\w:]+/\d+\*?$# }
( $filters{$firstFilter} ) );
foreach my $k ( keys %filters ) {
$self->logger->debug("Removing unless $k =~ /^$filters{$k}\$/");
if ( $filters{$k} =~ m#^([\w:]+)/(\d+)\*?$# ) {
my ( $net, $bits ) = ( $1, $2 );
foreach my $session ( keys %$res ) {
delete $res->{$session}
unless ( net6( $res->{$session}->{$k}, $bits ) eq $net );
}
}
else {
$filters{$k} =~ s/\./\\./g;
$filters{$k} =~ s/\*/\.\*/g;
foreach my $session ( keys %$res ) {
if ( $res->{$session}->{$k} ) {
delete $res->{$session}
unless ( $res->{$session}->{$k} =~ /^$filters{$k}$/ );
}
}
}
}
# Display sessions with registered U2F key only
foreach my $session ( keys %$res ) {
delete $res->{$session}
unless ( defined $res->{$session}->{_u2fKeyHandle} and length $res->{$session}->{_u2fKeyHandle} )
}
my $total = ( keys %$res );
if ( my $group = $req->params('groupBy') ) {
my $r;
$group =~ s/\b_whatToTrace\b/$whatToTrace/o;
# Substrings
if ( $group =~ /^substr\((\w+)(?:,(\d+)(?:,(\d+))?)?\)$/ ) {
my ( $field, $length, $start ) = ( $1, $2, $3 );
$start ||= 0;
$length = 1 if ( $length < 1 );
foreach my $k ( keys %$res ) {
$r->{ substr $res->{$k}->{$field}, $start, $length }++
if ( $res->{$k}->{$field} );
}
$group = $field;
}
# Simple field groupBy query
elsif ( $group =~ /^\w+$/ ) {
eval {
foreach my $k ( keys %$res ) {
$r->{ $res->{$k}->{$group} }++;
}
};
return $self->sendError(
$req,
qq{Use of an uninitialized attribute "$group" to group sessions},
400
) if ($@);
}
else {
return $self->sendError( $req, 'Syntax error in groupBy', 400 );
}
# Build result
$res = [
sort {
my @a = ( $a->{value} =~ /^(\d+)(?:\.(\d+))*$/ );
my @b = ( $b->{value} =~ /^(\d+)(?:\.(\d+))*$/ );
( @a and @b )
? ( $a[0] <=> $b[0]
or $a[1] <=> $b[1]
or $a[2] <=> $b[2]
or $a[3] <=> $b[3] )
: $a->{value} cmp $b->{value}
}
map { { value => $_, count => $r->{$_} } } keys %$r
];
}
# Else, $res elements will be like:
# { session => <sessionId>, date => <timestamp> }
else {
$res = [
sort { $a->{date} <=> $b->{date} }
map { { session => $_, date => $res->{$_}->{_utime} } }
keys %$res
];
}
return $self->sendJSONresponse(
$req,
{
result => 1,
count => scalar(@$res),
total => $total,
values => $res
}
);
}
1;
###
# Session explorer
###
# Max number of session to display (see overScheme)
max = 25
# Queries to do each type of display: each array item corresponds to the depth
# of opened nodes in the tree
schemes =
_whatToTrace: [
(t,v) ->
"groupBy=substr(#{t},1)"
(t,v) ->
"#{t}=#{v}*&groupBy=#{t}"
(t,v) ->
"#{t}=#{v}"
]
ipAddr: [
(t,v) ->
"groupBy=net(#{t},16,1)"
(t,v) ->
v = v + '.' unless v.match /:/
"#{t}=#{v}*&groupBy=net(#{t},32,2)"
(t,v) ->
v = v + '.' unless v.match /:/
"#{t}=#{v}*&groupBy=net(#{t},48,3)"
(t,v) ->
v = v + '.' unless v.match /:/
"#{t}=#{v}*&groupBy=net(#{t},128,4)"
(t,v) ->
"#{t}=#{v}&groupBy=_whatToTrace"
(t,v,q) ->
q.replace(/\&groupBy.*$/, '') + "&_whatToTrace=#{v}"
]
_startTime: [
(t,v) ->
"groupBy=substr(#{t},8)"
(t,v) ->
"#{t}=#{v}*&groupBy=substr(#{t},10)"
(t,v) ->
"#{t}=#{v}*&groupBy=substr(#{t},11)"
(t,v) ->
"#{t}=#{v}*&groupBy=substr(#{t},12)"
(t,v) ->
"#{t}=#{v}*&groupBy=_whatToTrace"
(t,v,q) ->
console.log t
console.log v
console.log q
q.replace(/\&groupBy.*$/, '') + "&_whatToTrace=#{v}"
]
doubleIp: [
(t,v) ->
t
(t,v) ->
"_whatToTrace=#{v}&groupBy=ipAddr"
(t,v,q) ->
q.replace(/\&groupBy.*$/, '') + "&ipAddr=#{v}"
]
overScheme =
_whatToTrace: (t,v,level,over) ->
if level == 1
"#{t}=#{v}*&groupBy=substr(#{t},#{(level+over+1)})"
else
null
ipAddr: (t,v,level,over) ->
if level > 0 and level < 4
"#{t}=#{v}*&groupBy=net(#{t},#{16*level+4*(over+1)},2)"
else
null
hiddenAttributes = '_password'
# Attributes to group in session display
categories =
dateTitle: ['_utime', '_startTime', '_updateTime', '_lastAuthnUTime', '_lastSeen']
connectionTitle: ['ipAddr', '_timezone', '_url']
authenticationTitle:['_session_id', '_user', '_password', 'authenticationLevel']
modulesTitle: ['_auth', '_userDB', '_passwordDB', '_issuerDB', '_authChoice', '_authMulti', '_userDBMulti']
saml: ['_idp', '_idpConfKey', '_samlToken', '_lassoSessionDump', '_lassoIdentityDump']
groups: ['groups', 'hGroups']
ldap: ['dn']
BrowserID: ['_browserIdAnswer', '_browserIdAnswerRaw']
OpenIDConnect: ['_oidc_id_token', '_oidc_OP', '_oidc_access_token']
# Menu entries
menu =
delU2FKey: [
title: 'deleteU2FKey'
icon: 'trash'
]
addU2FKey: [
title: 'addU2FKey'
icon: 'plus'
]
verifyU2FKey: [
title: 'verifyU2FKey'
icon: 'check'
]
home: []
###
# AngularJS application
###
llapp = angular.module 'llngSessionsExplorer', ['ui.tree', 'ui.bootstrap', 'llApp']
# Main controller
llapp.controller 'SessionsExplorerCtrl', ['$scope', '$translator', '$location', '$q', '$http', ($scope, $translator, $location, $q, $http) ->
$scope.links = links
$scope.menulinks = menulinks
$scope.staticPrefix = staticPrefix
$scope.scriptname = scriptname
$scope.formPrefix = formPrefix
$scope.availableLanguages = availableLanguages
$scope.waiting = true
$scope.showM = false
$scope.showT = true
$scope.data = []
$scope.currentScope = null
$scope.currentSession = null
$scope.menu = menu
# Import translations functions
$scope.translateP = $translator.translateP
$scope.translate = $translator.translate
$scope.translateTitle = (node) ->
$translator.translateField node, 'title'
sessionType = 'global'
# Handle menu items
$scope.menuClick = (button) ->
if button.popup
window.open button.popup
else
button.action = button.title unless button.action
switch typeof button.action
when 'function'
button.action $scope.currentNode, $scope
when 'string'
$scope[button.action]()
else
console.log typeof button.action
$scope.showM = false
# SESSION MANAGEMENT
# Delete U2F key
$scope.deleteU2FKey = ->
$scope.waiting = true
$http['delete']("#{scriptname}u2f/#{sessionType}/#{$scope.currentSession.id}").then (response) ->
$scope.currentSession = null
$scope.currentScope.remove()
$scope.waiting = false
, (resp) ->
$scope.currentSession = null
$scope.currentScope.remove()
$scope.waiting = false
$scope.showT = false
# Add U2F key
$scope.addU2FKey = ->
$scope.waiting = true
$http['put']("#{scriptname}u2f/#{sessionType}/#{$scope.currentSession.id}").then (response) ->
#$scope.currentSession = null
#$scope.currentScope.remove()
$scope.waiting = false
, (resp) ->
#$scope.currentSession = null
#$scope.currentScope.remove()
$scope.waiting = false
$scope.showT = false
$http.get("#{scriptname}u2f/#{sessionType}/#{$scope.currentSession.id}").then (response) ->
$scope.currentSession = transformSession response.data
$scope.showT = false
# Verify U2F key
$scope.verifyU2FKey = ->
$scope.waiting = true
$http['post']("#{scriptname}u2f/#{sessionType}/#{$scope.currentSession.id}").then (response) ->
#$scope.currentSession = null
#$scope.currentScope.remove()
$scope.waiting = false
, (resp) ->
#$scope.currentSession = null
#$scope.currentScope.remove()
$scope.waiting = false
$scope.showT = true
$http.get("#{scriptname}u2f/#{sessionType}/#{$scope.currentSession.id}").then (response) ->
$scope.currentSession = transformSession response.data
$scope.showT = false
# Open node
$scope.stoggle = (scope) ->
node = scope.$modelValue
if node.nodes.length == 0
$scope.updateTree node.value, node.nodes, node.level, node.over, node.query, node.count
scope.toggle()
# Display selected session
$scope.displaySession = (scope) ->
# Private functions
# Session preparation
transformSession = (session) ->
_stToStr = (s) ->
s
_insert = (re, title) ->
tmp = []
reg = new RegExp(re)
for key,value of session
if key.match(reg) and value
tmp.push
title: key
value: value
delete session[key]
if tmp.length > 0
res.push
title: title
nodes: tmp
time = session._utime
id = session._session_id
# 1. Replace values if needed
for key, value of session
unless value
delete session[key]
else
if typeof session == 'string' and value.match(/; /)
session[key] = value.split '; '
if typeof session[key] != 'object'
if hiddenAttributes.match(new RegExp('\b' + key + '\b'))
session[key] = '********'
else if key.match /^(_utime|_lastAuthnUTime|_lastSeen|notification)$/
session[key] = $scope.localeDate value
else if key.match /^(_startTime|_updateTime)$/
session[key] = _stToStr value
#else if key.match /^(_u2fKeyHandle|_u2fUserKey)$/
# session[key] = '########'
res = []
# 2. Push session keys in result, grouped by categories
for category, attrs of categories
subres = []
for attr in attrs
if session[attr]
subres.push
title: attr
value: session[attr]
delete session[attr]
if subres.length >0
res.push
title: "__#{category}__"
nodes: subres
# 3. Add OpenID and notifications already notified
_insert '^openid', 'OpenID'
_insert '^notification_(.+)', '__notificationsDone__'
# 4. Add session history if exists
if session._loginHistory
tmp = []
if session._loginHistory.successLogin
for l in session._loginHistory.successLogin
tmp.push
t: l._utime
title: $scope.localeDate l._utime
value: "Success (IP #{l.ipAddr})"
if session._loginHistory.failedLogin
for l in session._loginHistory.failedLogin
tmp.push
t: l._utime
title: $scope.localeDate l._utime
value: "#{l.error} (IP #{l.ipAddr})"
delete session._loginHistory
tmp.sort (a,b) ->
a.t - b.t
res.push
title: '__loginHistory__'
nodes: tmp
# 5. Other keys (attributes and macros)
tmp = []
for key, value of session
tmp.push
title: key
value: value
tmp.sort (a,b) ->
if a.title > b.title then 1
else if a.title < b.title then -1
else 0
res.push
title: '__attributesAndMacros__'
nodes: tmp
return {
_utime: time
id: id
nodes: res
}
$scope.currentScope = scope
sessionId = scope.$modelValue.session
$http.get("#{scriptname}u2f/#{sessionType}/#{sessionId}").then (response) ->
$scope.currentSession = transformSession response.data
$scope.showT = false
$scope.localeDate = (s) ->
d = new Date(s * 1000)
return d.toLocaleString()
# Function to change interface language
$scope.getLanguage = (lang) ->
$scope.lang = lang
$scope.form = 'white'
$scope.init()
$scope.showM = false
# URI local path management
pathEvent = (event, next, current) ->
n = next.match /#\/(\w+)/
sessionType = 'global'
if n == null
$scope.type = '_whatToTrace'
else if n[1].match /^(persistent)$/
sessionType = RegExp.$1
$scope.type = '_session_uid'
else
$scope.type = n[1]
$scope.init()
$scope.$on '$locationChangeSuccess', pathEvent
# Function to update tree: download value of opened subkey
autoId = 0
$scope.updateTree = (value, node, level, over, currentQuery, count) ->
$scope.waiting = true
# Query scheme selection:
# - if defined above
scheme = if schemes[$scope.type]
schemes[$scope.type]
# - _updateTime must be displayed as startDate
else if $scope.type == '_updateTime'
schemes._startTime
# - default to _whatToTrace scheme
else
schemes._whatToTrace
# Build query using schemes
query = scheme[level] $scope.type, value, currentQuery
# If number of session exceeds "max" and overScheme exists, call it
if count > max and overScheme[$scope.type]
if tmp = overScheme[$scope.type] $scope.type, value, level, over, currentQuery
over++
query = tmp
level = level - 1
else
over = 0
else
over = 0
# Launch HTTP query
$http.get("#{scriptname}u2f/#{sessionType}?#{query}").then (response) ->
data = response.data
if data.result
for n in data.values
autoId++
n.id = "node#{autoId}"
if level < scheme.length - 1
n.nodes = []
n.level = level + 1
n.query = query
n.over = over
# Date display in tree
if $scope.type.match /^(?:start|update)Time$/
n.title = n.value
# 12 digits -> 12:34
.replace(/^(\d{8})(\d{2})(\d{2})$/,'$2:$3')
# 11 digits -> 12:30
.replace(/^(\d{8})(\d{2})(\d)$/,'$2:$30')
# 10 digits -> 12h
.replace(/^(\d{8})(\d{2})$/,'$2h')