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