Parser.pm 45.5 KB
Newer Older
1
package Lemonldap::NG::Manager::Conf::Parser;
2

3 4 5
# 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.
#
Christophe Maudoux's avatar
Christophe Maudoux committed
6
# The new object must be built with the following properties:
7 8 9 10 11 12 13 14 15 16 17 18 19 20
#  - 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.

Xavier Guimard's avatar
Xavier Guimard committed
21 22
use strict;
use utf8;
23
use Mouse;
24
use JSON 'to_json';
25
use Lemonldap::NG::Common::Conf::ReConstants;
26 27
use Lemonldap::NG::Manager::Attributes;

28 29
our $VERSION = '2.0.0';

Xavier Guimard's avatar
Xavier Guimard committed
30 31
extends 'Lemonldap::NG::Common::Conf::Compact';

32
# High debugging for developpers, set this to 1
33 34
use constant HIGHDEBUG => 0;

35
# Messages storage
36 37 38
has errors => (
    is      => 'rw',
    isa     => 'ArrayRef',
Xavier Guimard's avatar
Xavier Guimard committed
39
    default => sub { return [] }
40 41 42 43 44 45 46 47 48
);
has warnings => (
    is      => 'rw',
    isa     => 'ArrayRef',
    default => sub { return [] },
    trigger => sub {
        hdebug( 'warnings contains', $_[0]->{warnings} );
    }
);
49 50
has changes =>
    ( is => 'rw', isa => 'ArrayRef', default => sub { return [] } );
51 52 53 54 55 56 57 58
has message => (
    is      => 'rw',
    isa     => 'Str',
    default => '',
    trigger => sub {
        hdebug( "Message becomes " . $_[0]->{message} );
    }
);
59 60
has needConfirmation =>
    ( is => 'rw', isa => 'ArrayRef', default => sub { return [] } );
61 62

# Booleans
63 64
has confChanged => (
    is      => 'rw',
65
    isa     => 'Bool',
66 67 68 69 70
    default => 0,
    trigger => sub {
        hdebug( "condChanged: " . $_[0]->{confChanged} );
    }
);
71 72

# Properties required during build
73
has refConf => ( is => 'ro', isa => 'HashRef', required => 1 );
74 75 76 77
has req     => ( is => 'ro', required => 1 );
has newConf => ( is => 'rw', isa      => 'HashRef' );
has tree    => ( is => 'rw', isa      => 'ArrayRef' );

78
# High debug method
79 80 81 82 83
sub hdebug {
    if (HIGHDEBUG) {
        foreach my $d (@_) {
            if ( ref $d ) {
                require Data::Dumper;
84
                $Data::Dumper::Useperl = 1;
85 86 87 88 89 90 91 92
                print STDERR Data::Dumper::Dumper($d);
            }
            else { print STDERR "$d\n" }
        }
    }
    undef;
}

93 94 95
##@method boolean check()
# Main method
#@return result
96 97
sub check {
    my $self = shift;
98
    hdebug("# check()");
99
    unless ( $self->newConf ) {
100
        return 0 unless ( $self->scanTree );
101 102
    }
    unless ( $self->testNewConf ) {
103
        hdebug("  testNewConf() failed");
104 105
        return 0;
    }
106
    hdebug("  tests succeed");
Xavier Guimard's avatar
Xavier Guimard committed
107
    $self->compactConf( $self->newConf );
108
    unless ( $self->confChanged ) {
Christophe Maudoux's avatar
Christophe Maudoux committed
109
        hdebug("  no change detected");
110 111 112 113
        $self->message('__confNotChanged__');
        return 0;
    }
    return 1;
114 115
}

116 117 118
##@method boolean scanTree()
# Methods to build new conf from JSON string
#@result true if succeed
119 120
sub scanTree {
    my $self = shift;
121
    hdebug("# scanTree()");
122 123 124 125
    $self->newConf( {} );
    $self->_scanNodes( $self->tree ) or return 0;

    # Set cfgNum to ref cfgNum (will be changed when saving), set other
Xavier Guimard's avatar
Xavier Guimard committed
126
    # metadata and set a value to the key if empty
Xavier Guimard's avatar
Xavier Guimard committed
127
    $self->newConf->{cfgNum} = $self->req->params('cfgNum');
128 129 130 131
    $self->newConf->{cfgAuthor}
        = $self->req->userData
        ->{ $Lemonldap::NG::Handler::Main::tsv->{whatToTrace}
            || '_whatToTrace' } // "anonymous";
Xavier Guimard's avatar
Xavier Guimard committed
132
    $self->newConf->{cfgAuthorIP} = $self->req->address;
133
    $self->newConf->{cfgDate}     = time;
134
    $self->newConf->{cfgVersion}  = $VERSION;
135 136
    $self->newConf->{key}
        ||= join( '', map { chr( int( rand(94) ) + 33 ) } ( 1 .. 16 ) );
137 138 139 140 141 142

    return 1;
}

use feature 'state';

143
##@method private boolean _scanNodes()
144
# Recursive JSON parser
145
#@result true if succeed
146
sub _scanNodes {
147
    my ( $self, $tree, ) = @_;
148
    hdebug("# _scanNodes()");
149 150 151 152 153 154
    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;
    }
155 156 157
    unless (@$tree) {
        hdebug('  empty tree !?');
    }
158 159
    foreach my $leaf (@$tree) {
        my $name = $leaf->{title};
160
        hdebug("Looking to $name");
161 162

        # subnode
163
        my $subNodes     = $leaf->{nodes}      // $leaf->{_nodes};
164 165 166 167 168 169 170 171
        my $subNodesCond = $leaf->{nodes_cond} // $leaf->{_nodes_cond};

        ##################################
        # VirtualHosts and SAML partners #
        ##################################

        # Root nodes
        if ( $leaf->{id} =~ /^($specialNodeKeys)$/io ) {
172
            hdebug("Root special node detected $leaf->{id}");
173 174 175

            # If node has not been opened
            if ( $leaf->{cnodes} ) {
176
                hdebug("  not opened");
177
                foreach my $k ( @{ $specialNodeHash->{ $leaf->{id} } } ) {
178
                    hdebug("  copying $k");
179 180 181 182 183 184 185 186 187 188 189 190 191
                    $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) {
192
                hdebug( "Keys detected as removed:", \@old );
193 194 195
                $self->confChanged(1);
                foreach my $deletedHost (@old) {
                    push @{ $self->changes },
196
                        { key => $leaf->{id}, old => $deletedHost };
197 198 199 200 201 202 203
                }
            }
            next;
        }

        # 1st sublevel
        elsif ( $leaf->{id} =~ /^($specialNodeKeys)\/([^\/]+)$/io ) {
204
            hdebug("Special node chield detected $leaf->{id}");
205 206 207 208
            my ( $base, $host ) = ( $1, $2 );

            # Check hostname/partner name changes (id points to the old name)
            $newNames{$host} = $leaf->{title};
209 210 211 212
            if ( $newNames{$host} ne $host and $host !~ /^new__/ ) {
                hdebug("  $host becomes $newNames{$host}");
                $self->confChanged(1);
                push @{ $self->changes },
213
                    { key => $base, old => $host, new => $newNames{$host} };
214
            }
215 216 217 218 219 220

            $self->_scanNodes($subNodes);
            next;
        }

        # Other sub levels
221 222
        elsif ( $leaf->{id}
            =~ /^($specialNodeKeys)\/([^\/]+)\/([^\/]+)(?:\/(.*))?$/io )
223
        {
224 225
            my ( $base, $key, $oldName, $target, $h )
                = ( $1, $newNames{$2}, $2, $3, $4 );
226 227 228
            hdebug(
                "Special node chield subnode detected $leaf->{id}",
                "  base $base, key $key, target $target, h "
229
                    . ( $h ? $h : 'undef' )
230
            );
231 232 233

            # VirtualHosts
            if ( $base eq 'virtualHosts' ) {
234
                hdebug("  virtualhost");
235 236
                if ( $target =~ /^(?:locationRules|exportedHeaders|post)$/ ) {
                    if ( $leaf->{cnodes} ) {
237
                        hdebug('    unopened subnode');
238 239
                        $self->newConf->{$target}->{$key}
                            = $self->refConf->{$target}->{$oldName} // {};
240 241 242
                    }

                    elsif ($h) {
243
                        hdebug('    4 levels');
244
                        if ( $target eq 'locationRules' ) {
245
                            hdebug('    locationRules');
246 247 248 249
                            my $k
                                = $leaf->{comment}
                                ? "(?#$leaf->{comment})$leaf->{re}"
                                : $leaf->{re};
250 251 252
                            $self->set( $target, $key, $k, $leaf->{data} );
                        }
                        else {
253
                            hdebug('    other than locationrules');
254 255 256 257 258 259 260
                            $self->set( $target, $key, $leaf->{title},
                                $leaf->{data} );
                        }
                    }

                    # Unless $h is set, scan subnodes and check changes
                    else {
261
                        hdebug('    3 levels only (missing $h)');
262
                        if ( ref $subNodes ) {
263
                            hdebug('    has subnodes');
264
                            $self->_scanNodes($subNodes)
265
                                or return 0;
266 267 268 269
                        }
                        if ( exists $self->refConf->{$target}->{$key}
                            and %{ $self->refConf->{$target}->{$key} } )
                        {
270
                            hdebug('    old conf subnode has values');
271 272 273 274 275
                            my $c = $self->newConf->{$target};
                            foreach my $k (
                                keys %{ $self->refConf->{$target}->{$key} } )
                            {
                                unless ( defined $c->{$key}->{$k} ) {
276
                                    hdebug('      missing value in old conf');
277 278
                                    $self->confChanged(1);
                                    push @{ $self->changes },
279
                                        {
280 281
                                        key => "$target, $key",
                                        old => $k,
282
                                        };
283 284 285 286 287 288
                                }
                            }
                        }
                        elsif ( exists $self->newConf->{$target}->{$key}
                            and %{ $self->newConf->{$target}->{$key} } )
                        {
289
                            hdebug("    '$key' has values");
290 291
                            $self->confChanged(1);
                            push @{ $self->changes },
292
                                { key => "$target", new => $key };
293 294 295 296
                        }
                    }
                }
                elsif ( $target =~ /^$virtualHostKeys$/o ) {
297 298
                    $self->set( 'vhostOptions', [ $oldName, $key ],
                        $target, $leaf->{data} );
299 300 301
                }
                else {
                    push @{ $self->errors },
302
                        { message => "Unknown vhost key $target" };
303 304 305 306
                    return 0;
                }
                next;
            }
307

308
            # SAML
309
            elsif ( $base =~ /^saml(?:S|ID)PMetaDataNodes$/ ) {
310
                hdebug('SAML');
311 312
                if ( defined $leaf->{data}
                    and ref( $leaf->{data} ) eq 'ARRAY' )
Xavier Guimard's avatar
Xavier Guimard committed
313
                {
314 315 316
                    hdebug("  SAML data is an array, serializing");
                    $leaf->{data} = join ';', @{ $leaf->{data} };
                }
317 318
                if ( $target =~ /^saml(?:S|ID)PMetaDataExportedAttributes$/ )
                {
319
                    if ( $leaf->{cnodes} ) {
320
                        hdebug("  $target: unopened node");
321 322
                        $self->newConf->{$target}->{$key}
                            = $self->refConf->{$target}->{$oldName} // {};
323
                    }
Xavier Guimard's avatar
Xavier Guimard committed
324
                    elsif ($h) {
325
                        hdebug("  $target: opened node");
326
                        $self->confChanged(1);
327 328 329
                        $self->set( $target, $key, $leaf->{title},
                            $leaf->{data} );
                    }
330
                    else {
331 332
                        hdebug("  $target: looking for subnodes");
                        $self->_scanNodes($subNodes);
333
                    }
334
                }
335
                elsif ( $target =~ /^saml(?:S|ID)PMetaDataXML$/ ) {
336
                    hdebug("  $target");
337 338 339 340
                    $self->set(
                        $target, [ $oldName, $key ],
                        $target, $leaf->{data}
                    );
341
                }
342 343 344
                elsif ( $target =~ /^saml(?:ID|S)PMetaDataOptions/ ) {
                    my $optKey = $&;
                    hdebug("  $base sub key: $target");
345 346 347
                    if ( $target
                        =~ /^(?:$samlIDPMetaDataNodeKeys|$samlSPMetaDataNodeKeys)/o
                        )
348
                    {
349 350 351 352
                        $self->set(
                            $optKey, [ $oldName, $key ],
                            $target, $leaf->{data}
                        );
353 354 355
                    }
                    else {
                        push @{ $self->errors },
356 357
                            { message =>
                                "Unknown SAML metadata option $target" };
358 359
                        return 0;
                    }
360 361 362
                }
                else {
                    push @{ $self->errors },
363
                        { message => "Unknown SAML key $target" };
364 365 366 367
                    return 0;
                }
                next;
            }
368

Xavier Guimard's avatar
Xavier Guimard committed
369
            # OIDC
370
            elsif ( $base =~ /^oidc(?:O|R)PMetaDataNodes$/ ) {
371
                hdebug('OIDC');
Xavier Guimard's avatar
Xavier Guimard committed
372
                if ( $target =~ /^oidc(?:O|R)PMetaDataOptions$/ ) {
373 374
                    hdebug("  $target: looking for subnodes");
                    $self->_scanNodes($subNodes);
375 376
                    $self->set( $target, $key, $leaf->{title},
                        $leaf->{data} );
Xavier Guimard's avatar
Xavier Guimard committed
377 378
                }
                elsif ( $target =~ /^oidcOPMetaData(?:JSON|JWKS)$/ ) {
379
                    hdebug("  $target");
Xavier Guimard's avatar
Xavier Guimard committed
380 381 382
                    $self->set( $target, $key, $leaf->{data} );
                }
                elsif ( $target =~ /^oidc(?:O|R)PMetaDataExportedVars$/ ) {
383
                    hdebug("  $target");
Xavier Guimard's avatar
Xavier Guimard committed
384
                    if ( $leaf->{cnodes} ) {
385
                        hdebug('    unopened');
386 387
                        $self->newConf->{$target}->{$key}
                            = $self->refConf->{$target}->{$oldName} // {};
Xavier Guimard's avatar
Xavier Guimard committed
388 389
                    }
                    elsif ($h) {
390
                        hdebug('    opened');
Xavier Guimard's avatar
Xavier Guimard committed
391 392 393
                        $self->set( $target, $key, $leaf->{title},
                            $leaf->{data} );
                    }
394 395 396 397
                    else {
                        hdebug("  $target: looking for subnodes");
                        $self->_scanNodes($subNodes);
                    }
Xavier Guimard's avatar
Xavier Guimard committed
398
                }
399
                elsif ( $target =~ /^oidc(?:O|R)PMetaDataOptions/ ) {
400
                    my $optKey = $&;
401
                    hdebug "  $base sub key: $target";
Xavier Guimard's avatar
Xavier Guimard committed
402 403 404
                    if ( $target eq 'oidcRPMetaDataOptionsExtraClaims' ) {
                        if ( $leaf->{cnodes} ) {
                            hdebug('    unopened');
405 406
                            $self->newConf->{$target}->{$key}
                                = $self->refConf->{$target}->{$oldName} // {};
Xavier Guimard's avatar
Xavier Guimard committed
407 408 409 410 411 412 413 414 415 416 417
                        }
                        elsif ($h) {
                            hdebug('    opened');
                            $self->set( $target, $key, $leaf->{title},
                                $leaf->{data} );
                        }
                        else {
                            hdebug("  $target: looking for subnodes");
                            $self->_scanNodes($subNodes);
                        }
                    }
418 419 420
                    elsif ( $target
                        =~ /^(?:$oidcOPMetaDataNodeKeys|$oidcRPMetaDataNodeKeys)/o
                        )
421
                    {
422 423 424 425
                        $self->set(
                            $optKey, [ $oldName, $key ],
                            $target, $leaf->{data}
                        );
426 427 428
                    }
                    else {
                        push @{ $self->errors },
429 430
                            { message =>
                                "Unknown OIDC metadata option $target" };
431 432
                        return 0;
                    }
433
                }
Xavier Guimard's avatar
Xavier Guimard committed
434 435
                else {
                    push @{ $self->errors },
436
                        { message => "Unknown OIDC key $target" };
Xavier Guimard's avatar
Xavier Guimard committed
437 438 439 440
                    return 0;
                }
                next;
            }
441 442 443 444 445

            # CAS
            elsif ( $base =~ /^cas(?:App|Srv)MetaDataNodes$/ ) {
                my $optKey = $&;
                hdebug('CAS');
446 447 448
                if ( $target =~ /^cas(?:App|Srv)MetaDataOptions$/ ) {
                    hdebug("  $target: looking for subnodes");
                    $self->_scanNodes($subNodes);
449 450
                    $self->set( $target, $key, $leaf->{title},
                        $leaf->{data} );
451 452 453 454 455
                }
                elsif ( $target =~ /^cas(?:App|Srv)MetaDataExportedVars$/ ) {
                    hdebug("  $target");
                    if ( $leaf->{cnodes} ) {
                        hdebug('    unopened');
456 457
                        $self->newConf->{$target}->{$key}
                            = $self->refConf->{$target}->{$oldName} // {};
458 459 460 461 462 463 464 465 466 467 468
                    }
                    elsif ($h) {
                        hdebug('    opened');
                        $self->set( $target, $key, $leaf->{title},
                            $leaf->{data} );
                    }
                    else {
                        hdebug("  $target: looking for subnodes");
                        $self->_scanNodes($subNodes);
                    }
                }
469 470 471
                elsif ( $target =~ /^cas(?:Srv|App)MetaDataOptions/ ) {
                    my $optKey = $&;
                    hdebug "  $base sub key: $target";
472 473 474
                    if ( $target eq 'casSrvMetaDataOptionsProxiedServices' ) {
                        if ( $leaf->{cnodes} ) {
                            hdebug('    unopened');
475 476
                            $self->newConf->{$target}->{$key}
                                = $self->refConf->{$target}->{$oldName} // {};
477 478 479 480 481 482 483 484 485 486 487
                        }
                        elsif ($h) {
                            hdebug('    opened');
                            $self->set( $target, $key, $leaf->{title},
                                $leaf->{data} );
                        }
                        else {
                            hdebug("  $target: looking for subnodes");
                            $self->_scanNodes($subNodes);
                        }
                    }
488 489 490
                    elsif ( $target
                        =~ /^(?:$casSrvMetaDataNodeKeys|$casAppMetaDataNodeKeys)/o
                        )
491
                    {
dcoutadeur dcoutadeur's avatar
dcoutadeur dcoutadeur committed
492 493 494 495
                        $self->set(
                            $optKey, [ $oldName, $key ],
                            $target, $leaf->{data}
                        );
496 497 498
                    }
                    else {
                        push @{ $self->errors },
499 500
                            { message =>
                                "Unknown CAS metadata option $target" };
501 502
                        return 0;
                    }
503 504 505
                }
                else {
                    push @{ $self->errors },
506
                        { message => "Unknown CAS option $target" };
507 508
                    return 0;
                }
509
                next;
510
            }
511
            else {
512
                push @{ $self->errors },
513
                    { message => "Fatal: unknown special sub node $base" };
514 515
                return 0;
            }
516 517 518 519 520
        }

        ####################
        # Application list #
        ####################
521 522 523 524 525 526

        # Application list root node
        elsif ( $leaf->{title} eq 'applicationList' ) {
            hdebug( $leaf->{title} );
            if ( $leaf->{cnodes} ) {
                hdebug('  unopened');
527 528
                $self->newConf->{applicationList}
                    = $self->refConf->{applicationList} // {};
529 530 531 532 533
            }
            else {
                $self->_scanNodes($subNodes) or return 0;

                # Check for deleted
534 535 536 537 538 539 540
                my @listCatRef
                    = map { $self->refConf->{applicationList}->{$_}->{catname} }
                    keys %{ $self->refConf->{applicationList} };
                my @listCatNew
                    = map { $self->newConf->{applicationList}->{$_}->{catname} }
                    keys(
                    %{  ref $self->newConf->{applicationList}
541 542 543
                        ? $self->newConf->{applicationList}
                        : {}
                    }
544
                    );
545 546
                @listCatRef =  sort @listCatRef;
                @listCatNew =  sort @listCatNew;
547 548
                hdebug( '# @listCatRef : ' . \@listCatRef );
                hdebug( '# @listCatNew : ' . \@listCatNew );
549
                for ( my $i = 0; $i < @listCatNew; $i++ ) {
550 551 552
                    if ( not( defined $listCatRef[$i] )
                        or $listCatRef[$i] ne $listCatNew[$i] )
                    {
553
                        push @{ $self->changes },
554
                            {
555 556 557
                            key => $leaf->{id},
                            new => $listCatNew[$i],
                            old => $listCatRef[$i]
558
                            };
559 560 561 562 563 564 565
                    }
                }
            }
            next;
        }

        # Application list sub nodes
566
        elsif ( $leaf->{id} =~ /^applicationList\/(.+)$/ ) {
567
            hdebug('Application list subnode');
568 569 570 571 572 573 574 575 576 577 578 579
            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) {
580
                hdebug("  looking to cat $cat");
581 582
                unless ( defined $knownCat->{$cat} ) {
                    push @{ $self->{errors} },
583 584 585
                        { message =>
                            "Fatal: sub cat/app before parent ($leaf->{id})"
                        };
586 587 588 589 590 591 592 593 594 595 596 597 598
                    return 0;
                }
                $cn = $cn->{ $knownCat->{$cat} };
                push @path, $cn->{catname};
                $cmp->{$cat} //= {};
                $cmp = $cmp->{$cat};
            }

            # 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' ) {
599
                hdebug('  menu cat');
600 601
                $knownCat->{__id}++;
                my $s = $knownCat->{$app} = sprintf '%04d-cat',
602 603
                    $knownCat->{__id};
                $cn->{$s} = { catname => $leaf->{title}, type => 'category' };
604 605 606 607 608
                unless ($cmp->{$app}
                    and $cmp->{$app}->{catname} eq $cn->{$s}->{catname} )
                {
                    $self->confChanged(1);
                    push @{ $self->changes },
609 610 611
                        {
                        key => join( ', ',
                            'applicationList', @path, $leaf->{title} ),
612 613
                        new => $cn->{$s}->{catname},
                        old => ( $cn->{$s} ? $cn->{$s}->{catname} : undef )
614
                        };
615 616 617 618
                }
                if ( ref $subNodes ) {
                    $self->_scanNodes($subNodes) or return 0;
                }
619 620
                my @listCatRef = keys %{ $cmp->{$app} };
                my @listCatNew = keys %{ $cn->{$s} };
621

622 623 624 625
                # Check for deleted
                unless ( @listCatRef == @listCatNew ) {
                    $self->confChanged(1);
                    push @{ $self->changes },
626
                        {
627
                        key => join( ', ', 'applicationList', @path ),
628
                        new => 'Changes in cat(s)/app(s)',
629
                        };
630
                }
631 632 633 634
            }

            # Create new apps
            else {
635
                hdebug('  new app');
636 637
                $knownCat->{__id}++;
                my $name = sprintf( '%04d-app', $knownCat->{__id} );
638 639
                $cn->{$name}
                    = { type => 'application', options => $leaf->{data} };
640 641 642 643
                $cn->{$name}->{options}->{name} = $leaf->{title};
                unless ( $cmp->{$app} ) {
                    $self->confChanged(1);
                    push @{ $self->changes },
644
                        {
645 646
                        key => join( ', ', 'applicationList', @path ),
                        new => $leaf->{title},
647
                        };
648 649 650 651 652 653 654 655
                }
                else {
                    foreach my $k ( keys %{ $cn->{$name}->{options} } ) {
                        unless ( $cmp->{$app}->{options}->{$k} eq
                            $cn->{$name}->{options}->{$k} )
                        {
                            $self->confChanged(1);
                            push @{ $self->changes },
656
                                {
657 658 659 660 661
                                key => join( ', ',
                                    'applicationList', @path,
                                    $leaf->{title},    $k ),
                                new => $cn->{$name}->{options}->{$k},
                                old => $cmp->{$app}->{options}->{$k}
662
                                };
663 664 665 666 667 668
                        }
                    }
                }
            }
            next;
        }
669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684
        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");
685 686
                    my $k = $n->{re}
                        . ( $n->{comment} ? "##$n->{comment}" : '' );
687 688 689 690 691
                    $self->newConf->{grantSessionRules}->{$k} = $n->{data};
                    $count++;
                    unless ( defined $ref->{$k} ) {
                        $self->confChanged(1);
                        push @{ $self->changes },
692
                            { keys => 'grantSessionRules', new => $k };
693 694 695 696
                    }
                    elsif ( $ref->{$k} ne $n->{data} ) {
                        $self->confChanged(1);
                        push @{ $self->changes },
697
                            {
698 699 700
                            key => "grantSessionRules, $k",
                            old => $self->refConf->{grantSessionRules}->{$k},
                            new => $n->{data}
701
                            };
702 703 704 705 706 707
                    }
                    @old = grep { $_ ne $k } @old;
                }
                if (@old) {
                    $self->confChanged(1);
                    push @{ $self->changes },
708 709
                        { key => 'grantSessionRules', old => $_, }
                        foreach (@old);
710 711 712 713
                }
            }
            next;
        }
714

715 716 717 718 719 720 721
        # 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} },
722 723
                        { message => 'Malformed openIdIDPList '
                            . $leaf->{data} };
724 725 726 727 728 729 730 731 732 733
                    return 0;
                }
                $self->set( $name, join( ';', @{ $leaf->{data} } ) );
            }
            else {
                $self->set( $name, undef );
            }
            next;
        }

734 735 736
        ####################
        # Other hash nodes #
        ####################
737
        elsif ( $leaf->{title} =~ /^$simpleHashKeys$/o
738 739
            and not $leaf->{title} eq 'applicationList' )
        {
740
            hdebug( $leaf->{title} );
741 742 743

            # If a `cnodes` key is found, keep old key unchanges
            if ( $leaf->{cnodes} ) {
744
                hdebug('  unopened');
745 746 747
                $self->newConf->{$name} = $self->refConf->{$name} // {};
            }
            else {
748
                hdebug('  opened');
749 750 751 752 753 754 755

                # combModules: just to replace "over" key
                if ( $name eq 'combModules' ) {
                    hdebug('     combModules');
                    $self->newConf->{$name} = {};
                    foreach my $node ( @{ $leaf->{nodes} } ) {
                        my $tmp;
756 757
                        $tmp->{$_} = $node->{data}->{$_}
                            foreach (qw(type for));
758 759 760 761 762 763 764 765 766 767 768 769
                        $tmp->{over} = {};
                        foreach ( @{ $node->{data}->{over} } ) {
                            $tmp->{over}->{ $_->[0] } = $_->[1];
                        }
                        $self->newConf->{$name}->{ $node->{title} } = $tmp;
                    }

                    # TODO: check changes
                    $self->confChanged(1);
                    next;
                }

770 771 772 773 774 775 776 777 778
                $subNodes //= [];
                my $count = 0;
                my @old   = (
                    ref( $self->refConf->{$name} )
                    ? ( keys %{ $self->refConf->{$name} } )
                    : ()
                );
                $self->newConf->{$name} = {};
                foreach my $n (@$subNodes) {
779
                    hdebug("  looking at $n subnode");
Xavier Guimard's avatar
Xavier Guimard committed
780
                    if ( ref $n->{data} and ref $n->{data} eq 'ARRAY' ) {
Clément OUDOT's avatar
Clément OUDOT committed
781

782 783 784 785
                        # authChoiceModules
                        if ( $name eq 'authChoiceModules' ) {
                            hdebug('     combModules');
                            $n->{data}->[5] ||= {};
Clément OUDOT's avatar
Clément OUDOT committed
786
                            $n->{data}->[5] = to_json( $n->{data}->[5] );
787 788
                        }

Xavier Guimard's avatar
Xavier Guimard committed
789 790
                        $n->{data} = join ';', @{ $n->{data} };
                    }
791 792
                    $self->newConf->{$name}->{ $n->{title} } = $n->{data};
                    $count++;
793 794
                    unless (
                        defined $self->refConf->{$name}->{ $n->{title} } )
795 796 797
                    {
                        $self->confChanged(1);
                        push @{ $self->changes },
798
                            { key => $name, new => $n->{title}, };
799
                    }
800 801
                    elsif ( $self->refConf->{$name}->{ $n->{title} } ne
                        $n->{data} )
802 803 804
                    {
                        $self->confChanged(1);
                        push @{ $self->changes },
805
                            {
806 807 808
                            key => "$name, $n->{title}",
                            old => $self->refConf->{$name}->{ $n->{title} },
                            new => $n->{data}
809
                            };
810 811 812 813 814 815
                    }
                    @old = grep { $_ ne $n->{title} } @old;
                }
                if (@old) {
                    $self->confChanged(1);
                    push @{ $self->changes }, { key => $name, old => $_, }
816
                        foreach (@old);
817 818 819 820 821
                }
            }
            next;
        }

822 823 824 825 826 827 828 829 830
        # Double hash nodes
        elsif ( $leaf->{title} =~ /^$doubleHashKeys$/ ) {
            hdebug( $leaf->{title} );
            my @oldHosts = (
                ref( $self->refConf->{$name} )
                ? ( keys %{ $self->refConf->{$name} } )
                : ()
            );
            $self->newConf->{$name} = {};
831 832
            unless ( defined $leaf->{data} ) {
                hdebug('  unopened');
833
                $self->newConf->{$name} = $self->refConf->{$name} || {};
834
                next;
835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852
            }
            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} } ) {
853 854 855
                    $self->newConf->{$name}->{$host}->{ $prm->{k} }
                        = $prm->{v};
                    if (!$change
856 857
                        and (
                            not defined(
858 859
                                $self->refConf->{$name}->{$host}
                                    ->{ $prm->{k} }
860 861 862 863
                            )
                            or $self->newConf->{$name}->{$host}->{ $prm->{k} }
                            ne $self->refConf->{$name}->{$host}->{ $prm->{k} }
                        )
864
                        )
865 866 867 868
                    {
                        $self->confChanged(1);
                        hdebug("    key $prm->{k} has been changed");
                        push @{ $self->changes },
869
                            { key => "$name/$host", new => $prm->{k} };
870 871 872 873 874 875 876 877 878
                    }
                    elsif ( !$change ) {
                        @oldKeys = grep { $_ ne $prm->{k} } @oldKeys;
                    }
                }
                if (@oldKeys) {
                    $self->confChanged(1);
                    hdebug( "  old keys: " . join( ' ', @oldKeys ) );
                    push @{ $self->changes },
879 880
                        { key => "$name/$host", old => $_ }
                        foreach (@oldKeys);
881 882 883 884 885 886
                }
            }
            if (@oldHosts) {
                $self->confChanged(1);
                hdebug( "  old hosts " . join( ' ', @oldHosts ) );
                push @{ $self->changes }, { key => "$name", old => $_ }
887
                    foreach (@oldHosts);
888 889 890 891
            }
            next;
        }

892 893 894 895 896 897 898
        ###############
        # Other nodes #
        ###############

        # Check if subnodes
        my $n = 0;
        if ( ref $subNodesCond ) {
899 900 901 902
            hdebug('  conditional subnodes detected');

            # Bad idea,subnode unopened are not read
            #$subNodesCond = [ grep { $_->{show} } @$subNodesCond ];
903 904 905 906
            $self->_scanNodes($subNodesCond) or return 0;
            $n++;
        }
        if ( ref $subNodes ) {
907
            hdebug('  subnodes detected');
908 909 910
            $self->_scanNodes($subNodes) or return 0;
            $n++;
        }
911 912 913
        if ($n) {
            next;
        }
914 915
        if ( defined $leaf->{data} and ref( $leaf->{data} ) eq 'ARRAY' ) {
            if ( ref( $leaf->{data}->[0] ) eq 'HASH' ) {
916
                hdebug("  array found");
917 918 919 920 921 922 923
                $self->_scanNodes( $leaf->{data} ) or return 0;
            }
            else {
                $self->set( $name, join( ';', @{ $leaf->{data} } ) );
            }
        }

924 925 926 927 928 929 930 931
        # 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 );
            }
        }

932 933 934 935 936 937 938 939
        # Normal leaf
        else {
            $self->set( $name, $leaf->{data} );
        }
    }
    return 1;
}

940 941
##@method private void set($target, @path, $data)
# Store a value in the $target key (following subkeys if @path is set)
942 943 944 945 946 947 948 949 950
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 ) {
951 952 953
            my $v = ref($tmp) ? $tmp->[$i] : $tmp;
            $confs[$i]->{$v} //= {};
            $confs[$i] = $confs[$i]->{$v};
954 955 956
        }
    }
    my $target = shift;
957 958
    hdebug( "# set() called:",
        { data => $data, path => \@path, target => $target } );
959 960 961 962
    die @path unless ($target);

    # Check new value
    if ( defined $data ) {
963
        hdebug("  data defined");
964 965 966 967 968 969 970 971

        # 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 )
Xavier Guimard's avatar
Xavier Guimard committed
972
                or (   !defined $confs[0]->{$target}
973 974
                    and defined $self->defaultValue($target)
                    and $data eq $self->defaultValue($target) )
975
                )
976 977 978
            {
                $self->confChanged(1);
                push @{ $self->changes },
979
                    {
980
                    key => join( ', ', @path, $target ),
981 982
                    old => $confs[0]->{$target}
                        // $self->defaultValue($target),
983
                    new => $confs[1]->{$target}
984
                    };
985 986 987 988 989 990
            }
        };
    }

    # Set old value if exists
    else {
991
        hdebug("  data undefined");
992
        if ( exists $confs[0]->{$target} ) {
993
            hdebug("    old value exists");
994 995
            $confs[1]->{$target} = $confs[0]->{$target};
        }
996 997 998
        else {
            hdebug("    no old value, skipping");
        }
999 1000 1001 1002
    }
}

sub defaultValue {
1003
    my ( $self, $target ) = @_;
Xavier Guimard's avatar
Xavier Guimard committed
1004
    hdebug("# defaultValue($target)");
1005 1006 1007
    die unless ($target);
    my $res = eval {
        &Lemonldap::NG::Manager::Attributes::attributes()->{$target}
1008
            ->{'default'};
1009 1010 1011 1012
    };
    return $res;
}

1013 1014 1015 1016
##@method boolean testNewConf()
# Launch _unitTest() and _globaTest()
#
#@return true if tests succeed
1017 1018
sub testNewConf {
    my $self = shift;
1019
    hdebug('# testNewConf()');
1020
    return $self->_unitTest( $self->newConf(), '' ) && $self->_globalTest();
1021 1022
}

1023 1024 1025 1026
##@method private boolean _unitTest()
# Launch unit tests declared in Lemonldap::NG::Manager::Build::Attributes file
#
#@return true if tests succeed
1027
sub _unitTest {
1028
    my ( $self, $conf ) = @_;
1029
    hdebug('# _unitTest()');
1030 1031 1032 1033
    my $types = &Lemonldap::NG::Manager::Attributes::types();
    my $attrs = &Lemonldap::NG::Manager::Attributes::attributes();
    my $res   = 1;
    foreach my $key ( keys %$conf ) {
1034 1035 1036 1037 1038 1039
        if (    $self->{skippedUnitTests}
            and $self->{skippedUnitTests} =~ /\b$key\b/ )
        {
            $self->logger->debug("Ignore test for $key");
            next;
        }
1040
        hdebug("Testing $key");
1041 1042
        my $attr = $attrs->{$key};
        my $type = $types->{ $attr->{type} };
1043 1044 1045 1046 1047
        unless ( $type or $attr->{test} ) {
            print STDERR "Unknown attribute $key, deleting it\n";
            delete $conf->{$key};
            next;
        }
1048

1049
        if ( $attr->{type} and $attr->{type} eq 'subContainer' ) {
1050

Xavier Guimard's avatar
Xavier Guimard committed
1051
            # TODO Recursive for SAML/OIDC nodes
1052 1053
        }
        else {
1054 1055

            # Check if key exists
1056
            unless ($attr) {
1057 1058
                push @{ $self->errors },
                    { message => "__unknownKey__: $key" };
1059
                $res = 0;
1060 1061 1062
                next;
            }

1063
            # Hash parameters
1064
            if ( $key =~ /^$simpleHashKeys$/o ) {
1065 1066 1067
                $conf->{$key} //= {};
                unless ( ref $conf->{$key} eq 'HASH' ) {
                    push @{ $self->errors },
1068
                        { message => "$key is not a hash ref" };
1069 1070 1071
                    $res = 0;
                    next;
                }
1072
            }
1073
            elsif ( $attr->{type} =~ /Container$/ ) {
1074

1075 1076
                #TODO
            }
1077
            if (   $key =~ /^(?:$simpleHashKeys|$doubleHashKeys)$/o
1078 1079
                or $attr->{type} =~ /Container$/ )
            {
1080
                my $keyMsg = $attr->{keyMsgFail} // $type->{keyMsgFail};
1081
                my $msg    = $attr->{msgFail}    // $type->{msgFail};
1082
                $res = 0
1083
                    unless (
1084
                    $self->_execTest(
1085
                        {   keyTest => $attr->{keyTest} // $type->{keyTest},
1086
                            keyMsgFail => $attr->{keyMsgFail}
1087 1088
                                // $type->{keyMsgFail},
                            test    => $attr->{test}    // $type->{test},
1089 1090 1091 1092 1093
                            msgFail => $attr->{msgFail} // $type->{msgFail},
                        },
                        $conf->{$key},
                        $key, $attr, undef, $conf
                    )
1094
                    );
1095 1096
            }
            elsif ( defined $attr->{keyTest} ) {
1097 1098 1099 1100

                #TODO
            }
            else {
1101 1102
                my $msg = $attr->{msgFail} // $type->{msgFail};
                $res = 0
1103
                    unless (
1104 1105 1106 1107
                    $self->_execTest(
                        $attr->{test} // $type->{test},
                        $conf->{$key}, $key, $attr, $msg, $conf
                    )
1108
                    );
1109 1110 1111 1112 1113 1114
            }
        }
    }
    return $res;
}

1115 1116 1117 1118 1119 1120 1121
##@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;
1122
    die
1123 1124
        "Malformed test for $key: only regexp ref or sub are accepted (type \"$ref\")"
        unless ( $ref = ref($test) and $ref =~ /^(CODE|Regexp|HASH)$/ );
1125
    if ( $ref eq 'CODE' ) {
1126
        my ( $r, $m ) = ( $test->( $value, $conf, $attr ) );
1127 1128
        if ($m) {
            push @{ $self->{ ( $r ? 'warnings' : 'errors' ) } },
1129
                { message => "$key: $m" };
1130 1131 1132 1133