package Lemonldap::NG::Manager::Conf::Parser; # This module is called either to parse a new configuration in JSON format (as # posted by the web interface) and test a new configuration object. # # The new object must be built with the following properties: # - refConf: the actual configuration # - req : the Lemonldap::NG::Common::PSGI::Request # - tree : the new configuration in JSON format # or # - newConf: the configuration to test # # The main method is check() which calls: # - scanTree() if configuration is not parsed (JSON string) # - testNewConf() # # It returns a boolean. Errors, warnings and changes are stored as array # containing `{ message => 'Explanation' }. A main message is stored in # `message` property. use strict; use utf8; use Crypt::URandom; use Mouse; use JSON 'to_json'; use Lemonldap::NG::Common::Conf::ReConstants; use Lemonldap::NG::Manager::Attributes; our $VERSION = '2.0.12'; extends 'Lemonldap::NG::Common::Conf::Compact'; # High debugging for developers, set this to 1 use constant HIGHDEBUG => 0; # Messages storage has errors => ( is => 'rw', isa => 'ArrayRef', default => sub { return [] } ); has warnings => ( is => 'rw', isa => 'ArrayRef', default => sub { return [] }, trigger => sub { hdebug( 'warnings contains', $_[0]->{warnings} ); } ); has changes => ( is => 'rw', isa => 'ArrayRef', default => sub { return [] } ); has message => ( is => 'rw', isa => 'Str', lazy => 1, default => sub { return join( ', ', map { $_->{message} } @{ $_[0]->errors } ); }, trigger => sub { hdebug( "Message becomes " . $_[0]->{message} ); } ); has needConfirmation => ( is => 'rw', isa => 'ArrayRef', default => sub { return [] } ); # Booleans has confChanged => ( is => 'rw', isa => 'Bool', default => 0, trigger => sub { hdebug( "condChanged: " . $_[0]->{confChanged} ); } ); # Properties required during build has refConf => ( is => 'ro', isa => 'HashRef', required => 1 ); has req => ( is => 'ro', required => 1 ); has newConf => ( is => 'rw', isa => 'HashRef' ); has tree => ( is => 'rw', isa => 'ArrayRef' ); # High debug method sub hdebug { if (HIGHDEBUG) { foreach my $d (@_) { if ( ref $d ) { require Data::Dumper; $Data::Dumper::Useperl = 1; print STDERR Data::Dumper::Dumper($d); } else { print STDERR "$d\n" } } } undef; } ##@method boolean check() # Main method #@return result sub check { my ( $self, $localConf ) = @_; hdebug("# check()"); unless ( $self->newConf ) { return 0 unless ( $self->scanTree ); } unless ( $self->testNewConf($localConf) ) { hdebug(" testNewConf() failed"); return 0; } my $separator = $self->newConf->{multiValuesSeparator} || '; '; hdebug(" tests succeed"); my %conf = %{ $self->newConf }; my %compactedConf = %{ $self->compactConf( $self->newConf ) }; my @removedKeys = (); unless ( $self->confChanged ) { hdebug(" no change detected"); $self->message('__confNotChanged__'); return 0; } # Return removed keys if conf compacted @removedKeys = map { exists $compactedConf{$_} ? () : $_ } sort keys %conf if ( $self->newConf->{compactConf} ); push @{ $self->changes }, ( $self->{newConf}->{compactConf} ? { confCompacted => '1', removedKeys => join( $separator, @removedKeys ) } : { confCompacted => '0' } ); return 1; } ##@method boolean scanTree() # Methods to build new conf from JSON string #@result true if succeed sub scanTree { my $self = shift; hdebug("# scanTree()"); $self->newConf( {} ); $self->_scanNodes( $self->tree ) or return 0; # Set cfgNum to ref cfgNum (will be changed when saving), set other # metadata and set a value to the key if empty $self->newConf->{cfgNum} = $self->req->params('cfgNum'); $self->newConf->{cfgAuthor} = $self->req->userData->{ Lemonldap::NG::Handler::Main->tsv->{whatToTrace} || '_whatToTrace' } // $self->req->env->{REMOTE_USER} // $ENV{REMOTE_USER} // "anonymous"; $self->newConf->{cfgAuthorIP} = $self->req->address; $self->newConf->{cfgAuthorIP} .= ' (maybe ' . ( $self->req->env->{'X-Real-IP'} || $self->req->env->{HTTP_X_FORWARDED_FOR} ) . ')' if $self->req->env->{'X-Real-IP'} or $self->req->env->{HTTP_X_FORWARDED_FOR}; $self->newConf->{cfgDate} = time; $self->newConf->{cfgVersion} = $Lemonldap::NG::Manager::VERSION; $self->newConf->{key} ||= join( '', map { chr( int( ord( Crypt::URandom::urandom(1) ) * 94 / 256 ) + 33 ) } ( 1 .. 16 ) ); return 1; } use feature 'state'; ##@method private boolean _scanNodes() # Recursive JSON parser #@result true if succeed sub _scanNodes { my ( $self, $tree, ) = @_; hdebug("# _scanNodes()"); state( $knownCat, %newNames ); unless ( ref($tree) eq 'ARRAY' ) { print STDERR 'Fatal: node is not an array'; push @{ $self->errors }, { message => 'Fatal: node is not an array' }; return 0; } unless (@$tree) { hdebug(' empty tree !?'); } foreach my $leaf (@$tree) { my $name = $leaf->{title}; hdebug("Looking to $name"); # subnode my $subNodes = $leaf->{nodes} // $leaf->{_nodes}; my $subNodesCond = $leaf->{nodes_cond} // $leaf->{_nodes_cond}; ################################## # VirtualHosts and SAML partners # ################################## # Root nodes if ( $leaf->{id} =~ /^($specialNodeKeys)$/io ) { hdebug("Root special node detected $leaf->{id}"); # If node has not been opened if ( $leaf->{cnodes} ) { hdebug(" not opened"); foreach my $k ( @{ $specialNodeHash->{ $leaf->{id} } } ) { hdebug(" copying $k"); $self->newConf->{$k} = $self->refConf->{$k}; } next; } $self->_scanNodes($subNodes); # Check deleted keys my $field = $specialNodeHash->{ $leaf->{id} }->[0]; my @old = keys %{ $self->refConf->{$field} }; foreach my $k ( keys %{ $self->newConf->{$field} } ) { @old = grep { $_ ne $k } @old; } if (@old) { hdebug( "Keys detected as removed:", \@old ); $self->confChanged(1); foreach my $deletedHost (@old) { push @{ $self->changes }, { key => $leaf->{id}, old => $deletedHost }; } } next; } # 1st sublevel elsif ( $leaf->{id} =~ /^($specialNodeKeys)\/([^\/]+)$/io ) { hdebug("Special node chield detected $leaf->{id}"); my ( $base, $host ) = ( $1, $2 ); # Check hostname/partner name changes (id points to the old name) $newNames{$host} = $leaf->{title}; if ( $newNames{$host} ne $host and $host !~ /^new__/ ) { hdebug(" $host becomes $newNames{$host}"); $self->confChanged(1); push @{ $self->changes }, { key => $base, old => $host, new => $newNames{$host} }; } $self->_scanNodes($subNodes); next; } # Other sub levels elsif ( $leaf->{id} =~ /^($specialNodeKeys)\/([^\/]+)\/([^\/]+)(?:\/(.*))?$/io ) { my ( $base, $key, $oldName, $target, $h ) = ( $1, $newNames{$2}, $2, $3, $4 ); hdebug( "Special node chield subnode detected $leaf->{id}", " base $base, key $key, target $target, h " . ( $h ? $h : 'undef' ) ); # VirtualHosts if ( $base eq 'virtualHosts' ) { hdebug(" virtualhost"); if ( $target =~ /^(?:locationRules|exportedHeaders|post)$/ ) { if ( $leaf->{cnodes} ) { hdebug(' unopened subnode'); $self->newConf->{$target}->{$key} = $self->refConf->{$target}->{$oldName} // {}; } elsif ($h) { hdebug(' 4 levels'); if ( $target eq 'locationRules' ) { hdebug(' locationRules'); my $k = $leaf->{comment} ? "(?#$leaf->{comment})$leaf->{re}" : $leaf->{re}; $k .= "(?#AuthnLevel=$leaf->{level})" if $leaf->{level}; $self->set( $target, $key, $k, $leaf->{data} ); } else { hdebug(' other than locationrules'); $self->set( $target, $key, $leaf->{title}, $leaf->{data} ); } } # Unless $h is set, scan subnodes and check changes else { hdebug(' 3 levels only (missing $h)'); if ( ref $subNodes ) { hdebug(' has subnodes'); $self->_scanNodes($subNodes) or return 0; } if ( exists $self->refConf->{$target}->{$key} and %{ $self->refConf->{$target}->{$key} } ) { hdebug(' old conf subnode has values'); my $c = $self->newConf->{$target}; foreach my $k ( keys %{ $self->refConf->{$target}->{$key} } ) { unless ( defined $c->{$key}->{$k} ) { hdebug(' missing value in old conf'); $self->confChanged(1); push @{ $self->changes }, { key => "$target, $key", old => $k, }; } } } elsif ( exists $self->newConf->{$target}->{$key} and %{ $self->newConf->{$target}->{$key} } ) { hdebug(" '$key' has values"); $self->confChanged(1); push @{ $self->changes }, { key => "$target", new => $key }; } } } elsif ( $target =~ /^$virtualHostKeys$/o ) { $self->set( 'vhostOptions', [ $oldName, $key ], $target, $leaf->{data} ); } else { push @{ $self->errors }, { message => "Unknown vhost key $target" }; return 0; } next; } # SAML elsif ( $base =~ /^saml(?:S|ID)PMetaDataNodes$/ ) { hdebug('SAML'); if ( defined $leaf->{data} and ref( $leaf->{data} ) eq 'ARRAY' ) { hdebug(" SAML data is an array, serializing"); $leaf->{data} = join ';', @{ $leaf->{data} }; } if ( $target =~ /^saml(?:S|ID)PMetaData(?:ExportedAttributes|Macros)$/ ) { if ( $leaf->{cnodes} ) { hdebug(" $target: unopened node"); $self->newConf->{$target}->{$key} = $self->refConf->{$target}->{$oldName} // {}; } elsif ($h) { hdebug(" $target: opened node"); $self->confChanged(1); $self->set( $target, $key, $leaf->{title}, $leaf->{data} ); } else { hdebug(" $target: looking for subnodes"); $self->_scanNodes($subNodes); } } elsif ( $target =~ /^saml(?:S|ID)PMetaDataXML$/ ) { hdebug(" $target"); $self->set( $target, [ $oldName, $key ], $target, $leaf->{data} ); } elsif ( $target =~ /^saml(?:ID|S)PMetaDataOptions/ ) { my $optKey = $&; hdebug(" $base sub key: $target"); if ( $target =~ /^(?:$samlIDPMetaDataNodeKeys|$samlSPMetaDataNodeKeys)/o ) { $self->set( $optKey, [ $oldName, $key ], $target, $leaf->{data} ); } else { push @{ $self->errors }, { message => "Unknown SAML metadata option $target" }; return 0; } } else { push @{ $self->errors }, { message => "Unknown SAML key $target" }; return 0; } next; } # OIDC elsif ( $base =~ /^oidc(?:O|R)PMetaDataNodes$/ ) { hdebug('OIDC'); if ( $target =~ /^oidc(?:O|R)PMetaDataOptions$/ ) { hdebug(" $target: looking for subnodes"); $self->_scanNodes($subNodes); $self->set( $target, $key, $leaf->{title}, $leaf->{data} ); } elsif ( $target =~ /^oidcOPMetaData(?:JSON|JWKS)$/ ) { hdebug(" $target"); $self->set( $target, $key, $leaf->{data} ); } elsif ( $target =~ /^oidcRPMetaDataExportedVars$/ ) { hdebug(" $target"); if ( $leaf->{cnodes} ) { hdebug(' unopened'); $self->newConf->{$target}->{$key} = $self->refConf->{$target}->{$oldName} // {}; } elsif ($h) { hdebug(' opened'); $self->confChanged(1); my $tmp = $leaf->{data}; if ( ref( $leaf->{data} ) eq 'ARRAY' ) { # Forward compatibility. If Type and Array have # default values, store in old format if ( $leaf->{data}->[1] eq "string" and $leaf->{data}->[2] eq "auto" ) { $tmp = $leaf->{data}->[0]; } else { $tmp = join ';', @{ $leaf->{data} }; } } $self->set( $target, $key, $leaf->{title}, $tmp ); } else { hdebug(" $target: looking for subnodes"); $self->_scanNodes($subNodes); } } elsif ( $target =~ /^oidc(?:O|R)PMetaData(?:ExportedVars|Macros|ScopeRules)$/ ) { hdebug(" $target"); if ( $leaf->{cnodes} ) { hdebug(' unopened'); $self->newConf->{$target}->{$key} = $self->refConf->{$target}->{$oldName} // {}; } elsif ($h) { hdebug(' opened'); $self->confChanged(1); $self->set( $target, $key, $leaf->{title}, $leaf->{data} ); } else { hdebug(" $target: looking for subnodes"); $self->_scanNodes($subNodes); } } elsif ( $target =~ /^oidc(?:O|R)PMetaDataOptions/ ) { my $optKey = $&; hdebug " $base sub key: $target"; if ( $target eq 'oidcRPMetaDataOptionsExtraClaims' ) { if ( $leaf->{cnodes} ) { hdebug(' unopened'); $self->newConf->{$target}->{$key} = $self->refConf->{$target}->{$oldName} // {}; } elsif ($h) { hdebug(' opened'); $self->set( $target, $key, $leaf->{title}, $leaf->{data} ); } else { hdebug(" $target: looking for subnodes"); $self->_scanNodes($subNodes); } } elsif ( $target =~ /^(?:$oidcOPMetaDataNodeKeys|$oidcRPMetaDataNodeKeys)/o ) { $self->set( $optKey, [ $oldName, $key ], $target, $leaf->{data} ); } else { push @{ $self->errors }, { message => "Unknown OIDC metadata option $target" }; return 0; } } else { push @{ $self->errors }, { message => "Unknown OIDC key $target" }; return 0; } next; } # CAS elsif ( $base =~ /^cas(?:App|Srv)MetaDataNodes$/ ) { my $optKey = $&; hdebug('CAS'); if ( $target =~ /^cas(?:App|Srv)MetaDataOptions$/ ) { hdebug(" $target: looking for subnodes"); $self->_scanNodes($subNodes); $self->set( $target, $key, $leaf->{title}, $leaf->{data} ); } elsif ( $target =~ /^cas(?:App|Srv)MetaData(?:ExportedVars|Macros)$/ ) { hdebug(" $target"); if ( $leaf->{cnodes} ) { hdebug(' unopened'); $self->newConf->{$target}->{$key} = $self->refConf->{$target}->{$oldName} // {}; } elsif ($h) { hdebug(' opened'); $self->confChanged(1); $self->set( $target, $key, $leaf->{title}, $leaf->{data} ); } else { hdebug(" $target: looking for subnodes"); $self->_scanNodes($subNodes); } } elsif ( $target =~ /^cas(?:Srv|App)MetaDataOptions/ ) { my $optKey = $&; hdebug " $base sub key: $target"; if ( $target eq 'casSrvMetaDataOptionsProxiedServices' ) { if ( $leaf->{cnodes} ) { hdebug(' unopened'); $self->newConf->{$target}->{$key} = $self->refConf->{$target}->{$oldName} // {}; } elsif ($h) { hdebug(' opened'); $self->set( $target, $key, $leaf->{title}, $leaf->{data} ); } else { hdebug(" $target: looking for subnodes"); $self->_scanNodes($subNodes); } } elsif ( $target =~ /^(?:$casSrvMetaDataNodeKeys|$casAppMetaDataNodeKeys)/o ) { $self->set( $optKey, [ $oldName, $key ], $target, $leaf->{data} ); } else { push @{ $self->errors }, { message => "Unknown CAS metadata option $target" }; return 0; } } else { push @{ $self->errors }, { message => "Unknown CAS option $target" }; return 0; } next; } else { push @{ $self->errors }, { message => "Fatal: unknown special sub node $base" }; return 0; } } #################### # Application list # #################### # Application list root node elsif ( $leaf->{title} eq 'applicationList' ) { hdebug( $leaf->{title} ); if ( $leaf->{cnodes} ) { hdebug(' unopened'); $self->newConf->{applicationList} = $self->refConf->{applicationList} // {}; } else { $self->_scanNodes($subNodes) or return 0; # Check for deleted my @listCatRef = map { $self->refConf->{applicationList}->{$_}->{catname} } keys %{ $self->refConf->{applicationList} }; my @listCatNew = map { $self->newConf->{applicationList}->{$_}->{catname} } keys( %{ ref $self->newConf->{applicationList} ? $self->newConf->{applicationList} : {} } ); @listCatRef = map { $_ ? $_ : () } @listCatRef; @listCatNew = map { $_ ? $_ : () } @listCatNew; @listCatRef = sort @listCatRef; @listCatNew = sort @listCatNew; hdebug( '# @listCatRef : ', \@listCatRef ); hdebug( '# @listCatNew : ', \@listCatNew ); # Check for deleted my @diff = grep !${ { map { $_, 1 } @listCatNew } }{$_}, @listCatRef; if ( scalar @diff ) { $self->confChanged(1); push @{ $self->changes }, { new => join( ', ', 'categoryList', @listCatNew ), key => join( ', ', 'Deletes in cat(s)', @diff ), old => join( ', ', 'categoryList', @listCatRef ), }; } } next; } # Application list sub nodes elsif ( $leaf->{id} =~ /^applicationList\/(.+)$/ ) { hdebug('Application list subnode'); use feature 'state'; my @cats = split /\//, $1; my $app = pop @cats; $self->newConf->{applicationList} //= {}; # $cn is a pointer to the parent my $cn = $self->newConf->{applicationList}; my $cmp = $self->refConf->{applicationList}; my @path; # Makes $cn point to the parent foreach my $cat (@cats) { hdebug(" looking to cat $cat"); unless ( defined $knownCat->{$cat} ) { push @{ $self->{errors} }, { message => "Fatal: sub cat/app before parent ($leaf->{id})" }; return 0; } $cn = $cn->{ $knownCat->{$cat} }; push @path, $cn->{catname}; $cmp->{$cat} //= {}; $cmp = $cmp->{$cat}; } my $newapp = $app; # Compute a nice name for new nodes, taking care of potential conflicts # For some reason, the manager sends /nNaN sometimes if ( $newapp =~ /^n(\d+|NaN)$/ ) { # Remove all special characters my $baseName = $leaf->{title} =~ s/\W//gr; $baseName = lc $baseName; $newapp = $baseName; my $cnt = 1; while ( exists $cn->{$newapp} ) { $newapp = "${baseName}_" . $cnt++; } } # Create new category # # Note that this works because nodes are ordered so "cat/cat2/app" # is looked after "cat" and "cat/cat2" if ( $leaf->{type} eq 'menuCat' ) { hdebug(' menu cat'); $knownCat->{__id}++; $knownCat->{$app} = $newapp; $cn->{$newapp} = { catname => $leaf->{title}, type => 'category', order => $knownCat->{__id} }; unless ($cmp->{$app} and $cmp->{$app}->{catname} eq $cn->{$newapp}->{catname} ) { $self->confChanged(1); push @{ $self->changes }, { key => join( ', ', 'applicationList', @path, $leaf->{title} ), new => $cn->{$newapp}->{catname}, old => ( $cn->{$newapp} ? $cn->{$newapp}->{catname} : undef ) }; } if ( ref $subNodes ) { $self->_scanNodes($subNodes) or return 0; } my @listCatRef = keys %{ $cmp->{$app} }; my @listCatNew = keys %{ $cn->{$newapp} }; # Check for deleted unless ( @listCatRef == @listCatNew ) { $self->confChanged(1); push @{ $self->changes }, { key => join( ', ', 'applicationList', @path ), new => 'Changes in cat(s)/app(s)', }; } } # Create new apps if ( $leaf->{type} eq 'menuApp' ) { hdebug(' new app'); $knownCat->{__id}++; $cn->{$newapp} = { type => 'application', options => $leaf->{data}, order => $knownCat->{__id} }; $cn->{$newapp}->{options}->{name} = $leaf->{title}; unless ( $cmp->{$app} ) { $self->confChanged(1); push @{ $self->changes }, { key => join( ', ', 'applicationList', @path ), new => $leaf->{title}, }; } else { # Check for change in ordering if ( ( $cn->{$newapp}->{order} || 0 ) != ( $cmp->{$newapp}->{order} || 0 ) ) { $self->confChanged(1); } # Check for change in options foreach my $k ( keys %{ $cn->{$newapp}->{options} } ) { unless ( $cmp->{$app}->{options}->{$k} eq $cn->{$newapp}->{options}->{$k} ) { $self->confChanged(1); push @{ $self->changes }, { key => join( ', ', 'applicationList', @path, $leaf->{title}, $k ), new => $cn->{$newapp}->{options}->{$k}, old => $cmp->{$app}->{options}->{$k} }; } } } } next; } elsif ( $leaf->{id} eq 'grantSessionRules' ) { hdebug('grantSessionRules'); if ( $leaf->{cnodes} ) { hdebug(' unopened'); $self->newConf->{$name} = $self->refConf->{$name} // {}; } else { hdebug(' opened'); $subNodes //= []; my $count = 0; my $ref = $self->refConf->{grantSessionRules}; my $new = $self->newConf->{grantSessionRules}; my @old = ref $ref ? keys %$ref : (); $self->newConf->{grantSessionRules} = {}; foreach my $n (@$subNodes) { hdebug(" looking at $n subnode"); my $k = $n->{re} . ( $n->{comment} ? "##$n->{comment}" : '' ); $self->newConf->{grantSessionRules}->{$k} = $n->{data}; $count++; unless ( defined $ref->{$k} ) { $self->confChanged(1); push @{ $self->changes }, { keys => 'grantSessionRules', new => $k }; } elsif ( $ref->{$k} ne $n->{data} ) { $self->confChanged(1); push @{ $self->changes }, { key => "grantSessionRules, $k", old => $self->refConf->{grantSessionRules}->{$k}, new => $n->{data} }; } @old = grep { $_ ne $k } @old; } if (@old) { $self->confChanged(1); push @{ $self->changes }, { key => 'grantSessionRules', old => $_, } foreach (@old); } } next; } # openIdIDPList: data is splitted by Conf.pm into a boolean and a # string elsif ( $name eq 'openIdIDPList' ) { hdebug('openIdIDPList'); if ( $leaf->{data} ) { unless ( ref $leaf->{data} eq 'ARRAY' ) { push @{ $self->{errors} }, { message => 'Malformed openIdIDPList ' . $leaf->{data} }; return 0; } $self->set( $name, join( ';', @{ $leaf->{data} } ) ); } else { $self->set( $name, undef ); } next; } #################### # Other hash nodes # #################### elsif ( $leaf->{title} =~ /^$simpleHashKeys$/o and not $leaf->{title} eq 'applicationList' ) { hdebug( $leaf->{title} ); # If a `cnodes` key is found, keep old key unchanges if ( $leaf->{cnodes} ) { hdebug(' unopened'); $self->newConf->{$name} = $self->refConf->{$name} // {}; } else { hdebug(' opened'); # combModules: just to replace "over" key if ( $name eq 'combModules' ) { hdebug(' combModules'); $self->newConf->{$name} = {}; foreach my $node ( @{ $leaf->{nodes} } ) { my $tmp; $tmp->{$_} = $node->{data}->{$_} foreach (qw(type for)); $tmp->{over} = {}; foreach ( @{ $node->{data}->{over} } ) { $tmp->{over}->{ $_->[0] } = $_->[1]; } $self->newConf->{$name}->{ $node->{title} } = $tmp; } # TODO: check changes $self->confChanged(1); next; } # sfExtra: just to replace "over" key if ( $name eq 'sfExtra' ) { hdebug(' sfExtra'); $self->newConf->{$name} = {}; foreach my $node ( @{ $leaf->{nodes} } ) { my $tmp; $tmp->{$_} = $node->{data}->{$_} foreach (qw(type rule logo level label)); $tmp->{over} = {}; foreach ( @{ $node->{data}->{over} } ) { $tmp->{over}->{ $_->[0] } = $_->[1]; } $self->newConf->{$name}->{ $node->{title} } = $tmp; } # TODO: check changes $self->confChanged(1); next; } $subNodes //= []; my $count = 0; my @old = ( ref( $self->refConf->{$name} ) ? ( keys %{ $self->refConf->{$name} } ) : () ); $self->newConf->{$name} = {}; foreach my $n (@$subNodes) { hdebug(" looking at $n subnode"); if ( ref $n->{data} and ref $n->{data} eq 'ARRAY' ) { # authChoiceModules if ( $name eq 'authChoiceModules' ) { hdebug(' authChoiceModules'); if ( ref( $n->{data}->[5] ) eq 'ARRAY' ) { $n->{data}->[5] = to_json( { map { @$_ } @{ $n->{data}->[5] } } ); } else { $n->{data}->[5] = '{}'; } } $n->{data} = join ';', @{ $n->{data} }; } $self->newConf->{$name}->{ $n->{title} } = $n->{data}; $count++; unless ( defined $self->refConf->{$name}->{ $n->{title} } ) { $self->confChanged(1); push @{ $self->changes }, { key => $name, new => $n->{title}, }; } elsif ( $self->refConf->{$name}->{ $n->{title} } ne $n->{data} ) { $self->confChanged(1); push @{ $self->changes }, { key => "$name, $n->{title}", old => $self->refConf->{$name}->{ $n->{title} }, new => $n->{data} }; } @old = grep { $_ ne $n->{title} } @old; } if (@old) { $self->confChanged(1); push @{ $self->changes }, { key => $name, old => $_, } foreach (@old); } } next; } # Double hash nodes elsif ( $leaf->{title} =~ /^$doubleHashKeys$/ ) { hdebug( $leaf->{title} ); my @oldHosts = ( ref( $self->refConf->{$name} ) ? ( keys %{ $self->refConf->{$name} } ) : () ); $self->newConf->{$name} = {}; unless ( defined $leaf->{data} ) { hdebug(' unopened'); $self->newConf->{$name} = $self->refConf->{$name} || {}; next; } foreach my $getHost ( @{ $leaf->{data} } ) { my $change = 0; my @oldKeys; my $host = $getHost->{k}; hdebug(" looking at host: $host"); $self->newConf->{$name}->{$host} = {}; unless ( defined $self->refConf->{$name}->{$host} ) { $self->confChanged(1); $change++; push @{ $self->changes }, { key => $name, new => $host }; hdebug(" $host is new"); } else { @oldHosts = grep { $_ ne $host } @oldHosts; @oldKeys = keys %{ $self->refConf->{$name}->{$host} }; } foreach my $prm ( @{ $getHost->{h} } ) { $self->newConf->{$name}->{$host}->{ $prm->{k} } = $prm->{v}; if ( !$change and ( not defined( $self->refConf->{$name}->{$host}->{ $prm->{k} } ) or $self->newConf->{$name}->{$host}->{ $prm->{k} } ne $self->refConf->{$name}->{$host}->{ $prm->{k} } ) ) { $self->confChanged(1); hdebug(" key $prm->{k} has been changed"); push @{ $self->changes }, { key => "$name/$host", new => $prm->{k} }; } elsif ( !$change ) { @oldKeys = grep { $_ ne $prm->{k} } @oldKeys; } } if (@oldKeys) { $self->confChanged(1); hdebug( " old keys: " . join( ' ', @oldKeys ) ); push @{ $self->changes }, { key => "$name/$host", old => $_ } foreach (@oldKeys); } } if (@oldHosts) { $self->confChanged(1); hdebug( " old hosts " . join( ' ', @oldHosts ) ); push @{ $self->changes }, { key => "$name", old => $_ } foreach (@oldHosts); } next; } ############### # Other nodes # ############### # Check if subnodes my $n = 0; if ( ref $subNodesCond ) { hdebug(' conditional subnodes detected'); # Bad idea,subnode unopened are not read #$subNodesCond = [ grep { $_->{show} } @$subNodesCond ]; $self->_scanNodes($subNodesCond) or return 0; $n++; } if ( ref $subNodes ) { hdebug(' subnodes detected'); $self->_scanNodes($subNodes) or return 0; $n++; } if ($n) { next; } if ( defined $leaf->{data} and ref( $leaf->{data} ) eq 'ARRAY' ) { if ( ref( $leaf->{data}->[0] ) eq 'HASH' ) { hdebug(" array found"); $self->_scanNodes( $leaf->{data} ) or return 0; } else { $self->set( $name, join( ';', @{ $leaf->{data} } ) ); } } # Grouped nodes not opened elsif ( $leaf->{get} and ref $leaf->{get} eq 'ARRAY' ) { hdebug(" unopened grouped node"); foreach my $subkey ( @{ $leaf->{get} } ) { $self->set( $subkey, undef ); } } # Normal leaf else { $self->set( $name, $leaf->{data} ); } } return 1; } ##@method private void set($target, @path, $data) # Store a value in the $target key (following subkeys if @path is set) sub set { my $self = shift; my $data = pop; my @confs = ( $self->refConf, $self->newConf ); my @path; while ( @_ > 1 ) { my $tmp = shift; push @path, $tmp; foreach my $i ( 0, 1 ) { my $v = ref($tmp) ? $tmp->[$i] : $tmp; $confs[$i]->{$v} //= {}; $confs[$i] = $confs[$i]->{$v}; } } my $target = shift; hdebug( "# set() called:", { data => $data, path => \@path, target => $target } ); die @path unless ($target); # Check new value if ( defined $data ) { hdebug(" data defined"); # TODO: remove if $data == default value $confs[1]->{$target} = $data; eval { unless ( $target eq 'cfgLog' or ( defined $confs[0]->{$target} and $confs[0]->{$target} eq $data ) or ( !defined $confs[0]->{$target} and defined $self->defaultValue($target) and $data eq $self->defaultValue($target) ) ) { $self->confChanged(1); push @{ $self->changes }, { key => join( ', ', @path, $target ), old => $confs[0]->{$target} // $self->defaultValue($target), new => $confs[1]->{$target} }; } }; } # Set old value if exists else { hdebug(" data undefined"); if ( exists $confs[0]->{$target} ) { hdebug(" old value exists"); $confs[1]->{$target} = $confs[0]->{$target}; } else { hdebug(" no old value, skipping"); } } } sub defaultValue { my ( $self, $target ) = @_; hdebug("# defaultValue($target)"); die unless ($target); my $res = eval { &Lemonldap::NG::Manager::Attributes::attributes()->{$target} ->{'default'}; }; return $res; } ##@method boolean testNewConf() # Launch _unitTest() and _globaTest() # #@return true if tests succeed sub testNewConf { my ( $self, $localConf ) = @_; hdebug('# testNewConf()'); return $self->_unitTest( $self->newConf(), $localConf ) && $self->_globalTest($localConf); } ##@method private boolean _unitTest() # Launch unit tests declared in Lemonldap::NG::Manager::Build::Attributes file # #@return true if tests succeed sub _unitTest { my ( $self, $conf, $localConf ) = @_; hdebug('# _unitTest()'); my $types = &Lemonldap::NG::Manager::Attributes::types(); my $attrs = &Lemonldap::NG::Manager::Attributes::attributes(); my $res = 1; foreach my $key ( keys %$conf ) { if ( $localConf and $localConf->{skippedUnitTests} and $localConf->{skippedUnitTests} =~ /\b$key\b/ ) { $localConf->logger->debug("-> Ignore test for $key\n"); next; } hdebug("Testing $key"); my $attr = $attrs->{$key}; my $type = $types->{ $attr->{type} } if $attr; unless ( $type or $attr->{test} ) { $localConf->logger->debug("Unknown attribute $key, deleting it\n") if $localConf; delete $conf->{$key}; next; } # Vhost, CAS, SAML, OIDC options if ( $key =~ /^(?^:(?:(?:(?:saml(?:ID|S)|oidc[OR])P|cas(?:App|Srv))MetaData|vhost)Options)$/ ) { # Iterate on vhost names, or saml/cas/oidc configuration keys for my $vhost ( keys %{ $conf->{$key} } ) { my $options = $conf->{$key}->{$vhost}; if ( ref($options) eq "HASH" ) { # Recurse on option list, # FIXME this does check for oidcRPMetaDataOptionsXXX # appearing under samlSPMetadataOptions $res = 0 unless $self->_unitTest( $options, $localConf, "$key/$vhost/" ); } } } else { # Check if key exists unless ($attr) { push @{ $self->errors }, { message => "__unknownKey__: $key" }; $res = 0; next; } # Hash parameters if ( $key =~ /^$simpleHashKeys$/o ) { $conf->{$key} //= {}; unless ( ref $conf->{$key} eq 'HASH' ) { push @{ $self->errors }, { message => "$key is not a hash ref" }; $res = 0; next; } } elsif ( $attr->{type} =~ /Container$/ ) { #TODO } if ( $key =~ /^(?:$simpleHashKeys|$doubleHashKeys)$/o or $attr->{type} =~ /Container$/ ) { $res = 0 unless ( $self->_execTest( { keyTest => $attr->{keyTest} // $type->{keyTest}, keyMsgFail => $attr->{keyMsgFail} // $type->{keyMsgFail}, test => $attr->{test} // $type->{test}, msgFail => $attr->{msgFail} // $type->{msgFail}, }, $conf->{$key}, $key, $attr, undef, $conf ) ); } elsif ( defined $attr->{keyTest} ) { #TODO } else { my $msg = $attr->{msgFail} // $type->{msgFail}; $res = 0 unless ( $self->_execTest( $attr->{test} // $type->{test}, $conf->{$key}, $key, $attr, $msg, $conf ) ); } } } return $res; } ##@method private boolean _execTest($test, $value) # Execute the given test with value #@param test that can be a code-ref, or a regexp #@return result of test sub _execTest { my ( $self, $test, $value, $key, $attr, $msg, $conf ) = @_; my $ref; die "Malformed test for $key: only regexp ref or sub are accepted (type \"$ref\")" unless ( $ref = ref($test) and $ref =~ /^(CODE|Regexp|HASH)$/ ); if ( $ref eq 'CODE' ) { my ( $r, $m ) = ( $test->( $value, $conf, $attr ) ); if ($m) { push @{ $self->{ ( $r > 0 ? 'warnings' : ( $r < 0 ? 'needConfirmation' : 'errors' ) ) } }, { message => "$key: $m" }; } elsif ( !$r ) { push @{ $self->{errors} }, { message => "$key: $msg" }; } return $r; } elsif ( $ref eq 'Regexp' ) { my $r = $value =~ $test; push @{ $self->errors }, { message => "$key: $msg" } unless ($r); return $r; } # Recursive test (for locationRules,...) else { my $res = 1; return $res unless ( ref($value) eq 'HASH' ); foreach my $k ( keys %$value ) { $res = 0 unless ( $self->_execTest( $test->{keyTest}, $k, "$key/$k", $attr, $test->{keyMsgFail}, $conf ) and $self->_execTest( $test->{test}, $value->{$k}, "$key/$k", $attr, $test->{msgFail}, $conf ) ); } return $res; } } ##@method private boolean _globalTest() # Launch all tests declared in Lemonldap::NG::Manager::Conf::Tests::tests() # #@return true if tests succeed sub _globalTest { my ( $self, $localConf ) = @_; require Lemonldap::NG::Manager::Conf::Tests; hdebug('# _globalTest()'); my $result = 1; my $tests = &Lemonldap::NG::Manager::Conf::Tests::tests( $self->newConf ); foreach my $name ( keys %$tests ) { if ( $localConf and $localConf->{skippedGlobalTests} and $localConf->{skippedGlobalTests} =~ /\b$name\b/ ) { $localConf->logger->debug("-> Ignore test for $name\n") if $localConf; next; } my $sub = $tests->{$name}; my ( $res, $msg ); eval { ( $res, $msg ) = $sub->(); if ( $res == -1 ) { push @{ $self->needConfirmation }, { message => $msg }; } elsif ($res) { if ($msg) { push @{ $self->warnings }, { message => $msg }; } } else { $result = 0; push @{ $self->errors }, { message => $msg }; } }; if ($@) { push @{ $self->warnings }, "Test $name failed: $@"; $localConf->logger->debug("Test $name failed: $@\n") if $localConf; } } return $result; } 1; __END__ =head1 NAME =encoding utf8 Lemonldap::NG::Manager::Conf::Parser - Perl extension for parsing new uploaded configurations. =head1 SYNOPSIS require Lemonldap::NG::Manager::Conf::Parser; my $parser = Lemonldap::NG::Manager::Conf::Parser->new( { tree => $new, refConf => $self->currentConf } ); my $res = { result => $parser->check }; $res->{message} = $parser->{message}; foreach my $t (qw(errors warnings changes)) { push @{ $res->{details} }, { message => $t, items => $parser->$t } if ( @{$parser->$t} ); } =head1 DESCRIPTION Lemonldap::NG::Manager::Conf::Parser checks new configuration This package is used by Manager to examine uploaded configuration. It is currently called using check() which return a boolean. check() looks if a newConf is available. If not, it builds it from uploaded JSON (using scanTree() subroutine) Messages are stored in errors(), warnings() and changes() as arrays. This interface uses L to be compatible with CGI, FastCGI,... =head1 SEE ALSO L, L =head1 AUTHORS =over =item LemonLDAP::NG team L =back =head1 BUG REPORT Use OW2 system to report bug or ask for features: L If you want to report a bug concerning configuration upload, please change HIGHDEBUG constant value to produce more logs. Then post them in the Gitlab ticket. =head1 DOWNLOAD Lemonldap::NG is available at L =head1 COPYRIGHT AND LICENSE See COPYING file for details. 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