diff --git a/etc/bastion/bastion.conf.dist b/etc/bastion/bastion.conf.dist index fa6e9ab..47b0e3f 100644 --- a/etc/bastion/bastion.conf.dist +++ b/etc/bastion/bastion.conf.dist @@ -236,10 +236,22 @@ "enableAccountSqlLog": true, # # ttyrecFilenameFormat (string) -# DESC: Sets the filename format of the output files of ttyrec for a given session. Magic tokens are: ``&bastionname``, ``&uniqid``, ``&account``, ``&ip``, ``&port``, ``&user`` (they'll be replaced by the corresponding values of the current session). Then, this string (automatically prepended with the correct folder) will be passed to ttyrec's ``-F`` parameter, which uses ``strftime()`` to expand it, so the usual character conversions will be done (``%Y`` for the year, ``%H`` for the hour, etc., see ``man strftime``). Note that in a addition to the usual ``strftime()`` conversion specifications, ttyrec also supports ``#usec#``, to be replaced by the current microsecond value of the time. +# DESC: Sets the filename format of the output files of ttyrec for a given session. Magic tokens are: ``&bastionname``, ``&uniqid``, ``&account``, ``&ip``, ``&port``, ``&user``, ``&home`` (the connecting account's home directory) and ``&remoteaccount`` (the remote account name for realm accounts, empty otherwise); they'll be replaced by the corresponding values of the current session. Then, this string (automatically prepended with the correct folder) will be passed to ttyrec's ``-F`` parameter, which uses ``strftime()`` to expand it, so the usual character conversions will be done (``%Y`` for the year, ``%H`` for the hour, etc., see ``man strftime``). Note that in a addition to the usual ``strftime()`` conversion specifications, ttyrec also supports ``#usec#``, to be replaced by the current microsecond value of the time. NOTE: this option sets the *filename* only, which is placed within a standard folder layout. To control the full path (directories included), define ``ttyrecDirectPathFormat`` and/or ``ttyrecViaPathFormat`` instead, which take precedence over this option when set. # DEFAULT: "%Y-%m-%d.%H-%M-%S.#usec#.&uniqid.&account.&user.&ip.&port.ttyrec" "ttyrecFilenameFormat": "%Y-%m-%d.%H-%M-%S.#usec#.&uniqid.&account.&user.&ip.&port.ttyrec", # +# ttyrecDirectPathFormat (string) +# DESC: Full absolute path template (directories AND filename) of the ttyrec recording for a *direct* egress connection (i.e. not going through a jumphost). The supported magic tokens are the same as ``ttyrecFilenameFormat``. ``/`` separates directory components, the last one being the filename, which is passed to ttyrec's ``-F`` (hence ``strftime()``-expanded as for ``ttyrecFilenameFormat``). The directory tree is created as needed. When left empty, the standard layout ``&home/ttyrec/&ip/`` is used (with an extra ``&remoteaccount`` subfolder for realm accounts). +# EXAMPLE: "&home/ttyrec/&ip/%Y-%m-%d.%H-%M-%S.#usec#.&uniqid.&account.&user.&ip.&port.ttyrec" +# DEFAULT: "" +"ttyrecDirectPathFormat": "", +# +# ttyrecViaPathFormat (string) +# DESC: Same as ``ttyrecDirectPathFormat``, but used for egress connections going *through* a jumphost (proxy-jump). In addition to the tokens above, the proxy-related tokens ``&proxyip``, ``&proxyport`` and ``&proxyuser`` are available. When left empty, the standard layout ``&home/ttyrec/via-&proxyip-&ip/`` is used; note that with the default ``ttyrecFilenameFormat`` the proxy information then only appears in the folder name, so set this option explicitly if you want it in the filename too. +# EXAMPLE: "&home/ttyrec/via-&proxyip-&ip/%Y-%m-%d.%H-%M-%S.#usec#.&uniqid.&account.&user.&ip.&port.via.&proxyuser.&proxyip.&proxyport.ttyrec" +# DEFAULT: "" +"ttyrecViaPathFormat": "", +# # ttyrecAdditionalParameters (array of strings) # DESC: Additional parameters you want to pass to ``ttyrec`` invocation. Useful, for example, to enable on-the-fly compression, disable cheatcodes, or set/unset any other ``ttyrec`` option. This is an ARRAY, not a string. # EXAMPLE: ["-s", "This is a message with spaces", "--zstd"] diff --git a/lib/perl/OVH/Bastion.pm b/lib/perl/OVH/Bastion.pm index 99f6af8..799be33 100644 --- a/lib/perl/OVH/Bastion.pm +++ b/lib/perl/OVH/Bastion.pm @@ -1197,53 +1197,82 @@ sub build_ttyrec_cmdline_part1of2 { $params{'proxyIp'} = 'v6[' . $params{'proxyIp'} . ']'; } - # build ttyrec filename format - my $bastionName = OVH::Bastion::config('bastionName')->value; - my $ttyrecFilenameFormat = OVH::Bastion::config('ttyrecFilenameFormat')->value; - $ttyrecFilenameFormat =~ s/&bastionname/$bastionName/g; - $ttyrecFilenameFormat =~ s/&uniqid/$params{'uniqid'}/g if $params{'uniqid'}; - $ttyrecFilenameFormat =~ s/&ip/$params{'ip'}/g if $params{'ip'}; - $ttyrecFilenameFormat =~ s/&port/$params{'port'}/g if defined $params{'port'}; - $ttyrecFilenameFormat =~ s/&user/$params{'user'}/g if defined $params{'user'}; - $ttyrecFilenameFormat =~ s/&account/$params{'account'}/g if $params{'account'}; - $ttyrecFilenameFormat =~ s/&proxyip/$params{'proxyIp'}/g if defined $params{'proxyIp'}; - $ttyrecFilenameFormat =~ s/&proxyport/$params{'proxyPort'}/g if defined $params{'proxyPort'}; - $ttyrecFilenameFormat =~ s/&proxyuser/$params{'proxyUser'}/g if defined $params{'proxyUser'}; - - if ($ttyrecFilenameFormat =~ /&(bastionname|uniqid|ip|port|user|account)/) { - - # if we still have a placeholder here, then we were missing parameters - return R('ERR_MISSING_PARAMETER', - msg => "Missing bastionname, uniqid, ip, port, user or account in ttyrec cmdline building"); + # Resolve the ttyrec recording path. Two dedicated full-path templates (directories + filename) + # are used depending on whether the egress connection goes through a jumphost ('via') or not + # ('direct'). When the relevant option isn't set, the path is built from the standard layout + # using ttyrecFilenameFormat (which sets the filename only). + my $bastionName = OVH::Bastion::config('bastionName')->value; + my $isVia = defined $params{'proxyIp'}; + + my $pathFormat = + $isVia + ? OVH::Bastion::config('ttyrecViaPathFormat')->value + : OVH::Bastion::config('ttyrecDirectPathFormat')->value; + + if (!$pathFormat) { + my $filenameFormat = OVH::Bastion::config('ttyrecFilenameFormat')->value; + + # standard layout: /ttyrec[/]//, where + # is 'via--' for proxy-jump connections (so it sorts by proxy first) or '' + my $perHost = $isVia ? 'via-&proxyip-&ip' : '&ip'; + $pathFormat = "&home/ttyrec/&remoteaccount/$perHost/$filenameFormat"; + } + + # Substitute placeholders. Required tokens are left untouched when their value is missing, so + # the check just below catches the error; optional tokens (proxy*, remoteaccount) become the + # empty string when absent, and the resulting empty path components are collapsed afterwards. + my %requiredToken = ( + '&home' => $params{'home'}, + '&bastionname' => $bastionName, + '&uniqid' => $params{'uniqid'}, + '&ip' => $params{'ip'}, + '&port' => $params{'port'}, + '&user' => $params{'user'}, + '&account' => $params{'account'}, + ); + my %optionalToken = ( + '&remoteaccount' => (($params{'realm'} && $params{'remoteaccount'}) ? $params{'remoteaccount'} : ''), + '&proxyip' => $params{'proxyIp'}, + '&proxyport' => $params{'proxyPort'}, + '&proxyuser' => $params{'proxyUser'}, + ); + foreach my $tok (keys %requiredToken) { + my $val = $requiredToken{$tok}; + next if !defined $val; + $val =~ tr{/}{_} if $tok ne '&home'; # slash guard, except &home which is legitimately a path + $pathFormat =~ s/\Q$tok\E/$val/g; + } + foreach my $tok (keys %optionalToken) { + my $val = $optionalToken{$tok} // ''; + $val =~ tr{/}{_}; + $pathFormat =~ s/\Q$tok\E/$val/g; } - # if there is no proxyIp, remove the via part and all proxy placeholders - if (!defined $params{'proxyIp'}) { - $ttyrecFilenameFormat =~ s/\.via//g; - $ttyrecFilenameFormat =~ s/\.&proxyuser//g; - $ttyrecFilenameFormat =~ s/\.&proxyip//g; - $ttyrecFilenameFormat =~ s/\.&proxyport//g; + # a still-present required placeholder means we were called with a missing parameter + if ($pathFormat =~ /&(home|bastionname|uniqid|ip|port|user|account)/) { + return R('ERR_MISSING_PARAMETER', + msg => "Missing home, bastionname, uniqid, ip, port, user or account in ttyrec cmdline building"); } - # ensure there are no '/' - $ttyrecFilenameFormat =~ tr{/}{_}; + # collapse empty path components (e.g. an empty &remoteaccount for non-realm accounts) + $pathFormat =~ s{/+}{/}g; - # prepend (and create) directory - my $saveDir = $params{'home'} . "/ttyrec"; - mkdir($saveDir); - if ($params{'realm'} && $params{'remoteaccount'}) { - $saveDir .= "/" . $params{'remoteaccount'}; - mkdir($saveDir); + # safety: the resolved path must be absolute and must not climb out of the tree with '..' + if ($pathFormat !~ m{^/} || $pathFormat =~ m{(?:^|/)\.\.(?:/|$)}) { + return R('ERR_SECURITY_VIOLATION', msg => "Refusing to use unsafe ttyrec path '$pathFormat'"); } - # format it like via-$proxyIp-$ip so that it's sorted by proxyIp first, then by ip if you list the directory - if ($params{'proxyIp'}) { - $saveDir .= "/via-" . $params{'proxyIp'} . "-" . $params{'ip'}; + # split into directory + filename, and create the directory tree (best-effort, default perms) + my ($saveDir, $ttyrecFilenameFormat) = $pathFormat =~ m{^(.*)/([^/]+)$}; + if (!$saveDir || !$ttyrecFilenameFormat) { + return R('ERR_INVALID_PARAMETER', msg => "Invalid ttyrec path '$pathFormat'"); } - else { - $saveDir .= "/" . $params{'ip'}; + my $mkpath = ''; + foreach my $component (split m{/}, $saveDir) { + next if !$component; + $mkpath .= "/$component"; + mkdir($mkpath); # ignore errors: parent directories likely already exist } - mkdir($saveDir); my $saveFileFormat = "$saveDir/$ttyrecFilenameFormat"; diff --git a/lib/perl/OVH/Bastion/configuration.inc b/lib/perl/OVH/Bastion/configuration.inc index 271d604..3e7ff97 100644 --- a/lib/perl/OVH/Bastion/configuration.inc +++ b/lib/perl/OVH/Bastion/configuration.inc @@ -156,11 +156,19 @@ sub load_configuration { {name => 'syslogDescription', default => 'bastion', validre => qr/^([a-zA-Z0-9_.-]+)$/}, { name => 'ttyrecFilenameFormat', - default => - '%Y-%m-%d.%H-%M-%S.#usec#.&uniqid.&account.&user.&ip.&port.via.&proxyuser.&proxyip.&proxyport.ttyrec', + default => '%Y-%m-%d.%H-%M-%S.#usec#.&uniqid.&account.&user.&ip.&port.ttyrec', validre => qr/^([a-zA-Z0-9%&#_.-]+)$/ }, - {name => 'accountExpiredMessage', default => '', validre => qr/^(.*)$/, emptyok => 1}, + + # ttyrecDirectPathFormat/ttyrecViaPathFormat: full absolute path templates (directories + + # filename) for the session recording, used respectively for direct egress and for + # proxy-jump ('via') egress. When left empty (the default), the path is built from the + # standard layout using ttyrecFilenameFormat. '/' separates directory components; the last + # component is the filename. The supported placeholders are the same as ttyrecFilenameFormat; + # see bastion.conf.dist for the full list. + {name => 'ttyrecDirectPathFormat', default => '', validre => qr{^([a-zA-Z0-9%&#_./-]*)$}, emptyok => 1}, + {name => 'ttyrecViaPathFormat', default => '', validre => qr{^([a-zA-Z0-9%&#_./-]*)$}, emptyok => 1}, + {name => 'accountExpiredMessage', default => '', validre => qr/^(.*)$/, emptyok => 1}, {name => 'fanciness', default => 'full', validre => qr/^((none|boomer)|(basic|millenial)|(full|genz))$/}, {name => 'accountExternalValidationProgram', default => '', validre => qr'^([a-zA-Z0-9/$_.-]*)$', emptyok => 1}, {name => 'ttyrecStealthStdoutPattern', default => '', validre => qr'^(.{0,4096})$', emptyok => 1}, diff --git a/tests/unit/tests/base.t b/tests/unit/tests/base.t index 834bd74..65061ef 100644 --- a/tests/unit/tests/base.t +++ b/tests/unit/tests/base.t @@ -174,7 +174,7 @@ $fnret = OVH::Bastion::build_ttyrec_cmdline_part1of2( cmp_deeply( $fnret->value->{'saveFile'}, re( - qr{^\Q/home/randomuser/ttyrec/via-10.0.0.1-192.168.1.100/20\E\d\d-\d\d-\d\d.\d\d\-\d\d\-\d\d\.\d{6}\Q.cafed00dcafe.bastionuser.targetuser.192.168.1.100.22.via.jumpi.10.0.0.1.2222.ttyrec\E$} + qr{^\Q/home/randomuser/ttyrec/via-10.0.0.1-192.168.1.100/20\E\d\d-\d\d-\d\d.\d\d\-\d\d\-\d\d\.\d{6}\Q.cafed00dcafe.bastionuser.targetuser.192.168.1.100.22.ttyrec\E$} ), "build_ttyrec_cmdline_part1of2 with proxy saveFile" ); @@ -185,7 +185,7 @@ cmp_deeply( '-f', $fnret->value->{'saveFile'}, '-F', - '/home/randomuser/ttyrec/via-10.0.0.1-192.168.1.100/%Y-%m-%d.%H-%M-%S.#usec#.cafed00dcafe.bastionuser.targetuser.192.168.1.100.22.via.jumpi.10.0.0.1.2222.ttyrec' + '/home/randomuser/ttyrec/via-10.0.0.1-192.168.1.100/%Y-%m-%d.%H-%M-%S.#usec#.cafed00dcafe.bastionuser.targetuser.192.168.1.100.22.ttyrec' ], "build_ttyrec_cmdline_part1of2 with proxy cmd" ); @@ -205,7 +205,7 @@ $fnret = OVH::Bastion::build_ttyrec_cmdline_part1of2( cmp_deeply( $fnret->value->{'saveFile'}, re( - qr{^\Q/home/randomuser/ttyrec/via-v6[2001.db8..1]-192.168.1.200/20\E\d\d-\d\d-\d\d.\d\d\-\d\d\-\d\d\.\d{6}\Q.cafed00dcafe.bastionuser.targetuser.192.168.1.200.22.via.jumpi.v6[2001.db8..1].22.ttyrec\E$} + qr{^\Q/home/randomuser/ttyrec/via-v6[2001.db8..1]-192.168.1.200/20\E\d\d-\d\d-\d\d.\d\d\-\d\d\-\d\d\.\d{6}\Q.cafed00dcafe.bastionuser.targetuser.192.168.1.200.22.ttyrec\E$} ), "build_ttyrec_cmdline_part1of2 with IPv6 proxy saveFile" ); @@ -216,7 +216,7 @@ cmp_deeply( '-f', $fnret->value->{'saveFile'}, '-F', - '/home/randomuser/ttyrec/via-v6[2001.db8..1]-192.168.1.200/%Y-%m-%d.%H-%M-%S.#usec#.cafed00dcafe.bastionuser.targetuser.192.168.1.200.22.via.jumpi.v6[2001.db8..1].22.ttyrec' + '/home/randomuser/ttyrec/via-v6[2001.db8..1]-192.168.1.200/%Y-%m-%d.%H-%M-%S.#usec#.cafed00dcafe.bastionuser.targetuser.192.168.1.200.22.ttyrec' ], "build_ttyrec_cmdline_part1of2 with IPv6 proxy cmd" ); @@ -486,4 +486,187 @@ is( "build_re_from_wildcards() 2" ); +# ttyrecDirectPathFormat / ttyrecViaPathFormat + +# a custom direct path format: full path with &home, &account, &ip, &port and a filename +OVH::Bastion::load_configuration( + mock_data => { + bastionName => 'mock', + ttyrecDirectPathFormat => '&home/rec/&account/&ip-&port/%Y.#usec#.&uniqid.&user.ttyrec', + } +); +$fnret = OVH::Bastion::build_ttyrec_cmdline_part1of2( + ip => "203.0.113.5", + port => 2222, + user => "alice", + account => "bob", + uniqid => 'deadbeef', + home => "/home/bob", +); +cmp_deeply( + $fnret->value->{'saveFile'}, + re(qr{^\Q/home/bob/rec/bob/203.0.113.5-2222/20\E\d\d\Q.\E\d{6}\Q.deadbeef.alice.ttyrec\E$}), + "ttyrecDirectPathFormat: custom path saveFile" +); +is( + $fnret->value->{'cmd'}[4], + '/home/bob/rec/bob/203.0.113.5-2222/%Y.#usec#.deadbeef.alice.ttyrec', + "ttyrecDirectPathFormat: custom path -F format" +); + +# a custom via path format: proxy tokens are available, and only the via format is consulted for proxy connections +OVH::Bastion::load_configuration( + mock_data => { + bastionName => 'mock', + ttyrecViaPathFormat => '&home/rec/via-&proxyuser-at-&proxyip-&proxyport/&ip/%Y.#usec#.&uniqid.ttyrec', + } +); +$fnret = OVH::Bastion::build_ttyrec_cmdline_part1of2( + ip => "203.0.113.5", + port => 22, + user => "alice", + account => "bob", + uniqid => 'deadbeef', + home => "/home/bob", + proxyIp => "198.51.100.9", + proxyPort => 2222, + proxyUser => "jump", +); +is( + $fnret->value->{'cmd'}[4], + '/home/bob/rec/via-jump-at-198.51.100.9-2222/203.0.113.5/%Y.#usec#.deadbeef.ttyrec', + "ttyrecViaPathFormat: custom path with proxy tokens" +); + +# realm account, fallback layout: the &remoteaccount subfolder is inserted +OVH::Bastion::load_configuration(mock_data => {bastionName => 'mock'}); +$fnret = OVH::Bastion::build_ttyrec_cmdline_part1of2( + ip => "203.0.113.5", + port => 22, + user => "alice", + account => "realm_foo", + uniqid => 'deadbeef', + home => "/home/realm_foo", + realm => "foo", + remoteaccount => "bob", +); +cmp_deeply( + $fnret->value->{'saveFile'}, + re(qr{^\Q/home/realm_foo/ttyrec/bob/203.0.113.5/20\E\d\d.*\Q.deadbeef.realm_foo.alice.203.0.113.5.22.ttyrec\E$}), + "fallback realm account: &remoteaccount subfolder is present" +); + +# a path format that tries to climb out of the tree must be refused +OVH::Bastion::load_configuration( + mock_data => {bastionName => 'mock', ttyrecDirectPathFormat => '&home/../../etc/&ip/x'}); +$fnret = OVH::Bastion::build_ttyrec_cmdline_part1of2( + ip => "1.2.3.4", + port => 22, + user => "u", + account => "a", + uniqid => 'x', + home => "/home/u", +); +is($fnret->err, 'ERR_SECURITY_VIOLATION', "ttyrec path with '..' is refused"); + +# full matrix: {only Direct, only Via, both, neither} x {direct conn, via conn} +# each of ttyrecDirectPathFormat and ttyrecViaPathFormat falls back independently to the legacy +# layout (built from ttyrecFilenameFormat) when its own value is empty. We verify every cell. + +# returns the ttyrec '-F' format (i.e. the resolved path template, before strftime expansion) +sub _ttyrec_F { + my %extra = @_; + my $r = OVH::Bastion::build_ttyrec_cmdline_part1of2( + ip => "203.0.113.5", + port => 22, + user => "alice", + account => "bob", + uniqid => 'deadbeef', + home => "/home/bob", + %extra, + ); + return $r ? $r->value->{'cmd'}[4] : "ERR:" . $r->err; +} + +my $DIRECT_FMT = '&home/D/&account/&ip-&port/%Y.#usec#.&uniqid.&user.ttyrec'; +my $VIA_FMT = '&home/V/&proxyuser-&proxyip-&proxyport/&ip/%Y.#usec#.&uniqid.ttyrec'; +my %VIA_CONN = (proxyIp => "198.51.100.9", proxyPort => 2222, proxyUser => "jump"); + +my $DIRECT_CUSTOM = '/home/bob/D/bob/203.0.113.5-22/%Y.#usec#.deadbeef.alice.ttyrec'; +my $VIA_CUSTOM = '/home/bob/V/jump-198.51.100.9-2222/203.0.113.5/%Y.#usec#.deadbeef.ttyrec'; +my $DIRECT_FALLBACK = '/home/bob/ttyrec/203.0.113.5/%Y-%m-%d.%H-%M-%S.#usec#.deadbeef.bob.alice.203.0.113.5.22.ttyrec'; +my $VIA_FALLBACK = + '/home/bob/ttyrec/via-198.51.100.9-203.0.113.5/%Y-%m-%d.%H-%M-%S.#usec#.deadbeef.bob.alice.203.0.113.5.22.ttyrec'; + +# only ttyrecDirectPathFormat defined +OVH::Bastion::load_configuration(mock_data => {bastionName => 'mock', ttyrecDirectPathFormat => $DIRECT_FMT}); +is(_ttyrec_F(), $DIRECT_CUSTOM, "only Direct defined: direct conn uses ttyrecDirectPathFormat"); +is(_ttyrec_F(%VIA_CONN), $VIA_FALLBACK, "only Direct defined: via conn falls back (ttyrecViaPathFormat empty)"); + +# only ttyrecViaPathFormat defined +OVH::Bastion::load_configuration(mock_data => {bastionName => 'mock', ttyrecViaPathFormat => $VIA_FMT}); +is(_ttyrec_F(), $DIRECT_FALLBACK, "only Via defined: direct conn falls back (ttyrecDirectPathFormat empty)"); +is(_ttyrec_F(%VIA_CONN), $VIA_CUSTOM, "only Via defined: via conn uses ttyrecViaPathFormat"); + +# both defined +OVH::Bastion::load_configuration( + mock_data => {bastionName => 'mock', ttyrecDirectPathFormat => $DIRECT_FMT, ttyrecViaPathFormat => $VIA_FMT}); +is(_ttyrec_F(), $DIRECT_CUSTOM, "both defined: direct conn uses ttyrecDirectPathFormat"); +is(_ttyrec_F(%VIA_CONN), $VIA_CUSTOM, "both defined: via conn uses ttyrecViaPathFormat"); + +# neither defined (fallback for both) +OVH::Bastion::load_configuration(mock_data => {bastionName => 'mock'}); +is(_ttyrec_F(), $DIRECT_FALLBACK, "neither defined: direct conn uses fallback layout"); +is(_ttyrec_F(%VIA_CONN), $VIA_FALLBACK, "neither defined: via conn uses fallback layout"); + +# &home and &remoteaccount are usable in ttyrecFilenameFormat too: the substitution is applied to +# the whole resolved path, so these tokens are not restricted to the new path-format options. +OVH::Bastion::load_configuration( + mock_data => {bastionName => 'mock', ttyrecFilenameFormat => '&uniqid.&remoteaccount.&user.&ip.&port.ttyrec'}); +is( + _ttyrec_F(account => "realm_foo", home => "/home/realm_foo", realm => "foo", remoteaccount => "bob"), + '/home/realm_foo/ttyrec/bob/203.0.113.5/deadbeef.bob.alice.203.0.113.5.22.ttyrec', + "&remoteaccount is usable inside ttyrecFilenameFormat" +); +OVH::Bastion::load_configuration(mock_data => {bastionName => 'mock', ttyrecFilenameFormat => '&home.&uniqid.ttyrec'}); +is(_ttyrec_F(), '/home/bob/ttyrec/203.0.113.5/home/bob.deadbeef.ttyrec', "&home is usable inside ttyrecFilenameFormat"); + +# pre-proxyjump directory-layout regression: the &remoteaccount subfolder must be inserted IFF both 'realm' +# and 'remoteaccount' are passed (matching the pre-proxyjump 'if ($realm && $remoteaccount)' behavior). +OVH::Bastion::load_configuration(mock_data => {bastionName => 'mock'}); +my $WITH_REMACCT = + '/home/bob/ttyrec/remacct/203.0.113.5/%Y-%m-%d.%H-%M-%S.#usec#.deadbeef.bob.alice.203.0.113.5.22.ttyrec'; +is(_ttyrec_F(), $DIRECT_FALLBACK, "pre-proxyjump layout: no realm + no remoteaccount => no subfolder"); +is(_ttyrec_F(realm => "myrealm", remoteaccount => "remacct"), + $WITH_REMACCT, "pre-proxyjump layout: realm + remoteaccount => remoteaccount subfolder"); +is(_ttyrec_F(realm => "myrealm"), $DIRECT_FALLBACK, + "pre-proxyjump layout: realm without remoteaccount => no subfolder"); +is(_ttyrec_F(remoteaccount => "remacct"), + $DIRECT_FALLBACK, "pre-proxyjump layout: remoteaccount without realm => no subfolder"); + +# Default vanilla install: ttyrecFilenameFormat at its shipped default, and both path-format options +# unset (''). This is the most common configuration, so its behavior must be identical before and +# after this PR. First pin those default values (mock-defaulting == a freshly copied bastion.conf): +OVH::Bastion::load_configuration(mock_data => {bastionName => 'mock'}); +my $DEFAULT_FNAME = '%Y-%m-%d.%H-%M-%S.#usec#.&uniqid.&account.&user.&ip.&port.ttyrec'; +is(OVH::Bastion::config('ttyrecFilenameFormat')->value, $DEFAULT_FNAME, "vanilla: ttyrecFilenameFormat default value"); +is(OVH::Bastion::config('ttyrecDirectPathFormat')->value, '', "vanilla: ttyrecDirectPathFormat defaults to ''"); +is(OVH::Bastion::config('ttyrecViaPathFormat')->value, '', "vanilla: ttyrecViaPathFormat defaults to ''"); + +# Same, but with all three options set EXPLICITLY to their vanilla values (as a copied +# bastion.conf.dist would have them): the resolved paths must match the pre-proxyjump layout, i.e. be +# identical to omitting the path-format options entirely. +OVH::Bastion::load_configuration( + mock_data => { + bastionName => 'mock', + ttyrecFilenameFormat => $DEFAULT_FNAME, + ttyrecDirectPathFormat => '', + ttyrecViaPathFormat => '', + } +); +is(_ttyrec_F(), $DIRECT_FALLBACK, "vanilla (explicit defaults): direct conn => pre-proxyjump layout"); +is(_ttyrec_F(%VIA_CONN), $VIA_FALLBACK, "vanilla (explicit defaults): via conn => pre-proxyjump layout"); +is(_ttyrec_F(realm => "myrealm", remoteaccount => "remacct"), + $WITH_REMACCT, "vanilla (explicit defaults): realm+remoteaccount => pre-proxyjump layout"); + done_testing();