diff --git a/lemonldap-ng-common/lib/Lemonldap/NG/Common/Conf/Constants.pm b/lemonldap-ng-common/lib/Lemonldap/NG/Common/Conf/Constants.pm index a43405b8e..330298a9d 100644 --- a/lemonldap-ng-common/lib/Lemonldap/NG/Common/Conf/Constants.pm +++ b/lemonldap-ng-common/lib/Lemonldap/NG/Common/Conf/Constants.pm @@ -26,6 +26,8 @@ use constant APPLYSECTION => "apply"; our $hashParameters = qr/^(?:(?:l(?:o(?:ca(?:lSessionStorageOption|tionRule)|goutService)|dapExportedVar|wpSslOpt)|(?:(?:d(?:emo|bi)|facebook|webID)ExportedVa|exported(?:Heade|Va))r|ca(?:s(?:StorageOption|Attribute)|ptchaStorageOption)|g(?:r(?:antSessionRule|oup)|lobalStorageOption)|n(?:otificationStorageOption|ginxCustomHandler)|re(?:moteGlobalStorageOption|loadUrl)|CAS_proxiedService|macro)s|o(?:idc(?:RPMetaData(?:(?:Option(?:sExtraClaim)?|ExportedVar)s|Node)|OPMetaData(?:(?:ExportedVar|Option)s|J(?:SON|WKS)|Node)|S(?:erviceMetaDataAuthnContext|torageOptions))|penIdExportedVars)|s(?:aml(?:S(?:PMetaData(?:(?:ExportedAttribute|Option)s|Node|XML)|torageOptions)|IDPMetaData(?:(?:ExportedAttribute|Option)s|Node|XML))|essionDataToRemember|laveExportedVars)|p(?:ersistentStorageOptions|o(?:rtalSkinRules|st))|a(?:uthChoiceModules|pplicationList)|v(?:hostOptions|irtualHost))$/; +sub NO {qr/^(?:off|no|0)?$/i} + our %EXPORT_TAGS = ( 'all' => [ qw( @@ -43,6 +45,7 @@ our %EXPORT_TAGS = ( MANAGERSECTION SESSIONSEXPLORERSECTION APPLYSECTION + NO $hashParameters ) ] diff --git a/lemonldap-ng-common/lib/Lemonldap/NG/Common/Conf/RESTServer.pm b/lemonldap-ng-common/lib/Lemonldap/NG/Common/Conf/RESTServer.pm new file mode 100644 index 000000000..7a83ff69e --- /dev/null +++ b/lemonldap-ng-common/lib/Lemonldap/NG/Common/Conf/RESTServer.pm @@ -0,0 +1,712 @@ +package Lemonldap::NG::Common::Conf::RESTServer; + +use strict; +use Mouse; +use Lemonldap::NG::Common::Conf::Constants; +use Lemonldap::NG::Manager::Constants; + +our $VERSION = '2.0.0'; + +extends 'Lemonldap::NG::Common::Conf::AccessLib'; + +####################### +# I. PRIVATE METHODS # +####################### + +## @method scalar getConfKey($req, $key) +# Return key value +# +# Return the value of $key key in current configuration. If cfgNum is set to +# `latest`, get before last configuration number. +# +# Errors: set an error in $req->error and return undef if: +# * query does not have a cfgNum parameter (set by Common/PSGI/Router.pm) +# * cfgNum is not a number +# +#@param $req Lemonldap::NG::Common::PSGI::Request +#@param $key Key name +#@return keyvalue (string, int or hashref) +sub getConfKey { + my ( $self, $req, $key, @args ) = @_; + $self->lmLog( "Search for $key in conf", 'debug' ); + + # Verify that cfgNum has been asked + unless ( defined $req->params('cfgNum') ) { + $req->error("Missing configuration number"); + return undef; + } + $self->lmLog( "Cfgnum set to " . $req->params('cfgNum'), 'debug' ); + + # when 'latest' => replace by last cfgNum + if ( $req->params('cfgNum') eq 'latest' ) { + my $tmp = $self->confAcc->lastCfg; + $req->set_param( 'cfgNum', $tmp ); + if ($Lemonldap::NG::Common::Conf::msg) { + $req->error($Lemonldap::NG::Common::Conf::msg); + return undef; + } + } + elsif ( $req->params('cfgNum') !~ /^\d+$/ ) { + $req->error("cfgNum must be a number"); + return undef; + } + unless ( + defined $self->getConfByNum( scalar( $req->params('cfgNum') ), @args ) ) + { + $req->error( "Configuration " + . $req->params('cfgNum') + . " is not available (" + . $Lemonldap::NG::Common::Conf::msg + . ')' ); + return undef; + } + + # TODO: insert default values + # Set an error if key is not defined + return $self->currentConf->{$key}; +} + +sub getConfByNum { + my ( $self, $cfgNum, @args ) = @_; + unless ( %{ $self->currentConf } + and $cfgNum == $self->currentConf->{cfgNum} ) + { + my $tmp; + if ( $cfgNum == 0 ) { + require Lemonldap::NG::Manager::Conf::Zero; + $tmp = Lemonldap::NG::Manager::Conf::Zero::zeroConf(); + } + else { + $tmp = + $self->confAcc->getConf( + { cfgNum => $cfgNum, raw => 1, noCache => 1, @args } ); + return undef unless ( $tmp and ref($tmp) and %$tmp ); + } + $self->currentConf($tmp); + } + return $cfgNum; +} + +######################## +# II. Display methods # +######################## + +# Values are send depending of the /path/info/. For example, +# /confs/1/portal to get portal value. + +# This section contains several methods: +# - complex nodes: +# * complexNodesRoot() call for root queries (no subkeys) to display the list +# * virtualHosts() +# * _samlMetaDataNodes() is called by saml(IDP|RP)MetaDataNode +# * _oidcMetaDataNodes() is called by oidc(OP|RP)MetaDataNodes +# - other special nodes: +# * authChoiceModules() +# * grantSessionRules() +# * openIdIDPList() (old OpenID) +# * applicationList() +# - root: +# root query (/confs/latest for example) is redirected to metadatas() +# - other requests: +# they are managed by getKey() +# - newRSAKey() returns a new RSA key pair if /confs/newRSAKey is called in a +# POST request +# - prx() load a request and return the content (for SAML/OIDC metadatas) + +# 31 - Complex subnodes +# ---------------- + +## @method PSGI-JSON-response complexNodesRoot($req, $query, $tpl) +# Respond to root requests for virtual hosts and SAMLmetadatas +# +#@param $req Lemonldap::NG::Common::PSGI::Request +#@param $query Configuration root key +#@param $tpl Javascript template to use (see JS/JSON generator script) +#@return PSGI JSON response +sub complexNodesRoot { + my ( $self, $req, $query, $tpl ) = @_; + $self->lmLog( "Query for $query template keys", 'debug' ); + + my $tmp = $self->getConfKey( $req, $query ); + return $self->sendError( $req, undef, 400 ) if ( $req->error ); + + my @res; + if ( ref($tmp) ) { + foreach my $f ( sort keys %$tmp ) { + push @res, + { + id => "${tpl}s/$f", + title => $f, + type => $tpl, + template => $tpl + }; + } + } + return $self->sendJSONresponse( $req, \@res ); +} + +# 311 - Virtual hosts +# ------------- + +## @method PSGI-JSON-response virtualHosts($req, @path) +# Respond to virtualhosts sub requests +# +#@param $req Lemonldap::NG::Common::PSGI::Request +#@param @path words in path after `virtualhosts` +#@return PSGI JSON response +sub virtualHosts { + my ( $self, $req, @path ) = @_; + + return $self->complexNodesRoot( $req, 'locationRules', 'virtualHost' ) + unless (@path); + + my $vh = shift @path; + my $query; + unless ( $query = shift @path ) { + return $self->sendError( $req, + 'Bad request: virtualHost query must ask for a key', 400 ); + } + + # Send setDefault for new vhosts + return $self->sendError( $req, 'setDefault', 200 ) if ( $vh =~ /^new__/ ); + + # Reject unknown vhosts + return $self->sendError( $req, "Unknown virtualhost ($vh)", 400 ) + unless ( defined $self->getConfKey( $req, 'locationRules' )->{$vh} ); + + if ( $query =~ /^(?:(?:exportedHeader|locationRule)s|post)$/ ) { + my ( $id, $resp ) = ( 1, [] ); + my $vhk = eval { $self->getConfKey( $req, $query )->{$vh} } // {}; + return $self->sendError( $req, undef, 400 ) if ( $req->error ); + $self->lmLog( "Query for $vh/$query keys", 'debug' ); + + # Keys are ordered except 'default' which must be at the end + foreach my $r ( + sort { + $query eq 'locationRules' + ? ( + $a eq 'default' + ? 1 + : ( $b eq 'default' ? -1 : $a cmp $b ) + ) + : $a cmp $b + } keys %$vhk + ) + { + my $res = { + id => "virtualHosts/$vh/$query/" . $id++, + title => $r, + data => $vhk->{$r}, + type => 'keyText', + }; + + # If rule contains a comment, split it + if ( $query eq 'locationRules' ) { + $res->{comment} = ''; + if ( $r =~ s/\(\?#(.*?)\)// ) { + $res->{title} = $res->{comment} = $1; + } + $res->{re} = $r; + $res->{type} = 'rule'; + } + elsif ( $query eq 'post' ) { + $res->{data} = $vhk->{$r}; + $res->{type} = 'post'; + } + push @$resp, $res; + } + return $self->sendJSONresponse( $req, $resp ); + } + elsif ( $query =~ /^vhost(?:(?:Aliase|Http)s|Maintenance|Port)$/ ) { + $self->lmLog( "Query for $vh/$query key", 'debug' ); + + # TODO: verify how this is done actually + my $k1 = $self->getConfKey( $req, 'vhostOptions' ); + return $self->sendError( $req, undef, 400 ) if ( $req->error ); + + # Default values are set by JS + my $res = eval { $k1->{$vh}->{$query} } // undef; + return $self->sendJSONresponse( $req, { value => $res } ); + } + else { + return $self->sendError( $req, "Unknown vhost subkey ($query)", 400 ); + } +} + +# 312 - SAML +# ---- + +## @method PSGI-JSON-response _samlMetaDataNode($type, $req, @path) +# Respond to SAML metadata subnodes +# +#@param $type `SP` or `IDP` +#@param $req Lemonldap::NG::Common::PSGI::Request +#@param @path words in path after `saml{IDP|SP}MetaDataNode` +#@return PSGI JSON response +sub _samlMetaDataNodes { + my ( $self, $type, $req, @path ) = @_; + + return $self->complexNodesRoot( $req, "saml${type}MetaDataXML", + "saml${type}MetaDataNode" ) + unless (@path); + my $partner = shift @path; + my $query = shift @path; + unless ($query) { + return $self->sendError( $req, + "Bad request: saml${type}MetaDataNode query must ask for a key", + 400 ); + } + + # setDefault response for new partners + return $self->sendError( $req, 'setDefault', 200 ) + if ( $partner =~ /^new__/ ); + + # Reject unknown partners + return $self->sendError( $req, "Unknown SAML partner ($partner)", 400 ) + unless ( + defined eval { + $self->getConfKey( $req, "saml${type}MetaDataXML" )->{$partner}; + } + ); + + my ( $id, $resp ) = ( 1, [] ); + + # Return all exported attributes if asked + if ( $query =~ /^saml${type}MetaDataExportedAttributes$/ ) { + my $pk = + eval { $self->getConfKey( $req, $query )->{$partner} } // {}; + return $self->sendError( $req, undef, 400 ) if ( $req->error ); + foreach my $h ( sort keys %$pk ) { + push @$resp, + { + id => "saml${type}MetaDataNodes/$partner/$query/" . $id++, + title => $h, + data => [ split /;/, $pk->{$h} ], + type => 'samlAttribute', + }; + } + return $self->sendJSONresponse( $req, $resp ); + } + + # Simple root keys + elsif ( $query =~ /^saml${type}MetaDataXML$/ ) { + my $value = + eval { $self->getConfKey( $req, $query )->{$partner}->{$query}; } + // undef; + return $self->sendError( $req, undef, 400 ) if ( $req->error ); + return $self->sendJSONresponse( $req, { value => $value } ); + } + + # These regexps are generated by jsongenerator.pl and stored in + # Lemonldap::NG::Manager::Constants + elsif ( + $query =~ { + IDP => qr/^$samlIDPMetaDataNodeKeys$/o, + SP => qr/^$samlSPMetaDataNodeKeys$/o + }->{$type} + ) + { + my $value = eval { + $self->getConfKey( $req, "saml${type}MetaDataOptions" )->{$partner} + ->{$query}; + } // undef; + + # Note that types "samlService" and "samlAssertion" will be splitted by + # manager.js in an array + return $self->sendJSONresponse( $req, { value => $value } ); + } + else { + return $self->sendError( $req, + "Bad key for saml${type}MetaDataNode ($query)", 400 ); + } +} + +## @method PSGI-JSON-response samlIDPMetaDataNode($req, @path) +# Launch _samlMetaDataNode('IDP', @_) +# +#@param $req Lemonldap::NG::Common::PSGI::Request +#@param @path words in path after `samlIDPMetaDataNode` +#@return PSGI JSON response +sub samlIDPMetaDataNodes { + my ( $self, $req, @path ) = @_; + return $self->_samlMetaDataNodes( 'IDP', $req, @path ); +} + +## @method PSGI-JSON-response samlSPMetaDataNode($req, @path) +# Launch _samlMetaDataNode('SP', @_) +# +#@param $req Lemonldap::NG::Common::PSGI::Request +#@param @path words in path after `samlSPMetaDataNode` +#@return PSGI JSON response +sub samlSPMetaDataNodes { + my ( $self, $req, @path ) = @_; + return $self->_samlMetaDataNodes( 'SP', $req, @path ); +} + +# 313 - OpenID-Connect +# -------------- + +## @method PSGI-JSON-response _oidcMetaDataNodes($type, $req, @path) +# Respond to OpenID-Connect metadata subnodes +# +#@param $type `OP` or `RP` +#@param $req Lemonldap::NG::Common::PSGI::Request +#@param @path words in path after `oidc{OP|RP}MetaDataNode` +#@return PSGI JSON response +sub _oidcMetaDataNodes { + my ( $self, $type, $req, @path ) = @_; + + my $refKey = + ( $type eq 'RP' ? 'oidcRPMetaDataOptions' : 'oidcOPMetaDataJSON' ); + return $self->complexNodesRoot( $req, $refKey, "oidc${type}MetaDataNode" ) + unless (@path); + + my $partner = shift @path; + my $query = shift @path; + unless ($query) { + return $self->sendError( $req, + "Bad request: oidc${type}MetaDataNode query must ask for a key", + 400 ); + } + + # setDefault response for new partners + return $self->sendError( $req, 'setDefault', 200 ) + if ( $partner =~ /^new__/ ); + + # Reject unknown partners + return $self->sendError( $req, + "Unknown OpenID-Connect partner ($partner)", 400 ) + unless ( defined eval { $self->getConfKey( $req, $refKey )->{$partner}; } + ); + + my ( $id, $resp ) = ( 1, [] ); + + # Return all exported attributes if asked + if ( $query =~ + /^(?:oidc${type}MetaDataExportedVars|oidcRPMetaDataOptionsExtraClaims)$/ + ) + { + my $pk = eval { $self->getConfKey( $req, $query )->{$partner} } // {}; + return $self->sendError( $req, undef, 400 ) if ( $req->error ); + foreach my $h ( sort keys %$pk ) { + push @$resp, + { + id => "oidc${type}MetaDataNodes/$partner/$query/" . $id++, + title => $h, + data => $pk->{$h}, + type => 'keyText', + }; + } + return $self->sendJSONresponse( $req, $resp ); + } + + # Long text types (OP only) + elsif ( $query =~ /^oidcOPMetaData(?:JSON|JWKS)$/ ) { + my $value = + eval { $self->getConfKey( $req, $query )->{$partner}; } // undef; + return $self->sendError( $req, undef, 400 ) if ( $req->error ); + return $self->sendJSONresponse( $req, { value => $value } ); + } + + # Options + elsif ( + $query =~ { + OP => qr/^$oidcOPMetaDataNodeKeys$/o, + RP => qr/^$oidcRPMetaDataNodeKeys$/o + }->{$type} + ) + { + my $value = eval { + $self->getConfKey( $req, "oidc${type}MetaDataOptions" )->{$partner} + ->{$query}; + } // undef; + return $self->sendJSONresponse( $req, { value => $value } ); + } + else { + return $self->sendError( $req, + "Bad key for oidc${type}MetaDataNode ($query)", 400 ); + } +} + +## @method PSGI-JSON-response oidcOPMetaDataNodes($req, @path) +# Launch _oidcMetaDataNodes('SP', @_) +# +#@param $req Lemonldap::NG::Common::PSGI::Request +#@param @path words in path after `oidcOPMetaDataNode` +#@return PSGI JSON response +sub oidcOPMetaDataNodes { + my ( $self, $req, @path ) = @_; + return $self->_oidcMetaDataNodes( 'OP', $req, @path ); +} + +## @method PSGI-JSON-response oidcRPMetaDataNodes($req, @path) +# Launch _oidcMetaDataNodes('SP', @_) +# +#@param $req Lemonldap::NG::Common::PSGI::Request +#@param @path words in path after `oidcRPMetaDataNode` +#@return PSGI JSON response +sub oidcRPMetaDataNodes { + my ( $self, $req, @path ) = @_; + return $self->_oidcMetaDataNodes( 'RP', $req, @path ); +} + +# 32 - Other special nodes +# ------------------- + +# 321 - Choice authentication + +## @method PSGI-JSON-response authChoiceModules($req,$key) +# Returns authChoiceModules keys splitted in arrays +# +#@param $req Lemonldap::NG::Common::PSGI::Request +#@param key optional subkey +#@return PSGI JSON response +sub authChoiceModules { + my ( $self, $req, $key ) = @_; + my $value = $self->getConfKey( $req, 'authChoiceModules' ); + unless ($key) { + my @res; + foreach my $k ( sort keys %$value ) { + push @res, + { + id => "authChoiceModules/$k", + title => "$k", + data => [ split /;/, $value->{$k} ], + type => 'authChoice' + }; + } + return $self->sendJSONresponse( $req, \@res ); + } + else { + my $r = $value->{$key} ? [ split( /[;\|]/, $value->{$key} ) ] : []; + return $self->sendJSONresponse( $req, { value => $r } ); + } +} + +# 322 - Rules to grant sessions + +## @method PSGI-JSON-response grantSessionRules($req) +# Split grantSessionRules key=>value into 3 elements +# +#@param $req Lemonldap::NG::Common::PSGI::Request +#@return PSGI JSON response +sub grantSessionRules { + my ( $self, $req, $key ) = @_; + return $self->sendError( 'Subkeys forbidden for grantSessionRules', 400 ) + if ($key); + my $value = $self->getConfKey( $req, 'grantSessionRules' ); + my @res; + + sub _sort { + my $A = ( $a =~ /^.*?##(.*)$/ )[0]; + my $B = ( $b =~ /^.*?##(.*)$/ )[0]; + return !$A ? 1 : !$B ? -1 : $A cmp $B; + } + my $id = 0; + foreach my $k ( sort _sort keys %$value ) { + my $r = $k; + my $c = ( $r =~ s/^(.*)?##(.*)$/$1/ ? $2 : '' ); + $id++; + push @res, + { + id => "grantSessionRules/$id", + title => $c || $r, + re => $r, + comment => $c, + data => $value->{$k}, + type => 'grant' + }; + } + return $self->sendJSONresponse( $req, \@res ); +} + +# 323 - (old)OpenID IDP black/white list + +##method PSGI-JSON-response openIdIDPList($req) +# Split openIdIDPList parameter into 2 elements +sub openIdIDPList { + my ( $self, $req, $key ) = @_; + return $self->sendError( 'Subkeys forbidden for openIdIDPList', 400 ) + if ($key); + my $value = $self->getConfKey( $req, 'openIdIDPList' ); + $value //= '0;'; + my ( $type, $v ) = split /;/, $value; + $v //= ''; + return $self->sendJSONresponse( $req, { value => [ $type, $v ] } ); +} + +# 324 - Application for menu +# -------------------- + +## @method PSGI-JSON-response applicationList($req, @other) +# Return the full menu tree +# +#@param $req Lemonldap::NG::Common::PSGI::Request +#@param @other words in path after `applicationList` +#@return PSGI JSON response +sub applicationList { + my ( $self, $req, @other ) = @_; + return $self->sendError( $req, + 'There is no subkey for applicationList', 400 ) + if (@other); + my $apps = $self->getConfKey( $req, 'applicationList' ); + return $self->sendError( $req, undef, 400 ) if ( $req->error ); + $apps = {} unless ( ref($apps) eq 'HASH' ); + my $json = $self->_scanCatsAndApps( $apps, 'applicationList' ); + return $self->sendJSONresponse( $req, $json ); +} + +## @method arrayRef _scanCatsAndApps($apps) +# Recursive method used to build categories & applications menu +# +#@param $apps HashRef pointing to a subnode of catAndApps conf tree +#@return arrayRef +sub _scanCatsAndApps { + my ( $self, $apps, $baseId ) = @_; + my @res; + + foreach my $cat ( grep { not /^(?:catname|type)$/ } sort keys %$apps ) { + my $item = { id => "$baseId/$cat" }; + if ( $apps->{$cat}->{type} eq 'category' ) { + $item->{title} = $apps->{$cat}->{catname}; + $item->{type} = 'menuCat'; + $item->{nodes} = + $self->_scanCatsAndApps( $apps->{$cat}, "$baseId/$cat" ); + } + else { + $item->{title} = $apps->{$cat}->{options}->{name}; + $item->{type} = $apps->{$cat}->{type} = 'menuApp'; + foreach my $o ( + grep { not /^name$/ } + keys %{ $apps->{$cat}->{options} } + ) + { + $item->{data}->{$o} = $apps->{$cat}->{options}->{$o}; + } + } + push @res, $item; + } + return \@res; +} + +# 33 - Root queries +# ----------- + +## @method PSGI-JSON-response metadatas($req) +# Respond to `/conf/:cfgNum` requests by sending configuration metadatas +# +# NB: if `full=1` is set in the query, configuration is returned directly in +# JSON +# +#@param $req Lemonldap::NG::Common::PSGI::Request +#@return PSGI JSON response +sub metadatas { + my ( $self, $req ) = @_; + if ( $req->params('full') and $req->params('full') !~ NO ) { + my $c = $self->getConfKey( $req, 'cfgNum' ); + return $self->sendError( $req, undef, 400 ) if ( $req->error ); + $self->userNotice( 'User ' + . $self->userId($req) + . ' ask for full configuration ' + . $c ); + return $self->sendJSONresponse( + $req, + $self->currentConf, + forceJSON => 1, + headers => [ + 'Content-Disposition' => "Attachment; filename=lmConf-$c.json" + ], + ); + } + else { + my $res = {}; + $res->{cfgNum} = $self->getConfKey( $req, 'cfgNum' ); + return $self->sendError( $req, undef, 400 ) if ( $req->error ); + return $self->sendError( $req, "Configuration without cfgNum", 500 ) + unless ( defined $res->{cfgNum} ); + foreach my $key (qw(cfgAuthor cfgDate cfgAuthorIP cfgLog)) { + $res->{$key} = $self->getConfKey( $req, $key ); + } + + # Find next and previous conf + my @a = $self->confAcc->available; + my $id = -1; + my ($ind) = map { $id++; $_ == $res->{cfgNum} ? ($id) : () } @a; + if ($ind) { $res->{prev} = $a[ $ind - 1 ]; } + if ( $ind and $ind < $#a ) { $res->{next} = $a[ $ind + 1 ]; } + $self->userNotice( 'User ' + . $self->userId($req) + . ' ask for configuration metadatas (' + . $res->{cfgNum} + . ')' ); + return $self->sendJSONresponse( $req, $res ); + } +} + +# 34 - Other values +# ------------ + +## @method PSGI-JSON-response getKey($req, $key, $subkey) +# Return the value of a root key of current configuration +# +#@param $req Lemonldap::NG::Common::PSGI::Request +#@param $key Name of key requested +#@param $subkey Subkey for hash values +#@return PSGI JSON response +sub getKey { + my ( $self, $req, $key, $subkey ) = @_; + unless ($key) { + return $self->metadatas($req); + } + $self->userInfo( 'User ' . $self->userId($req) . " asks for key $key" ); + my $value = $self->getConfKey( $req, $key ); + return $self->sendError( $req, undef, 400 ) if ( $req->error ); + + # When "hash" + if ( $key =~ qr/^$simpleHashKeys$/o ) { + return $self->sendError( $req, 'setDefault', 200 ) + unless defined($value); + + # If a hash key is asked return its value + if ($subkey) { + return $self->sendJSONresponse( $req, + { value => $value->{$subkey} // undef, } ); + } + + # else return the list of keys + my @res; + foreach my $k ( sort keys %$value ) { + push @res, + { + id => "$key/$k", + title => "$k", + data => $value->{$k}, + type => 'keyText' + }; + } + return $self->sendJSONresponse( $req, \@res ); + } + elsif ( $key =~ qr/^$doubleHashKeys$/o ) { + my @res; + if ( defined $value ) { + foreach my $host ( sort keys %$value ) { + my @tmp; + foreach my $k ( sort keys %{ $value->{$host} } ) { + push @tmp, { k => $k, v => $value->{$host}->{$k} }; + } + push @res, { k => $host, h => \@tmp }; + } + } + return $self->sendJSONresponse( $req, { value => \@res } ); + } + + # When scalar + return $self->sendError( $req, "Key $key is not a hash", 400 ) + if ($subkey); + return $self->sendError( $req, 'setDefault', 200 ) unless defined($value); + return $self->sendJSONresponse( $req, { value => $value } ); + + # TODO authParam key +} + +1; diff --git a/lemonldap-ng-common/lib/Lemonldap/NG/Common/PSGI/Constants.pm b/lemonldap-ng-common/lib/Lemonldap/NG/Common/PSGI/Constants.pm index 3e8cb2af5..d5057acc9 100644 --- a/lemonldap-ng-common/lib/Lemonldap/NG/Common/PSGI/Constants.pm +++ b/lemonldap-ng-common/lib/Lemonldap/NG/Common/PSGI/Constants.pm @@ -15,7 +15,6 @@ use constant { NOTICE => 1, ERROR => 0, }; -our $no = qr/^(?:off|no|0)?$/i; our %EXPORT_TAGS = ( 'all' => [qw(DEBUG INFO WARN ERROR $no)] ); our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } ); diff --git a/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Build.pm b/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Build.pm index c12e97892..63cc5142c 100644 --- a/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Build.pm +++ b/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Build.pm @@ -302,6 +302,8 @@ use constant SESSIONSEXPLORERSECTION => "sessionsExplorer"; use constant APPLYSECTION => "apply"; $confConstants +sub NO {qr/^(?:off|no|0)?\$/i} + our %EXPORT_TAGS = ( 'all' => [ qw( @@ -319,6 +321,7 @@ our %EXPORT_TAGS = ( MANAGERSECTION SESSIONSEXPLORERSECTION APPLYSECTION + NO \$hashParameters ) ] diff --git a/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Conf.pm b/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Conf.pm index 3829d0d6a..006493a5d 100644 --- a/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Conf.pm +++ b/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Conf.pm @@ -1,24 +1,22 @@ # This module implements all the methods that responds to '/confs/*' requests -# It contains 4 sections: +# It contains 2 sections: # - initialization methods -# - private methods (to access required conf) -# - display methods # - upload method +# +# Read methods are inherited from Lemonldap::NG::Common::Conf::RESTServer package Lemonldap::NG::Manager::Conf; use 5.10.0; use utf8; use Mouse; use Lemonldap::NG::Common::Conf::Constants; -use Lemonldap::NG::Common::PSGI::Constants; -use Lemonldap::NG::Manager::Constants; use Crypt::OpenSSL::RSA; use Convert::PEM; use URI::URL; use feature 'state'; -extends 'Lemonldap::NG::Common::Conf::AccessLib'; +extends 'Lemonldap::NG::Common::Conf::RESTServer'; our $VERSION = '2.0.0'; @@ -65,707 +63,6 @@ sub addRoutes { ->addRoute( 'prx', undef, ['POST'] ); } -####################### -# II. PRIVATE METHODS # -####################### - -## @method scalar getConfKey($req, $key) -# Return key value -# -# Return the value of $key key in current configuration. If cfgNum is set to -# `latest`, get before last configuration number. -# -# Errors: set an error in $req->error and return undef if: -# * query does not have a cfgNum parameter (set by Common/PSGI/Router.pm) -# * cfgNum is not a number -# -#@param $req Lemonldap::NG::Common::PSGI::Request -#@param $key Key name -#@return keyvalue (string, int or hashref) -sub getConfKey { - my ( $self, $req, $key, @args ) = @_; - state $confAcc ||= $self->confAcc; - $self->lmLog( "Search for $key in conf", 'debug' ); - - # Verify that cfgNum has been asked - unless ( defined $req->params('cfgNum') ) { - $req->error("Missing configuration number"); - return undef; - } - $self->lmLog( "Cfgnum set to " . $req->params('cfgNum'), 'debug' ); - - # when 'latest' => replace by last cfgNum - if ( $req->params('cfgNum') eq 'latest' ) { - my $tmp = $self->confAcc->lastCfg; - $req->set_param( 'cfgNum', $tmp ); - if ($Lemonldap::NG::Common::Conf::msg) { - $req->error($Lemonldap::NG::Common::Conf::msg); - return undef; - } - } - elsif ( $req->params('cfgNum') !~ /^\d+$/ ) { - $req->error("cfgNum must be a number"); - return undef; - } - unless ( - defined $self->getConfByNum( scalar( $req->params('cfgNum') ), @args ) ) - { - $req->error( "Configuration " - . $req->params('cfgNum') - . " is not available (" - . $Lemonldap::NG::Common::Conf::msg - . ')' ); - return undef; - } - - # TODO: insert default values - # Set an error if key is not defined - return $self->currentConf->{$key}; -} - -sub getConfByNum { - my ( $self, $cfgNum, @args ) = @_; - unless ( %{ $self->currentConf } - and $cfgNum == $self->currentConf->{cfgNum} ) - { - my $tmp; - if ( $cfgNum == 0 ) { - require Lemonldap::NG::Manager::Conf::Zero; - $tmp = Lemonldap::NG::Manager::Conf::Zero::zeroConf(); - } - else { - $tmp = - $self->confAcc->getConf( - { cfgNum => $cfgNum, raw => 1, noCache => 1, @args } ); - return undef unless ( $tmp and ref($tmp) and %$tmp ); - } - $self->currentConf($tmp); - } - return $cfgNum; -} - -######################## -# III. Display methods # -######################## - -# Values are send depending of the /path/info/. For example, -# /confs/1/portal to get portal value. - -# This section contains several methods: -# - complex nodes: -# * complexNodesRoot() call for root queries (no subkeys) to display the list -# * virtualHosts() -# * _samlMetaDataNodes() is called by saml(IDP|RP)MetaDataNode -# * _oidcMetaDataNodes() is called by oidc(OP|RP)MetaDataNodes -# - other special nodes: -# * authChoiceModules() -# * grantSessionRules() -# * openIdIDPList() (old OpenID) -# * applicationList() -# - root: -# root query (/confs/latest for example) is redirected to metadatas() -# - other requests: -# they are managed by getKey() -# - newRSAKey() returns a new RSA key pair if /confs/newRSAKey is called in a -# POST request -# - prx() load a request and return the content (for SAML/OIDC metadatas) - -# 31 - Complex subnodes -# ---------------- - -## @method PSGI-JSON-response complexNodesRoot($req, $query, $tpl) -# Respond to root requests for virtual hosts and SAMLmetadatas -# -#@param $req Lemonldap::NG::Common::PSGI::Request -#@param $query Configuration root key -#@param $tpl Javascript template to use (see JS/JSON generator script) -#@return PSGI JSON response -sub complexNodesRoot { - my ( $self, $req, $query, $tpl ) = @_; - $self->lmLog( "Query for $query template keys", 'debug' ); - - my $tmp = $self->getConfKey( $req, $query ); - return $self->sendError( $req, undef, 400 ) if ( $req->error ); - - my @res; - if ( ref($tmp) ) { - foreach my $f ( sort keys %$tmp ) { - push @res, - { - id => "${tpl}s/$f", - title => $f, - type => $tpl, - template => $tpl - }; - } - } - return $self->sendJSONresponse( $req, \@res ); -} - -# 311 - Virtual hosts -# ------------- - -## @method PSGI-JSON-response virtualHosts($req, @path) -# Respond to virtualhosts sub requests -# -#@param $req Lemonldap::NG::Common::PSGI::Request -#@param @path words in path after `virtualhosts` -#@return PSGI JSON response -sub virtualHosts { - my ( $self, $req, @path ) = @_; - - return $self->complexNodesRoot( $req, 'locationRules', 'virtualHost' ) - unless (@path); - - my $vh = shift @path; - my $query; - unless ( $query = shift @path ) { - return $self->sendError( $req, - 'Bad request: virtualHost query must ask for a key', 400 ); - } - - # Send setDefault for new vhosts - return $self->sendError( $req, 'setDefault', 200 ) if ( $vh =~ /^new__/ ); - - # Reject unknown vhosts - return $self->sendError( $req, "Unknown virtualhost ($vh)", 400 ) - unless ( defined $self->getConfKey( $req, 'locationRules' )->{$vh} ); - - if ( $query =~ /^(?:(?:exportedHeader|locationRule)s|post)$/ ) { - my ( $id, $resp ) = ( 1, [] ); - my $vhk = eval { $self->getConfKey( $req, $query )->{$vh} } // {}; - return $self->sendError( $req, undef, 400 ) if ( $req->error ); - $self->lmLog( "Query for $vh/$query keys", 'debug' ); - - # Keys are ordered except 'default' which must be at the end - foreach my $r ( - sort { - $query eq 'locationRules' - ? ( - $a eq 'default' - ? 1 - : ( $b eq 'default' ? -1 : $a cmp $b ) - ) - : $a cmp $b - } keys %$vhk - ) - { - my $res = { - id => "virtualHosts/$vh/$query/" . $id++, - title => $r, - data => $vhk->{$r}, - type => 'keyText', - }; - - # If rule contains a comment, split it - if ( $query eq 'locationRules' ) { - $res->{comment} = ''; - if ( $r =~ s/\(\?#(.*?)\)// ) { - $res->{title} = $res->{comment} = $1; - } - $res->{re} = $r; - $res->{type} = 'rule'; - } - elsif ( $query eq 'post' ) { - $res->{data} = $vhk->{$r}; - $res->{type} = 'post'; - } - push @$resp, $res; - } - return $self->sendJSONresponse( $req, $resp ); - } - elsif ( $query =~ /^vhost(?:(?:Aliase|Http)s|Maintenance|Port)$/ ) { - $self->lmLog( "Query for $vh/$query key", 'debug' ); - - # TODO: verify how this is done actually - my $k1 = $self->getConfKey( $req, 'vhostOptions' ); - return $self->sendError( $req, undef, 400 ) if ( $req->error ); - - # Default values are set by JS - my $res = eval { $k1->{$vh}->{$query} } // undef; - return $self->sendJSONresponse( $req, { value => $res } ); - } - else { - return $self->sendError( $req, "Unknown vhost subkey ($query)", 400 ); - } -} - -# 312 - SAML -# ---- - -## @method PSGI-JSON-response _samlMetaDataNode($type, $req, @path) -# Respond to SAML metadata subnodes -# -#@param $type `SP` or `IDP` -#@param $req Lemonldap::NG::Common::PSGI::Request -#@param @path words in path after `saml{IDP|SP}MetaDataNode` -#@return PSGI JSON response -sub _samlMetaDataNodes { - my ( $self, $type, $req, @path ) = @_; - - return $self->complexNodesRoot( $req, "saml${type}MetaDataXML", - "saml${type}MetaDataNode" ) - unless (@path); - my $partner = shift @path; - my $query = shift @path; - unless ($query) { - return $self->sendError( $req, - "Bad request: saml${type}MetaDataNode query must ask for a key", - 400 ); - } - - # setDefault response for new partners - return $self->sendError( $req, 'setDefault', 200 ) - if ( $partner =~ /^new__/ ); - - # Reject unknown partners - return $self->sendError( $req, "Unknown SAML partner ($partner)", 400 ) - unless ( - defined eval { - $self->getConfKey( $req, "saml${type}MetaDataXML" )->{$partner}; - } - ); - - my ( $id, $resp ) = ( 1, [] ); - - # Return all exported attributes if asked - if ( $query =~ /^saml${type}MetaDataExportedAttributes$/ ) { - my $pk = - eval { $self->getConfKey( $req, $query )->{$partner} } // {}; - return $self->sendError( $req, undef, 400 ) if ( $req->error ); - foreach my $h ( sort keys %$pk ) { - push @$resp, - { - id => "saml${type}MetaDataNodes/$partner/$query/" . $id++, - title => $h, - data => [ split /;/, $pk->{$h} ], - type => 'samlAttribute', - }; - } - return $self->sendJSONresponse( $req, $resp ); - } - - # Simple root keys - elsif ( $query =~ /^saml${type}MetaDataXML$/ ) { - my $value = - eval { $self->getConfKey( $req, $query )->{$partner}->{$query}; } - // undef; - return $self->sendError( $req, undef, 400 ) if ( $req->error ); - return $self->sendJSONresponse( $req, { value => $value } ); - } - - # These regexps are generated by jsongenerator.pl and stored in - # Lemonldap::NG::Manager::Constants - elsif ( - $query =~ { - IDP => qr/^$samlIDPMetaDataNodeKeys$/o, - SP => qr/^$samlSPMetaDataNodeKeys$/o - }->{$type} - ) - { - my $value = eval { - $self->getConfKey( $req, "saml${type}MetaDataOptions" )->{$partner} - ->{$query}; - } // undef; - - # Note that types "samlService" and "samlAssertion" will be splitted by - # manager.js in an array - return $self->sendJSONresponse( $req, { value => $value } ); - } - else { - return $self->sendError( $req, - "Bad key for saml${type}MetaDataNode ($query)", 400 ); - } -} - -## @method PSGI-JSON-response samlIDPMetaDataNode($req, @path) -# Launch _samlMetaDataNode('IDP', @_) -# -#@param $req Lemonldap::NG::Common::PSGI::Request -#@param @path words in path after `samlIDPMetaDataNode` -#@return PSGI JSON response -sub samlIDPMetaDataNodes { - my ( $self, $req, @path ) = @_; - return $self->_samlMetaDataNodes( 'IDP', $req, @path ); -} - -## @method PSGI-JSON-response samlSPMetaDataNode($req, @path) -# Launch _samlMetaDataNode('SP', @_) -# -#@param $req Lemonldap::NG::Common::PSGI::Request -#@param @path words in path after `samlSPMetaDataNode` -#@return PSGI JSON response -sub samlSPMetaDataNodes { - my ( $self, $req, @path ) = @_; - return $self->_samlMetaDataNodes( 'SP', $req, @path ); -} - -# 313 - OpenID-Connect -# -------------- - -## @method PSGI-JSON-response _oidcMetaDataNodes($type, $req, @path) -# Respond to OpenID-Connect metadata subnodes -# -#@param $type `OP` or `RP` -#@param $req Lemonldap::NG::Common::PSGI::Request -#@param @path words in path after `oidc{OP|RP}MetaDataNode` -#@return PSGI JSON response -sub _oidcMetaDataNodes { - my ( $self, $type, $req, @path ) = @_; - - my $refKey = - ( $type eq 'RP' ? 'oidcRPMetaDataOptions' : 'oidcOPMetaDataJSON' ); - return $self->complexNodesRoot( $req, $refKey, "oidc${type}MetaDataNode" ) - unless (@path); - - my $partner = shift @path; - my $query = shift @path; - unless ($query) { - return $self->sendError( $req, - "Bad request: oidc${type}MetaDataNode query must ask for a key", - 400 ); - } - - # setDefault response for new partners - return $self->sendError( $req, 'setDefault', 200 ) - if ( $partner =~ /^new__/ ); - - # Reject unknown partners - return $self->sendError( $req, - "Unknown OpenID-Connect partner ($partner)", 400 ) - unless ( defined eval { $self->getConfKey( $req, $refKey )->{$partner}; } - ); - - my ( $id, $resp ) = ( 1, [] ); - - # Return all exported attributes if asked - if ( $query =~ - /^(?:oidc${type}MetaDataExportedVars|oidcRPMetaDataOptionsExtraClaims)$/ - ) - { - my $pk = eval { $self->getConfKey( $req, $query )->{$partner} } // {}; - return $self->sendError( $req, undef, 400 ) if ( $req->error ); - foreach my $h ( sort keys %$pk ) { - push @$resp, - { - id => "oidc${type}MetaDataNodes/$partner/$query/" . $id++, - title => $h, - data => $pk->{$h}, - type => 'keyText', - }; - } - return $self->sendJSONresponse( $req, $resp ); - } - - # Long text types (OP only) - elsif ( $query =~ /^oidcOPMetaData(?:JSON|JWKS)$/ ) { - my $value = - eval { $self->getConfKey( $req, $query )->{$partner}; } // undef; - return $self->sendError( $req, undef, 400 ) if ( $req->error ); - return $self->sendJSONresponse( $req, { value => $value } ); - } - - # Options - elsif ( - $query =~ { - OP => qr/^$oidcOPMetaDataNodeKeys$/o, - RP => qr/^$oidcRPMetaDataNodeKeys$/o - }->{$type} - ) - { - my $value = eval { - $self->getConfKey( $req, "oidc${type}MetaDataOptions" )->{$partner} - ->{$query}; - } // undef; - return $self->sendJSONresponse( $req, { value => $value } ); - } - else { - return $self->sendError( $req, - "Bad key for oidc${type}MetaDataNode ($query)", 400 ); - } -} - -## @method PSGI-JSON-response oidcOPMetaDataNodes($req, @path) -# Launch _oidcMetaDataNodes('SP', @_) -# -#@param $req Lemonldap::NG::Common::PSGI::Request -#@param @path words in path after `oidcOPMetaDataNode` -#@return PSGI JSON response -sub oidcOPMetaDataNodes { - my ( $self, $req, @path ) = @_; - return $self->_oidcMetaDataNodes( 'OP', $req, @path ); -} - -## @method PSGI-JSON-response oidcRPMetaDataNodes($req, @path) -# Launch _oidcMetaDataNodes('SP', @_) -# -#@param $req Lemonldap::NG::Common::PSGI::Request -#@param @path words in path after `oidcRPMetaDataNode` -#@return PSGI JSON response -sub oidcRPMetaDataNodes { - my ( $self, $req, @path ) = @_; - return $self->_oidcMetaDataNodes( 'RP', $req, @path ); -} - -# 32 - Other special nodes -# ------------------- - -# 321 - Choice authentication - -## @method PSGI-JSON-response authChoiceModules($req,$key) -# Returns authChoiceModules keys splitted in arrays -# -#@param $req Lemonldap::NG::Common::PSGI::Request -#@param key optional subkey -#@return PSGI JSON response -sub authChoiceModules { - my ( $self, $req, $key ) = @_; - my $value = $self->getConfKey( $req, 'authChoiceModules' ); - unless ($key) { - my @res; - foreach my $k ( sort keys %$value ) { - push @res, - { - id => "authChoiceModules/$k", - title => "$k", - data => [ split /;/, $value->{$k} ], - type => 'authChoice' - }; - } - return $self->sendJSONresponse( $req, \@res ); - } - else { - my $r = $value->{$key} ? [ split( /[;\|]/, $value->{$key} ) ] : []; - return $self->sendJSONresponse( $req, { value => $r } ); - } -} - -# 322 - Rules to grant sessions - -## @method PSGI-JSON-response grantSessionRules($req) -# Split grantSessionRules key=>value into 3 elements -# -#@param $req Lemonldap::NG::Common::PSGI::Request -#@return PSGI JSON response -sub grantSessionRules { - my ( $self, $req, $key ) = @_; - return $self->sendError( 'Subkeys forbidden for grantSessionRules', 400 ) - if ($key); - my $value = $self->getConfKey( $req, 'grantSessionRules' ); - my @res; - - sub _sort { - my $A = ( $a =~ /^.*?##(.*)$/ )[0]; - my $B = ( $b =~ /^.*?##(.*)$/ )[0]; - return !$A ? 1 : !$B ? -1 : $A cmp $B; - } - my $id = 0; - foreach my $k ( sort _sort keys %$value ) { - my $r = $k; - my $c = ( $r =~ s/^(.*)?##(.*)$/$1/ ? $2 : '' ); - $id++; - push @res, - { - id => "grantSessionRules/$id", - title => $c || $r, - re => $r, - comment => $c, - data => $value->{$k}, - type => 'grant' - }; - } - return $self->sendJSONresponse( $req, \@res ); -} - -# 323 - (old)OpenID IDP black/white list - -##method PSGI-JSON-response openIdIDPList($req) -# Split openIdIDPList parameter into 2 elements -sub openIdIDPList { - my ( $self, $req, $key ) = @_; - return $self->sendError( 'Subkeys forbidden for openIdIDPList', 400 ) - if ($key); - my $value = $self->getConfKey( $req, 'openIdIDPList' ); - $value //= '0;'; - my ( $type, $v ) = split /;/, $value; - $v //= ''; - return $self->sendJSONresponse( $req, { value => [ $type, $v ] } ); -} - -# 324 - Application for menu -# -------------------- - -## @method PSGI-JSON-response applicationList($req, @other) -# Return the full menu tree -# -#@param $req Lemonldap::NG::Common::PSGI::Request -#@param @other words in path after `applicationList` -#@return PSGI JSON response -sub applicationList { - my ( $self, $req, @other ) = @_; - return $self->sendError( $req, - 'There is no subkey for applicationList', 400 ) - if (@other); - my $apps = $self->getConfKey( $req, 'applicationList' ); - return $self->sendError( $req, undef, 400 ) if ( $req->error ); - $apps = {} unless ( ref($apps) eq 'HASH' ); - my $json = $self->_scanCatsAndApps( $apps, 'applicationList' ); - return $self->sendJSONresponse( $req, $json ); -} - -## @method arrayRef _scanCatsAndApps($apps) -# Recursive method used to build categories & applications menu -# -#@param $apps HashRef pointing to a subnode of catAndApps conf tree -#@return arrayRef -sub _scanCatsAndApps { - my ( $self, $apps, $baseId ) = @_; - my @res; - - foreach my $cat ( grep { not /^(?:catname|type)$/ } sort keys %$apps ) { - my $item = { id => "$baseId/$cat" }; - if ( $apps->{$cat}->{type} eq 'category' ) { - $item->{title} = $apps->{$cat}->{catname}; - $item->{type} = 'menuCat'; - $item->{nodes} = - $self->_scanCatsAndApps( $apps->{$cat}, "$baseId/$cat" ); - } - else { - $item->{title} = $apps->{$cat}->{options}->{name}; - $item->{type} = $apps->{$cat}->{type} = 'menuApp'; - foreach my $o ( - grep { not /^name$/ } - keys %{ $apps->{$cat}->{options} } - ) - { - $item->{data}->{$o} = $apps->{$cat}->{options}->{$o}; - } - } - push @res, $item; - } - return \@res; -} - -# 33 - Root queries -# ----------- - -## @method PSGI-JSON-response metadatas($req) -# Respond to `/conf/:cfgNum` requests by sending configuration metadatas -# -# NB: if `full=1` is set in the query, configuration is returned directly in -# JSON -# -#@param $req Lemonldap::NG::Common::PSGI::Request -#@return PSGI JSON response -sub metadatas { - my ( $self, $req ) = @_; - if ( $req->params('full') and $req->params('full') !~ $no ) { - my $c = $self->getConfKey( $req, 'cfgNum' ); - return $self->sendError( $req, undef, 400 ) if ( $req->error ); - $self->userNotice( 'User ' - . $self->userId($req) - . ' ask for full configuration ' - . $c ); - return $self->sendJSONresponse( - $req, - $self->currentConf, - forceJSON => 1, - headers => [ - 'Content-Disposition' => "Attachment; filename=lmConf-$c.json" - ], - ); - } - else { - my $res = {}; - $res->{cfgNum} = $self->getConfKey( $req, 'cfgNum' ); - return $self->sendError( $req, undef, 400 ) if ( $req->error ); - return $self->sendError( $req, "Configuration without cfgNum", 500 ) - unless ( defined $res->{cfgNum} ); - foreach my $key (qw(cfgAuthor cfgDate cfgAuthorIP cfgLog)) { - $res->{$key} = $self->getConfKey( $req, $key ); - } - - # Find next and previous conf - my @a = $self->confAcc->available; - my $id = -1; - my ($ind) = map { $id++; $_ == $res->{cfgNum} ? ($id) : () } @a; - if ($ind) { $res->{prev} = $a[ $ind - 1 ]; } - if ( $ind and $ind < $#a ) { $res->{next} = $a[ $ind + 1 ]; } - $self->userNotice( 'User ' - . $self->userId($req) - . ' ask for configuration metadatas (' - . $res->{cfgNum} - . ')' ); - return $self->sendJSONresponse( $req, $res ); - } -} - -# 34 - Other values -# ------------ - -## @method PSGI-JSON-response getKey($req, $key, $subkey) -# Return the value of a root key of current configuration -# -#@param $req Lemonldap::NG::Common::PSGI::Request -#@param $key Name of key requested -#@param $subkey Subkey for hash values -#@return PSGI JSON response -sub getKey { - my ( $self, $req, $key, $subkey ) = @_; - unless ($key) { - return $self->metadatas($req); - } - $self->userInfo( 'User ' . $self->userId($req) . " asks for key $key" ); - my $value = $self->getConfKey( $req, $key ); - return $self->sendError( $req, undef, 400 ) if ( $req->error ); - - # When "hash" - if ( $key =~ qr/^$simpleHashKeys$/o ) { - return $self->sendError( $req, 'setDefault', 200 ) - unless defined($value); - - # If a hash key is asked return its value - if ($subkey) { - return $self->sendJSONresponse( $req, - { value => $value->{$subkey} // undef, } ); - } - - # else return the list of keys - my @res; - foreach my $k ( sort keys %$value ) { - push @res, - { - id => "$key/$k", - title => "$k", - data => $value->{$k}, - type => 'keyText' - }; - } - return $self->sendJSONresponse( $req, \@res ); - } - elsif ( $key =~ qr/^$doubleHashKeys$/o ) { - my @res; - if ( defined $value ) { - foreach my $host ( sort keys %$value ) { - my @tmp; - foreach my $k ( sort keys %{ $value->{$host} } ) { - push @tmp, { k => $k, v => $value->{$host}->{$k} }; - } - push @res, { k => $host, h => \@tmp }; - } - } - return $self->sendJSONresponse( $req, { value => \@res } ); - } - - # When scalar - return $self->sendError( $req, "Key $key is not a hash", 400 ) - if ($subkey); - return $self->sendError( $req, 'setDefault', 200 ) unless defined($value); - return $self->sendJSONresponse( $req, { value => $value } ); - - # TODO authParam key -} - # 35 - New RSA key pair on demand # -------------------------- diff --git a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Plugin.pm b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Plugin.pm index ee3883987..30967b4a2 100644 --- a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Plugin.pm +++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Plugin.pm @@ -36,7 +36,8 @@ sub _addRoute { } } }; - return $self->p->$type( $word, $subName, $methods, $transform ); + $self->p->$type( $word, $subName, $methods, $transform ); + return $self; } 1;