File Coverage

blib/lib/Connector/Multi.pm
Criterion Covered Total %
statement 148 161 91.9
branch 37 48 77.0
condition 6 8 75.0
subroutine 23 24 95.8
pod 11 12 91.6
total 225 253 88.9


line stmt bran cond sub pod time code
1             # Connector::Multi
2             #
3             # Connector class capable of dealing with multiple personalities.
4             #
5             # Written by Scott Hardin and Martin Bartosch for the OpenXPKI project 2012
6             #
7              
8             use strict;
9 7     7   2406278 use warnings;
  7         21  
  7         220  
10 7     7   36 use English;
  7         11  
  7         181  
11 7     7   30 use Moose;
  7         14  
  7         45  
12 7     7   2390 use Connector::Wrapper;
  7         12  
  7         57  
13 7     7   42554  
  7         20  
  7         10637  
14             extends 'Connector';
15              
16             has 'BASECONNECTOR' => ( is => 'ro', required => 1 );
17              
18             has '+LOCATION' => ( required => 0 );
19              
20             has '_cache' => ( is => 'rw', required => 0, isa => 'HashRef', builder => '_init_cache' );
21              
22             my $self = shift;
23              
24 10     10   447 $self->_cache( { 'node' => {} } );
25             }
26 10         265  
27             my $self = shift;
28              
29             # Our config is merely a hash of connector instances
30 10     10   89 my $config = {};
31             my $baseconn = $self->BASECONNECTOR();
32             my $baseref;
33 10         471  
34 10         231 if ( ref($baseconn) ) { # if it's a ref, assume that it's a Connector
35 10         47 $baseref = $baseconn;
36             } else {
37 9 100       52 eval "use $baseconn;1" or die "Error use'ing $baseconn: $@";
38 8         23 $baseref = $baseconn->new({ LOCATION => $self->LOCATION() });
39             }
40 2 50   1   113 $config->{''} = $baseref;
  1         12  
  1         4  
  1         26  
41 2         37 $self->_config($config);
42             }
43 9         37  
44 9         256 # Proxy calls
45             my $self = shift;
46             unshift @_, 'get';
47             return $self->_route_call( @_ );
48             }
49 103     103 1 4831  
50 103         242 my $self = shift;
51 103         426 unshift @_, 'get_list';
52              
53             return $self->_route_call( @_ );
54             }
55 4     5 1 6  
56 4         9 my $self = shift;
57             unshift @_, 'get_size';
58 4         9 return $self->_route_call( @_ );
59             }
60              
61             my $self = shift;
62 0     1 1 0 my @args = @_;
63 0         0 unshift @_, 'get_hash';
64 0         0 my $hash = $self->_route_call( @_ );
65             return $hash unless (ref $hash); # undef
66              
67             # This assumes that all connectors that can handle references
68 5     5 1 1041 # use the symlink syntax introduced with Config::Versioned!
69 5         16 my @path;
70 5         13 foreach my $key (keys %{$hash}) {
71 5         16 # Connector in leaf - resolv it!
72 5 100       22 if (ref $hash->{$key} eq 'SCALAR') {
73             @path = $self->_build_path( $args[0] ) unless(@path);
74             $hash->{$key} = $self->get( [ @path , $key ] );
75             }
76 4         89 }
77 4         13 return $hash;
  4         16  
78             }
79 7 100       23  
80 2 50       16 my $self = shift;
81 2         12 unshift @_, 'get_keys';
82              
83             return $self->_route_call( @_ );
84 4         29 }
85              
86             my $self = shift;
87             unshift @_, 'set';
88 6     6 1 261 return $self->_route_call( @_ );
89 6         38 }
90              
91 6         40 my $self = shift;
92             unshift @_, 'get_meta';
93             return $self->_route_call( @_ );
94             }
95 4     4 1 10  
96 4         19 my $self = shift;
97 4         14 unshift @_, 'exists';
98             return $self->_route_call( @_ );
99             }
100              
101 112     112 1 191 my $self = shift;
102 112         262 foreach my $cache_id (keys %{$self->_config()}) {
103 112         323 # do not cleanup the base connector
104             next unless ($cache_id);
105             eval {
106             $self->_config()->{$cache_id}->cleanup();
107 7     7 1 1448 $self->log()->debug("Cleanup ok on $cache_id");
108 7         23 };
109 7         40 delete $self->_config()->{$cache_id};
110             $self->log()->warn("Error on cleanup in $cache_id: $EVAL_ERROR") if ($EVAL_ERROR);
111             }
112             }
113 1     1 1 2  
114 1         2  
  1         26  
115             my $self = shift;
116 8 100       18 my $call = shift;
117 7         9 my $location = shift;
118 7         126 my @args = @_;
119 7         122  
120             my $delim = $self->DELIMITER();
121 7         158  
122 7 50       18 my $conn = $self->_config()->{''};
123              
124             if ( ! $conn ) {
125             die "ERR: no default connector for Connector::Multi";
126             }
127              
128 240     240   461 my @prefix = ();
129 240         796 my @suffix = $self->_build_path_with_prefix( $location );
130 240         467 my $ptr_cache = $self->_cache()->{node};
131 240         446  
132             $self->log()->debug('Call '.$call.' in Multi to '. join('.', @suffix));
133 240         5547  
134             while ( @suffix > 0 ) {
135 240         4672 my $node = shift @suffix;
136             push @prefix, $node;
137 240 50       687  
138 0         0 # Easy Cache - skip all inner nodes, that are not a connector
139             # that might fail if you mix real path and complex path items
140             my $path = join($delim, @prefix);
141 240         441 if (exists $ptr_cache->{$path}) {
142 240         841 next;
143 240         6187 }
144              
145 240         4350 my $meta = $conn->get_meta($path);
146              
147 240         1819 if ( $meta && $meta->{TYPE} eq 'reference' ) {
148 772         1102 if ( $meta->{VALUE} =~ m/^([^:]+):(.+)$/ ) {
149 772         1101 my $schema = $1;
150             my $target = $2;
151             if ( $schema eq 'connector' ) {
152             $conn = $self->get_connector($target);
153 772         1353 if ( ! $conn ) {
154 772 100       2053 $self->_log_and_die("Connector::Multi: error creating connector for '$target': $@");
155 508         978 }
156             $self->log()->debug("Dispatch to connector at $target");
157             # Push path on top of the argument array
158 264         926 unshift @args, \@suffix;
159             return $conn->$call( @args );
160 264 100 100     1685 } elsif ( $schema eq 'env' ) {
    100 100        
161 51 100       354  
162 43         115 $self->log()->debug("Fetch from ENV with key $target");
163 43         108 # warn if the path is not empty
164 43 100       168 $self->log()->warn(sprintf("Call redirected to ENV but path is not final (%s)!", join(".",@suffix))) if (@suffix > 0);
    50          
165 40         171 if (!exists $ENV{$target}) {
166 40 50       111 return $self->_node_not_exists();
167 0         0 }
168             return $ENV{$target};
169 40         765  
170             } else {
171 40         369 $self->_log_and_die("Connector::Multi: unsupported schema for symlink: $schema");
172 40         397 }
173             } else {
174             # redirect
175 3         67 my @target = split(/[$delim]/, $meta->{VALUE});
176             # relative path - shift one item from prefix for each dot
177 3 100       57 if ($target[0] eq '') {
178 3 50       691 $self->log()->debug("Relative redirect at prefix " . join ".", @prefix);
179 0         0 while ($target[0] eq '') {
180             $self->_log_and_die("Relative path length exceeds prefix length") unless (scalar @prefix);
181 3         38 pop @prefix;
182             shift @target;
183             }
184 0         0 } else {
185             $self->log()->debug(sprintf("Plain redirect at prefix %s to %s", join(".", @prefix), $meta->{VALUE}));
186             @prefix = ();
187             }
188 8         126 unshift @suffix, @target;
189             $self->log()->debug("Final redirect target " . join ".", @suffix);
190 8 100       31 unshift @args, [ @prefix, @suffix ];
191 3         148 return $self->$call( @args );
192 3         27 }
193 8 100       26 } elsif ( $meta && $meta->{TYPE} eq 'connector' ) {
194 7         11  
195 7         15 my $conn = $meta->{VALUE};
196             $self->log()->debug("Got conncetor reference of type ". ref $conn);
197             $self->log()->debug("Dispatch to connector at " . join(".", @prefix));
198 5         125 # Push path on top of the argument array
199 5         61 unshift @args, \@suffix;
200             return $conn->$call( @args );
201 7         27  
202 7         171 } else {
203 7         54 $ptr_cache->{$path} = 1;
204 7         34 }
205             }
206              
207             # Push path on top of the argument array
208 2         4 unshift @args, [ @prefix, @suffix ];
209 2         46 return $conn->$call( @args );
210 2         47 }
211              
212 2         12 my $self = shift;
213 2         7 my $location = shift;
214             return Connector::Wrapper->new({ BASECONNECTOR => $self, TARGET => $location });
215             }
216 211         1165  
217             # getWrapper() is deprecated - use get_wrapper() instead
218             my $self = shift;
219             warn "using deprecated call to getWrapper - use get_wrapper instead";
220             $self->get_wrapper(@_);
221 187         594 }
222 187         767  
223             my $self = shift;
224             my $target = shift;
225              
226 7     7 1 15 # the cache needs to store the absolute path including the prefix
227 7         11 my @path = $self->_build_path( $target );
228 7         164 my $cache_id = join($self->DELIMITER(), $self->_build_path_with_prefix( \@path ));
229             my $conn = $self->_config()->{$cache_id};
230             if ( ! $conn ) {
231             # Note - we will use ourselves to read the connectors instance information
232             # this allows to put other connectors inside a connector definition but
233 0     0 0 0 # also lets connector definition paths depend on PREFIX!
234 0         0 my $class = $self->get( [ @path, 'class' ] );
235 0         0 if (!$class) {
236             my $prefix = $self->_get_prefix() || '-';
237             $self->_log_and_die("Nested connector without class ($target/$prefix)");
238             }
239 44     44 1 76 $self->log()->debug("Initialize connector $class at $target");
240 44         91 eval "use $class;1" or $self->_log_and_die("Error use'ing $class: $@");
241             $conn = $class->new( { CONNECTOR => $self, TARGET => $target } );
242             $self->_config()->{$cache_id} = $conn;
243 44         180 $self->log()->trace("Add connector to cache: $cache_id") if ($self->log()->is_trace());
244 44         1040 } elsif ($self->log()->is_trace()) {
245 44         1078 $self->log()->trace("Got connector for $target from cache $cache_id");
246 44 100       655 }
    50          
247             return $conn;
248             }
249              
250 14         89 no Moose;
251 14 50       71 __PACKAGE__->meta->make_immutable;
252 0   0     0  
253 0         0 1;
254              
255 14         324 =head1 NAME
256 4 50   4   1995  
  4     3   24  
  4         81  
  3         24  
  3         6  
  3         72  
  14         1223  
257 14         495 Connector::Multi
258 14         343  
259 14 50       286 =head1 DESCRIPTION
260              
261 0         0 This class implements a Connector that is capable of dealing with dynamically
262             configured Connector implementations and symlinks.
263 44         505  
264             The underlying concept is that there is a primary (i.e.: boot) configuration
265             source that Multi accesses for get() requests. If the request returns a reference
266 7     7   69 to a SCALAR, Multi interprets this as a symbolic link. The content of the
  7         12  
  7         54  
267             link contains an alias and a target key.
268              
269             =head1 Examples
270              
271             =head2 Connector References
272              
273             In this example, we will be using a YAML configuration file that is accessed
274             via the connector Connector::Proxy::YAML.
275              
276             From the programmer's view, the configuration should look something like this:
277              
278             smartcards:
279             tokens:
280             token_1:
281             status: ACTIVATED
282             token_2:
283             status: DEACTIVATED
284             owners:
285             joe:
286             tokenid: token_1
287             bob:
288             tokenid: token_2
289              
290             In the above example, calling get('smartcards.tokens.token_1.status') returns
291             the string 'ACTIVATED'.
292              
293             To have the data fetched from an LDAP server, we can redirect the
294             'smartcards.tokens' key to the LDAP connector using '@' to indicate symlinks.
295             Our primary configuration source for both tokens and owners would contain
296             the following entries:
297              
298             smartcards:
299             tokens@: connector:connectors.ldap-query-token
300             owners@: connector:connectors.ldap-query-owners
301              
302             With the symlink now in the key, Multi must walk down each level itself and
303             handle the symlink. When 'smartcards.tokens' is reached, it reads the contents
304             of the symlink, which is an alias to a connector 'ldap-query-token'. The
305             connector configuration is in the 'connectors' namespace of our primary data source.
306              
307             connectors:
308             ldap-query-tokens:
309             class: Connector::Proxy::Net::LDAP
310             basedn: ou=smartcards,dc=example,dc=org
311             uri: ldaps://example.org
312             bind_dn: uid=user,ou=Directory Users,dc=example,dc=org
313             password: secret
314              
315             connectors:
316             ldap-query-owners:
317             class: Connector::Proxy::Net::LDAP
318             basedn: ou=people,dc=example,dc=org
319             uri: ldaps://example.org
320             bind_dn: uid=user,ou=Directory Users,dc=example,dc=org
321             password: secret
322              
323              
324             =head2 Builtin Environment Connector
325              
326             Similar to connector you can define a redirect to read a value from the
327             environment.
328              
329             node1:
330             key@: env:OPENPKI_KEY_FROM_ENV
331              
332             calling get('node1.key') will return the value of the environment variable
333             `OPENPKI_KEY_FROM_ENV`.
334              
335             If the environment variable is not set, undef is returned. Walking over such a
336             node raises a warning but will silently swallow the remaining path components
337             and return the value of the node.
338              
339             =head2 Inline Redirects
340              
341             It is also possible to reference other parts of the configuration using a
342             kind of redirect/symlink.
343              
344             node1:
345             node2:
346             key@: shared.key1
347              
348             shared:
349             key1: secret
350              
351             The '@' sign indicates a symlink similar to the example given above but
352             there is no additional keyword in front of the value and the remainder of
353             the line is treated as an absolute path to read the value from.
354              
355             If the path value starts with the path separator (default 'dot'), then the
356             path is treated as a relative link and each dot means "one level up".
357              
358             node1:
359             node2:
360             key2@: ..node2a.key
361              
362             node2a:
363             key1@: .key
364             key: secret
365              
366             =head1 SYNOPSIS
367              
368             The parameter BASECONNECTOR may either be a class instance or
369             the name of the class, in which case the additional arguments
370             (e.g.: LOCATION) are passed to the base connector.
371              
372             use Connector::Proxy::Config::Versioned;
373             use Connector::Multi;
374              
375             my $base = Connector::Proxy::Config::Versioned->new({
376             LOCATION => $path_to_internal_config_git_repo,
377             });
378              
379             my $multi = Connector::Multi->new( {
380             BASECONNECTOR => $base,
381             });
382              
383             my $tok = $multi->get('smartcard.owners.bob.tokenid');
384              
385             or...
386              
387             use Connector::Multi;
388              
389             my $multi = Connector::Multi->new( {
390             BASECONNECTOR => 'Connector::Proxy::Config::Versioned',
391             LOCATION => $path_to_internal_config_git_repo,
392             });
393              
394             my $tok = $multi->get('smartcard.owners.bob.tokenid');
395              
396             You can also pass the path as an arrayref, where each element can be a path itself
397              
398             my $tok = $multi->get( [ 'smartcard.owners', 'bob.tokenid' ]);
399              
400             *Preset Connector References*
401              
402             If you create your config inside your code you and have a baseconnector that
403             can handle object references (e.g. Connector::Builtin::Memory), you can
404             directly set the value of a node to a blessed reference of a Connector class.
405              
406             my $sub = Connector::Proxy::Net::LDAP->new( {
407             basedn => "ou=smartcards,dc=example,dc=org"
408             });
409              
410             $base->set('smartcard.tokens', $sub )
411              
412             =head1 OPTIONS
413              
414             When creating a new instance, the C<new()> constructor accepts the
415             following options:
416              
417             =over 8
418              
419             =item BASECONNECTOR
420              
421             This is a reference to the Connector instance that Connector::Multi
422             uses at the base of all get() requests.
423              
424             =item PREFIX
425              
426             You can set a PREFIX that is prepended to all path. There is one important
427             caveat to mention: Any redirects made are relative to the prefix set so you can
428             use PREFIX only if the configuration was prepared to work with it (e.g. to split
429             differnet domains and switch between them using a PREFIX).
430              
431             Example:
432              
433             branch:
434             foo@: connector:foobar
435              
436             foobar:
437             class: ....
438              
439             Without a PREFIX set, this will return "undef" as the connector is not defined
440             at "foobar".
441              
442             my $bar = $multi->get( [ 'branch', 'foo', 'bar' ]);
443              
444             This will work and return the result from the connector call using "bar" as key:
445              
446             my $multi = Connector::Multi->new( {
447             BASECONNECTOR => $base,
448             PREFIX => "branch",
449             });
450             my $bar = $multi->get( [ 'branch', 'foo', 'bar' ]);
451              
452             Note: It is B<DANGEROUS> to use a dynamic PREFIX in the BASECONNECTOR as
453             Connector::Multi stores created sub-connectors in a cache using the path as key.
454             It is possible to change the prefix of the class itself during runtime.
455              
456             =back
457              
458             =head1 Supported methods
459              
460             =head2 get, get_list, get_size, get_hash, get_keys, set, get_meta
461             Those are routed to the appropriate connector.
462              
463             =head2 get_connector
464             Return the instance of the connector at this node
465              
466             =head2 get_wrapper
467             Return a wrapper around this node. This is like setting a prefix for all
468             subsequent queries.
469              
470             my $wrapper = $conn->get_wrapper('test.node');
471             $val = $wrapper->get('foo');
472              
473             Is the same as
474             $val = $conn->get_wrapper('test.node.foo');