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
Typo  
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 ) {
Xavier Guimard's avatar
Xavier Guimard committed
103
        hdebug("  testNewConf() failed");
104 105
        return 0;
    }
Xavier Guimard's avatar
Xavier Guimard committed
106
    hdebug("  tests succeed");
Xavier Guimard's avatar
Xavier Guimard committed
107
    $self->compactConf( $self->newConf );
Xavier Guimard's avatar
Xavier Guimard committed
108
    unless ( $self->confChanged ) {
Christophe Maudoux's avatar
Typo  
Christophe Maudoux committed
109
        hdebug("  no change detected");
Xavier Guimard's avatar
Xavier Guimard committed
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;
    }
Xavier Guimard's avatar
Xavier Guimard committed
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 547
                @listCatRef =  sort @listCatRef;
                @listCatNew =  sort @listCatNew;
548 549
                hdebug( '# @listCatRef : ' . \@listCatRef );
                hdebug( '# @listCatNew : ' . \@listCatNew );
550
                for ( my $i = 0; $i < @listCatNew; $i++ ) {
551 552 553
                    if ( not( defined $listCatRef[$i] )
                        or $listCatRef[$i] ne $listCatNew[$i] )
                    {
554
                        push @{ $self->changes },
555
                            {
556 557 558
                            key => $leaf->{id},
                            new => $listCatNew[$i],
                            old => $listCatRef[$i]
559
                            };
560 561 562 563 564 565 566
                    }
                }
            }
            next;
        }

        # Application list sub nodes
567
        elsif ( $leaf->{id} =~ /^applicationList\/(.+)$/ ) {
568
            hdebug('Application list subnode');
569 570 571 572 573 574 575 576 577 578 579 580
            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) {
581
                hdebug("  looking to cat $cat");
582 583
                unless ( defined $knownCat->{$cat} ) {
                    push @{ $self->{errors} },
584 585 586
                        { message =>
                            "Fatal: sub cat/app before parent ($leaf->{id})"
                        };
587 588 589 590 591 592 593 594 595 596 597 598 599
                    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' ) {
600
                hdebug('  menu cat');
601 602
                $knownCat->{__id}++;
                my $s = $knownCat->{$app} = sprintf '%04d-cat',
603 604
                    $knownCat->{__id};
                $cn->{$s} = { catname => $leaf->{title}, type => 'category' };
605 606 607 608 609
                unless ($cmp->{$app}
                    and $cmp->{$app}->{catname} eq $cn->{$s}->{catname} )
                {
                    $self->confChanged(1);
                    push @{ $self->changes },
610 611 612
                        {
                        key => join( ', ',
                            'applicationList', @path, $leaf->{title} ),
613 614
                        new => $cn->{$s}->{catname},
                        old => ( $cn->{$s} ? $cn->{$s}->{catname} : undef )
615
                        };
616 617 618 619
                }
                if ( ref $subNodes ) {
                    $self->_scanNodes($subNodes) or return 0;
                }
620 621
                my @listCatRef = keys %{ $cmp->{$app} };
                my @listCatNew = keys %{ $cn->{$s} };
622

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

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

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

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

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

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

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

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

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

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

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

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

        # Check if subnodes
        my $n = 0;
        if ( ref $subNodesCond ) {
Xavier Guimard's avatar
Xavier Guimard committed
900 901 902 903
            hdebug('  conditional subnodes detected');

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1076 1077
                #TODO
            }
1078
            if (   $key =~ /^(?:$simpleHashKeys|$doubleHashKeys)$/o
1079 1080
                or $attr->{type} =~ /Container$/ )
            {
1081
                my $keyMsg = $attr->{keyMsgFail} // $type->{keyMsgFail};
1082
                my $msg    = $attr->{msgFail}    // $type->{msgFail};
1083
                $res = 0
1084
                    unless (
1085
                    $self->_execTest(
1086
                        {   keyTest => $attr->{keyTest} // $type->{keyTest},