# This module implements all the methods that responds to '/confs/*' requests # It contains 4 sections: # - initialization methods # - private methods (to access required conf) # - display methods # - upload method package Lemonldap::NG::Manager::Conf; use 5.10.0; 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 feature 'state'; extends 'Lemonldap::NG::Manager::Lib'; ############################# # I. INITIALIZATION METHODS # ############################# use constant defaultRoute => 'manager.html'; sub addRoutes { my $self = shift; # HTML template $self->addRoute( 'manager.html', undef, ['GET'] ) # READ # Special keys ->addRoute( confs => { ':cfgNum' => [ qw(virtualHosts samlIDPMetaDataNodes samlSPMetaDataNodes applicationList oidcOPMetaDataNodes oidcRPMetaDataNodes authChoiceModules grantSessionRules) ] }, ['GET'] ) # Other keys ->addRoute( confs => { ':cfgNum' => { '*' => 'getKey' } }, ['GET'] ) # New key and conf save ->addRoute( confs => { newRSAKey => 'newRSAKey', raw => 'newRawConf', '*' => 'newConf' }, ['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 ) = splice @_; state $confAcc ||= $self->confAcc; $self->lmLog( "Search for $key in conf", 'debug' ); # Verify that cfgNum has been asked unless ( $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' ) { $req->params( 'cfgNum', $self->confAcc->lastCfg ); } unless ( $req->params('cfgNum') =~ /^\d+$/ ) { $req->error("cfgNum must be a number"); return undef; } unless ( $self->getConfByNum( $req->params('cfgNum') ) ) { $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 ) = splice @_; unless ( $self->currentConf and $cfgNum == $self->currentConf->{cfgNum} ) { my $tmp = $self->confAcc->getConf( { cfgNum => $cfgNum, raw => 1 } ); return undef unless ( $tmp and ref($tmp) and %$tmp ); $self->currentConf($tmp); } return $cfgNum; } ######################## # III. Display methods # ######################## # Special subnodes ## @method PSGI-JSON-response recursiveCnodes($req, $query, $tpl) # Respond to root requests for virtual hosts and SAMLmetadatas # #@param $req Lemonldap::NG::PSGI::Request #@param $query Configuration root key #@param $tpl Javascript template to use (see JS/JSON generator script) #@return PSGI JSON response sub recursiveCnodes { my ( $self, $req, $query, $tpl ) = splice @_; $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 ); } ## @method PSGI-JSON-response virtualHosts($req, @path) # Respond to virtualhosts sub requests # #@param $req Lemonldap::NG::PSGI::Request #@param @path words in path after `virtualhosts` #@return PSGI JSON response sub virtualHosts { my ( $self, $req, @path ) = splice @_; return $self->recursiveCnodes( $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} = [ split( /\|/, $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 ); } } ## @method PSGI-JSON-response _samlMetaDataNode($type, $req, @path) # Respond to SAML metadata subnodes # #@param $type `SP` or `IDP` #@param $req Lemonldap::NG::PSGI::Request #@param @path words in path after `saml{IDP|SP}MetaDataNode` #@return PSGI JSON response sub _samlMetaDataNodes { my ( $self, $type, $req, @path ) = splice @_; return $self->recursiveCnodes( $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}MetaDataNode/$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::PSGI::Request #@param @path words in path after `samlIDPMetaDataNode` #@return PSGI JSON response sub samlIDPMetaDataNodes { my ( $self, $req, @path ) = splice @_; return $self->_samlMetaDataNodes( 'IDP', $req, @path ); } ## @method PSGI-JSON-response samlSPMetaDataNode($req, @path) # Launch _samlMetaDataNode('SP', @_) # #@param $req Lemonldap::NG::PSGI::Request #@param @path words in path after `samlSPMetaDataNode` #@return PSGI JSON response sub samlSPMetaDataNodes { my ( $self, $req, @path ) = splice @_; return $self->_samlMetaDataNodes( 'SP', $req, @path ); } ## @method PSGI-JSON-response _oidcMetaDataNodes($type, $req, @path) # Respond to OpenID-Connect metadata subnodes # #@param $type `OP` or `RP` #@param $req Lemonldap::NG::PSGI::Request #@param @path words in path after `oidc{OP|RP}MetaDataNode` #@return PSGI JSON response sub _oidcMetaDataNodes { my ( $self, $type, $req, @path ) = splice @_; my $refKey = ( $type eq 'RP' ? 'oidcRPMetaDataOptions' : 'oidcOPMetaDataJSON' ); return $self->recursiveCnodes( $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$/ ) { 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}MetaDataNode/$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::PSGI::Request #@param @path words in path after `oidcOPMetaDataNode` #@return PSGI JSON response sub oidcOPMetaDataNodes { my ( $self, $req, @path ) = splice @_; return $self->_oidcMetaDataNodes( 'OP', $req, @path ); } ## @method PSGI-JSON-response oidcRPMetaDataNodes($req, @path) # Launch _oidcMetaDataNodes('SP', @_) # #@param $req Lemonldap::NG::PSGI::Request #@param @path words in path after `oidcRPMetaDataNode` #@return PSGI JSON response sub oidcRPMetaDataNodes { my ( $self, $req, @path ) = splice @_; return $self->_oidcMetaDataNodes( 'RP', $req, @path ); } ## @method PSGI-JSON-response authChoiceModules($req,$key) # Returns authChoiceModules keys splitted in arrays # #@param $req Lemonldap::NG::PSGI::Request #@param key optional subkey #@return PSGI JSON response sub authChoiceModules { my ( $self, $req, $key ) = splice @_; my $value = $self->getConfKey( $req, 'authChoiceModules' ); unless ($key) { my @res; foreach my $k ( sort keys %$value ) { push @res, { id => "authChoiceModules/$k", title => "$k", data => $value->{$k}, type => 'authChoice' }; } return $self->sendJSONresponse( $req, \@res ); } else { my $r = $value->{$key} ? [ split( /[;\|]/, $value->{$key} ) ] : []; return $self->sendJSONresponse( $req, { value => $r } ); } } ## @method PSGI-JSON-response $grantSessionRules($req) #Respond to `/conf/:cfgNum` requests by sending configuration metadatas # #@param $req Lemonldap::NG::PSGI::Request #@return PSGI JSON response sub grantSessionRules { my ( $self, $req, $key ) = splice @_; 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 ); } ## @method PSGI-JSON-response metadatas($req) #Respond to `/conf/:cfgNum` requests by sending configuration metadatas # #@param $req Lemonldap::NG::PSGI::Request #@return PSGI JSON response sub metadatas { my ( $self, $req ) = splice @_; 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( $req, 'User ' . $self->userId($req) . ' ask for full configuration ' . $c->{cfgNum} ); return $self->sendJSONresponse( $req, $self->currentConf, forceJSON => 1 ); } 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 < $#a ) { $res->{next} = $a[ $ind + 1 ]; } $self->userNotice( $req, 'User ' . $self->userId($req) . ' ask for configuration metadatas (' . $res->{cfgNum} . ')' ); return $self->sendJSONresponse( $req, $res ); } } ## @method PSGI-JSON-response applicationList($req, @other) # Return the full menu tree # #@param $req Lemonldap::NG::PSGI::Request #@param @other words in path after `applicationList` #@return PSGI JSON response sub applicationList { my ( $self, $req, @other ) = splice @_; 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 ) = splice @_; 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; } ## @method scalar getKey($req, $key, $subkey) # Return the value of a root key of current configuration # #@param $req Lemonldap::NG::PSGI::Request #@param $key Name of key requested #@param $subkey Subkey for hash values #@return Configuration value (as scalar) sub getKey { my ( $self, $req, $key, $subkey ) = splice @_; unless ($key) { return $self->metadatas($req); } $self->userInfo( $req, '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 ) { $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 ); } # When scalar return $self->sendError( $req, "Key $key is not a hash", 400 ) if ($subkey); return $self->sendJSONresponse( $req, { value => $value } ); # TODO authParam key } ###################### # IV. Upload methods # ###################### ## @method PSGI-JSON-response newConf($req) # Call Lemonldap::NG::Manager::ConfParser to parse new configuration and store # it # #@param $req Lemonldap::NG::PSGI::Request #@return PSGI JSON response sub newConf { my ( $self, $req, @other ) = splice @_; return $self->sendError( $req, 'There is no subkey for "newConf"', 400 ) if (@other); # Body must be json my $new = $req->jsonBodyToObj; unless ( defined($new) ) { return $self->sendError( $req, undef, 400 ); } # Verify that cfgNum has been asked unless ( $req->params('cfgNum') ) { return $self->sendError( $req, "Missing configuration number", 400 ); } # Set current conf to cfgNum unless ( $self->getConfByNum( $req->params('cfgNum') ) ) { return $self->sendError( $req, "Configuration " . $req->params('cfgNum') . " not available " . $Lemonldap::NG::Common::Conf::msg, 400 ); } # Parse new conf require Lemonldap::NG::Manager::ConfParser; my $parser = Lemonldap::NG::Manager::ConfParser->new( { tree => $new, refConf => $self->currentConf, req => $req } ); my $res = { result => $parser->check }; # "message" fields: note that words enclosed by "__" (__word__) will be # translated $res->{message} = $parser->{message}; foreach my $t (qw(errors warnings changes)) { $res->{details}->{ '__' . $t . '__' } = $parser->$t if ( @{ $parser->$t } ); } if ( $res->{result} ) { if ( $self->{demoMode} ) { $res->{message} = '__demoModeOn__'; } else { my %args; $args{force} = 1 if ( $req->params('force') ); my $s = $self->confAcc->saveConf( $parser->newConf, %args ); if ( $s > 0 ) { $self->userNotice( $req, 'User ' . $self->userId($req) . " has stored conf $s" ); $res->{result} = 1; $res->{cfgNum} = $s; if ( my $status = $self->applyConf( $parser->newConf ) ) { push @{ $res->{details}->{__applyResult__} }, { message => "$_: $status->{$_}" } foreach ( keys %$status ); } } else { $self->userNotice( $req, 'Saving attempt rejected, asking for confirmation to ' . $self->userId($req) ); $res->{result} = 0; if ( $s == CONFIG_WAS_CHANGED ) { $res->{needConfirm} = 1; $res->{message} .= '__needConfirmation__'; } } } } return $self->sendJSONresponse( $req, $res ); } ## @method PSGI-JSON-response newRawConf($req) # Store directly raw configuration # #@param $req Lemonldap::NG::PSGI::Request #@return PSGI JSON response sub newRawConf { my ( $self, $req, @other ) = splice @_; return $self->sendError( $req, 'There is no subkey for "newConf"', 400 ) if (@other); # Body must be json my $new = $req->jsonBodyToObj; unless ( defined($new) ) { return $self->sendError( $req, undef, 400 ); } my $res = {}; if ( $self->{demoMode} ) { $res->{message} = '__demoModeOn__'; } else { # When uploading a new conf, always force it since cfgNum has a few # chances to be equal to last config cfgNum my $s = $self->confAcc->saveConf( $new, force => 1 ); if ( $s > 0 ) { $self->userNotice( $req, 'User ' . $self->userId($req) . " has stored (raw) conf $s" ); $res->{result} = 1; $res->{cfgNum} = $s; } else { $self->userNotice( $req, 'Raw saving attempt rejected, asking for confirmation to ' . $self->userId($req) ); $res->{result} = 0; $res->{needConfirm} = 1 if ( $s == CONFIG_WAS_CHANGED ); $res->{message} .= '__needConfirmation__'; } } return $self->sendJSONresponse( $req, $res ); } ##@method public hashref generateKeys(string password) # Return a hashref containing private and public keys # @param $password A password to protect the private key # @return Hashref sub newRSAKey { my ( $self, $req, @others ) = splice @_; return $self->sendError( $req, 'There is no subkey for "newRSAKey"', 400 ) if (@others); my $query = $req->jsonBodyToObj; my $rsa = Crypt::OpenSSL::RSA->generate_key(2048); my $keys = { 'private' => $rsa->get_private_key_string(), 'public' => $rsa->get_public_key_x509_string(), }; if ( $query->{password} ) { my $pem = Convert::PEM->new( Name => 'RSA PRIVATE KEY', ASN => q( RSAPrivateKey SEQUENCE { version INTEGER, n INTEGER, e INTEGER, d INTEGER, p INTEGER, q INTEGER, dp INTEGER, dq INTEGER, iqmp INTEGER } ) ); $keys->{private} = $pem->encode( Content => $pem->decode( Content => $keys->{private} ), Password => $query->{password}, ); } return $self->sendJSONresponse( $req, $keys ); } ## @method private applyConf() # Try to apply configuration by reloading Handlers # @return reload status sub applyConf { my ( $self, $newConf ) = splice @_; my $status; # Get apply section values my %reloadUrls = %{ $self->confAcc->getLocalConf( APPLYSECTION, undef, 0 ) }; if ( !%reloadUrls && $newConf->{reloadUrls} ) { %reloadUrls = %{ $newConf->{reloadUrls} }; } return {} unless (%reloadUrls); # Create user agent require LWP::UserAgent; my $ua = new LWP::UserAgent( requests_redirectable => [] ); $ua->timeout(10); # Parse apply values while ( my ( $host, $request ) = each %reloadUrls ) { my ( $method, $vhost, $uri ) = ( $request =~ /^(https?):\/\/([^\/]+)(.*)$/ ); unless ($vhost) { $vhost = $host; $uri = $request; } my $r = HTTP::Request->new( 'GET', "$method://$host$uri", HTTP::Headers->new( Host => $vhost ) ); my $response = $ua->request($r); if ( $response->code != 200 ) { $status->{$host} = "Error " . $response->code . " (" . $response->message . ")"; $self->userError( "Apply configuration for $host: error " . $response->code . " (" . $response->message . ")" ); } else { $status->{$host} = "OK"; $self->userNotice("Apply configuration for $host: ok"); } } return $status; } 1; __END__ =head1 NAME =encoding utf8 Lemonldap::NG::Manager::Conf - Configuration management part of L. =head1 SYNOPSIS See L. =head1 DESCRIPTION Lemonldap::NG::Manager provides a web interface to manage Lemonldap::NG Web-SSO system. The Perl part of Lemonldap::NG::Manager is the REST server. Web interface is written in Javascript, using AngularJS framework and can be found in `site` directory. The REST API is described in REST-API.md file given in source tree. Lemonldap::NG Manager::Conf provides the configuration management part. =head1 ORGANIZATION Lemonldap::NG::Manager configuration is managed by 2 files: =over =item This file to display configuration metadatas and keys content, and to save new configuration, =item L used to check proposed configuration. =back =head1 OPERATION The first Ajax request given by the manager web interface is generaly `/confs/latest`, Lemonldap::NG::Manager::Conf returns the configuration metadatas (author, data, log,...). Then for each key read by the user, web interface launch an Ajax request to get the value. At the end, when modifications are saved, a POST request is done to `/confs`. Then Lemonldap::NG::Manager::Conf calls L to verify new configuration. If good, it tries to store it. Then it calls applyConf() that tries to call other servers to explain them that configuration has changed. Then it returns all errors, warnings in a JSON object that is displayed by web interface. =head1 SEE ALSO L, L, L =head1 AUTHORS =over =item Clement Oudot, Eclem.oudot@gmail.comE =item François-Xavier Deltombe, Efxdeltombe@gmail.com.E =item Xavier Guimard, Ex.guimard@free.frE =item Thomas Chemineau, Ethomas.chemineau@gmail.comE =back =head1 BUG REPORT Use OW2 system to report bug or ask for features: L =head1 DOWNLOAD Lemonldap::NG is available at L =head1 COPYRIGHT AND LICENSE =over =item Copyright (C) 2015 by Xavier Guimard, Ex.guimard@free.frE =item Copyright (C) 2015 by Clément Oudot, Eclem.oudot@gmail.comE =back This library is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see L. =cut