source: trunk/LATMOS-Accounts/lib/LATMOS/Accounts/Bases/Sql.pm @ 1843

Last change on this file since 1843 was 1843, checked in by nanardon, 8 years ago

Automatically register attributes a db start

  • Property svn:keywords set to Id Rev
File size: 18.3 KB
Line 
1package LATMOS::Accounts::Bases::Sql;
2
3use 5.010000;
4use strict;
5use warnings;
6
7use base qw(LATMOS::Accounts::Bases);
8use LATMOS::Accounts::Log;
9use LATMOS::Accounts::Bases::Sql::DataRequest;
10use DBI;
11use Crypt::RSA;
12use Crypt::RSA::Key::Public::SSH;
13use Crypt::RSA::Key::Private::SSH;
14use Crypt::RSA::Key::Public;
15use Crypt::RSA::Key::Private;
16use MIME::Base64;
17
18our $VERSION = (q$Rev$ =~ /^Rev: (\d+) /)[0];
19
20sub SCHEMA_VERSION { 11 };
21
22=head1 NAME
23
24LATMOS::Ad - Perl extension for blah blah blah
25
26=head1 SYNOPSIS
27
28  use LATMOS::Accounts::Bases;
29  my $base = LATMOS::Accounts::Bases->new('unix');
30  ...
31
32=head1 DESCRIPTION
33
34Account base access over standard unix file format.
35
36=head1 FUNCTIONS
37
38=cut
39
40=head2 SCHEMA_VERSION
41
42Return the SQL schema version to use for this software version.
43
44=head2 new(%config)
45
46Create a new LATMOS::Ad object for windows AD $domain.
47
48domain / server: either the Ad domain or directly the server
49
50ldap_args is an optionnal list of arguments to pass to L<Net::LDAP>.
51
52=cut
53
54sub new {
55    my ($class, %config) = @_;
56   
57    my $base = {
58        db_conn => $config{db_conn},
59    };
60
61    bless($base, $class);
62}
63
64sub DESTROY {
65    my ($self) = @_;
66    $self->{_db} && $self->{_db}->rollback;
67}
68
69=head2 db
70
71Return a L<DBI> handle over database, load it if need.
72
73=cut
74
75sub db {
76    my ($self) = @_;
77
78    if ($self->{_db} && $self->{_db}->ping) {
79        return $self->{_db};
80    } else {
81        $self->{_db} = DBI->connect_cached(
82            'dbi:Pg:' . $self->{db_conn},
83            undef, undef,
84            {
85                RaiseError => 0,
86                AutoCommit => 0,
87                PrintWarn => 1,
88                PrintError => 1,
89                ($self->config('no_pg_utf8') ? (pg_enable_utf8 => 0) : ()),
90            }
91        ) or do {
92            $self->log(LA_ERR, "Cannot connect to database: %s", $DBI::errstr);   
93            return;
94        };
95        $self->{_db}->do(q(SET SESSION CHARACTERISTICS AS TRANSACTION
96                    ISOLATION LEVEL SERIALIZABLE));
97        $self->{_db}->do(q{set DATESTYLE to 'DMY'});
98        $self->log(LA_DEBUG, 'New connection to DB');
99
100        foreach my $otype ($self->list_supported_objects) {
101            foreach my $attribute ($self->list_canonical_fields($otype, 'r')) {
102                my $attr = $self->attribute($otype, $attribute);
103                $attr->{inline} and next;
104                $attr->{managed} and next;
105
106                if ($self->is_registered_attribute($otype, $attribute)) {
107                } else {
108                    if($self->register_attribute($otype, $attribute, $attr->{comment})) {
109                        $self->log(LA_NOTICE, "Attr. $attribute for object type $otype registred");
110                    } else {
111                        $self->log(LA_ERR, "Can't register attribute $attribute");
112                        $self->{_db}->rollback;
113                        return;
114                    }
115                }
116            }
117        }
118
119        $self->{_db}->commit;
120        return $self->{_db};
121    }
122}
123
124=head2 load
125
126Read file and load data into memory
127
128=cut
129
130sub load {
131    my ($self) = @_;
132    if (!$self->db) { return 0 };
133
134    my $sv = $self->get_global_value('schema_version') || 1;
135    if ($sv < SCHEMA_VERSION) {
136        $self->log(LA_CRIT,
137            "Schema version %d found, %d is need, please update db using " .
138            "`la-sql-upgrade' tool for `%s' base",
139            $sv,
140            SCHEMA_VERSION,
141            $self->label,
142        );
143        # return;
144    }
145
146    1;
147}
148
149=head2 getObjectFromOKey ($okey)
150
151Return the object from the db internal key
152
153=cut
154
155sub getObjectFromOKey {
156    my ($self, $okey) = @_;
157
158    my $findobj = $self->{_db}->prepare_cached(q{
159        select * from objects_table where ikey = ?
160    });
161
162    $findobj->execute($okey);
163
164    my $res = $findobj->fetchrow_hashref;
165    $findobj->finish;
166
167    if ($res) {
168        return $self->get_object($res->{relname}, $res->{name});
169    } else {
170        return;
171    }
172}
173
174sub _sync_dyn_group {
175    my ($self) = @_;
176
177    my @groups = $self->search_objects('group', 'autoMemberFilter=*');
178
179    my $res = 0;
180    foreach (@groups) {
181        my $g = $self->get_object('group', $_) or next;
182        $res += $g->populate_dyn_group;
183    }
184
185    $self->log(LA_DEBUG, "Group Dyn res %d", $res);
186    $res
187}
188
189sub _sync_dyn_aliases {
190    my ($self) = @_;
191
192    my @groups = $self->search_objects('aliases', 'autoMemberFilter=*');
193
194    my $res = 0;
195    foreach (@groups) {
196        my $g = $self->get_object('aliases', $_) or next;
197        $res += $g->populate_dyn_aliases;
198    }
199
200    $self->log(LA_DEBUG, "Aliases Dyn res %d", $res);
201    $res
202}
203
204=head2 PopulateDynData
205
206Recomputate dynamics attributes (autoMembersFilters) if need
207
208=cut
209
210sub PopulateDynData {
211    my ($self) = @_;
212
213    foreach (1 .. 5) {
214        $self->log(LA_DEBUG, "%d loop for PopulateDynData", $_);
215        my $res = 0;
216        $res += $self->_sync_dyn_group   || 0;
217        $res += $self->_sync_dyn_aliases || 0;
218
219        if ($res == 0) {
220            last;
221        }
222    }
223
224    return 1;
225}
226
227sub _commit {
228    my ($self) = @_;
229
230    $self->PopulateDynData;
231
232    if ($ENV{LA_NO_COMMIT}) {
233        $self->log(LA_DEBUG, 'DB::COMMIT (ignore due to LA_NO_COMMIT)');
234        return 1;
235    } else {
236        $self->log(LA_DEBUG, 'DB::COMMIT');
237    }
238    $self->{__cache} = undef;
239    $self->db->commit;
240}
241
242sub _rollback {
243    my ($self) = @_;
244    if ($ENV{LA_NO_COMMIT}) {
245        $self->log(LA_DEBUG, 'DB::ROLLBACK (ignore due to LA_NO_COMMIT)');
246        return 1
247    } else {
248        $self->log(LA_DEBUG, 'DB::ROLLBACK');
249    }
250    $self->{__cache} = undef;
251    $self->db->rollback;
252}
253
254sub list_supported_objects {
255    my ($self, @otype) = @_;
256    $self->SUPER::list_supported_objects(qw(site), @otype);
257}
258
259sub current_rev {
260    my ($self) = @_;
261    my $sth = $self->db->prepare_cached(
262        q{select max(rev) from revisions}
263    );
264    $sth->execute;
265    my $res = $sth->fetchrow_hashref;
266    $sth->finish;
267    return ($res->{max});
268} 
269
270sub authenticate_user {
271    my ($self, $username, $passwd) = @_;
272    $username or return;
273    my $uobj = $self->get_object('user', $username) or do {
274        la_log(LA_ERR, "Cannot authenticate non existing user $username");
275        return;
276    };
277
278    if ($self->attribute('user', 'exported')) {
279        if (!$uobj->_get_c_field('exported')) {
280            la_log(LA_ERR, "User $username found but currently unexported");
281            return;
282        }
283    }
284
285    $self->SUPER::authenticate_user($username, $passwd);
286}
287
288=head1 SPECIFICS FUNCTIONS
289
290=head2 get_global_value ($varname)
291
292Return global value set into base
293
294=cut
295
296sub get_global_value {
297    my ($self, $varname) = @_;
298
299    my $sth = $self->db->prepare_cached(q{
300        select val from settings where varname = ?
301        });
302    $sth->execute($varname);
303    my $res = $sth->fetchrow_hashref;
304    $sth->finish;
305    $res->{val}
306}
307
308=head2 set_global_value ($varname, $value)
309
310Set global value.
311
312=cut
313
314sub set_global_value {
315    my ($self, $varname, $value) = @_;
316    my $sth = $self->db->prepare(q{
317        update settings set val = ? where varname = ?
318        });
319    $sth->execute($value, $varname) == 0 and do {
320        my $sth2 = $self->db->prepare(q{
321            insert into settings (val, varname) values (?,?)
322            });
323        $sth2->execute($value, $varname);
324    };
325}
326
327=head2 generate_rsa_key ($password)
328
329Return public and private peer rsa keys
330
331=cut
332
333sub generate_rsa_key {
334    my ($self, $password) = @_;
335
336    my $rsa = new Crypt::RSA ES => 'PKCS1v15';
337    my ($public, $private) = $rsa->keygen (
338        Identity  => 'LATMOS-Accounts',
339        Size      => 2048,
340        Password  => $password,
341        Verbosity => 0,
342    ) or die $rsa->errstr(); # TODO avoid die
343    return ($public, $private);
344}
345
346=head2 private_key ($password)
347
348Load and return private rsa key
349
350=cut
351
352sub private_key {
353    my ($self, $password) = @_;
354    my $base = $self;
355    my $serialize = $base->get_global_value('rsa_private_key') or return;
356    my $string = decode_base64($serialize);
357    my $privkey = $string =~ /^SSH PRIVATE KEY FILE/
358        ? Crypt::RSA::Key::Private::SSH->new
359        : Crypt::RSA::Key::Private->new;
360    $privkey = $privkey->deserialize(
361        String => [ $string ],
362        Password => $password
363    );
364    $privkey->reveal( Password => $password );
365    $privkey;
366}
367
368=head2 get_rsa_password
369
370Return hash with peer username => encryptedPassword
371
372=cut
373
374sub get_rsa_password {
375    my ($self) = @_;
376    my $base = $self;
377    my $sth = $base->db->prepare(q{
378        select "name", value from "user" join user_attributes_base
379        on "user".ikey = user_attributes_base.okey
380        where user_attributes_base.attr = 'encryptedPassword'
381    });
382    $sth->execute;
383    my %users;
384    while (my $res = $sth->fetchrow_hashref) {
385        $users{$res->{name}} = $res->{value};
386    }
387    %users
388}
389
390=head2 store_rsa_key ($public, $private)
391
392Store public and private RSA key info data base
393
394=cut
395
396sub store_rsa_key {
397    my ($self, $public, $private) = @_;
398    my $base = $self;
399    $private->hide;
400    $base->set_global_value('rsa_private_key',
401        encode_base64($private->serialize));
402    $base->set_global_value('rsa_public_key',
403        $public->serialize);
404    return;
405}
406
407=head2 find_next_expire_users ($expire)
408
409Search user expiring in C<$expire> delay
410
411=cut
412
413sub find_next_expire_users {
414    my ($self, $expire) = @_;
415
416    my $sth= $self->db->prepare(q{
417        select name from "user" where
418            expire < now() + ?::interval
419            and expire > now()
420            and expire is not null
421            } . ($self->{wexported} ? '' : 'and exported = true') . q{
422            order by expire
423        }
424    );
425    $sth->execute($expire || '1 month');
426    my @users;
427    while (my $res = $sth->fetchrow_hashref) {
428        push(@users, $res->{name});
429    }
430    @users
431}
432
433=head2 find_expired_users ($expire)
434
435Return list of user going to expires in C<$expire> delay
436
437=cut
438
439sub find_expired_users {
440    my ($self, $expire) = @_;
441
442    my $sth= $self->db->prepare(q{
443        select name from "user" where
444            expire < now() - ?::interval
445            and expire is not null
446        } . ($self->{wexported} ? '' : 'and exported = true') . q{
447            order by expire
448        }
449    );
450    $sth->execute($expire || '1 second');
451    my @users;
452    while (my $res = $sth->fetchrow_hashref) {
453        push(@users, $res->{name});
454    }
455    @users
456}
457
458=head2 rename_nethost ($nethostname, $to, %config)
459
460Facility function to rename computer to new name
461
462=cut
463
464sub rename_nethost {
465    my ($self, $nethostname, $to, %config) = @_;
466    {
467        my $obj = $self->get_object('nethost', $nethostname) or do {
468            $self->log(LA_ERR, 'Unable to rename non exisant host %s', $nethostname);
469            return;
470        };
471        $obj->_delAttributeValue(cname => $to);
472    }
473    $self->rename_object('nethost', $nethostname, $to) or return;
474    if ($config{'addcname'}) {
475        my $obj = $self->get_object('nethost', $to);
476        $obj->_addAttributeValue(cname => $nethostname);
477    }
478    return 1;
479}
480
481=head2 nethost_exchange_ip ($ip1, $ip2)
482
483Exchange ip1 with ip2 in base
484
485=cut
486
487sub nethost_exchange_ip {
488    my ($self, $ip1, $ip2) = @_;
489    my ($obj1, $obj2);
490    if (my ($host1) = $self->search_objects('nethost', "ip=$ip1")) {
491        $obj1 = $self->get_object('nethost', $host1);
492    } else {
493        $self->log(LA_ERR, "Cannot find host having $ip1");
494        return;
495    }
496    if (my ($host2) = $self->search_objects('nethost', "ip=$ip2")) {
497        $obj2 = $self->get_object('nethost', $host2);
498    } else {
499        $self->log(LA_ERR, "Cannot find host having $ip2");
500        return;
501    }
502    if ($obj1->id eq $obj2->id) {
503        $self->log(LA_ERR, "Both ip belong to same host (%s)", $obj1->id);
504        return;
505    }
506
507    $self->log(LA_NOTICE, "Exchanging IP between %s and %s", $obj1->id, $obj2->id);
508    $obj1->delAttributeValue('ip', $ip1) or return;
509    $obj2->delAttributeValue('ip', $ip2) or return;
510    $obj1->addAttributeValue('ip', $ip2) or return;
511    $obj2->addAttributeValue('ip', $ip1) or return;
512    return 1;
513}
514
515=head1 ATTRIBUTES FUNCTIONS
516
517=head2 register_attribute ($otype, $attribute, $comment)
518
519Register a new attribute in base
520
521=cut
522
523sub register_attribute {
524    my ($self, $otype, $attribute, $comment) = @_;
525    my $pclass = $self->_load_obj_class($otype) or return;
526    $pclass->register_attribute($self, $attribute, $comment);
527}
528
529=head2 is_registered_attribute ($otype, $attribute)
530
531Return true is attribute already exists
532
533=cut
534
535sub is_registered_attribute {
536    my ($self, $otype, $attribute) = @_;
537    my $pclass = $self->_load_obj_class($otype) or return;
538    $pclass->is_registered_attribute($self, $attribute);
539}
540
541=head2 get_attribute_comment ($otype, $attribute)
542
543Return the comment associated to attribute
544
545=cut
546
547sub get_attribute_comment {
548    my ($self, $otype, $attribute) = @_;
549    my $pclass = $self->_load_obj_class($otype) or return;
550    $pclass->get_attribute_comment($self, $attribute);
551}
552
553=head2 set_attribute_comment ($otype, $attribute, $comment)
554
555Set comment to attribute
556
557=cut
558
559sub set_attribute_comment {
560    my ($self, $otype, $attribute, $comment) = @_;
561    my $pclass = $self->_load_obj_class($otype) or return;
562    $pclass->set_attribute_comment($self, $attribute, $comment);
563}
564
565=head2 get_datarequest ($id)
566
567Return user request C<$id>
568
569=cut
570
571sub get_datarequest {
572    my ($self, $id) = @_;
573
574    my $sth = $self->db->prepare(q{
575        select name from request
576        where id = ?
577        });
578    $sth->execute($id);
579    if (my $res = $sth->fetchrow_hashref) {
580        my $accreq = $self->get_object('accreq', $res->{name});
581        return LATMOS::Accounts::Bases::Sql::DataRequest->new($accreq, $id);
582    } else {
583        return;
584    }
585}
586
587=head2 list_requests
588
589list user request currently waiting in base
590
591=cut
592
593sub list_requests {
594    my ($self, $due) = @_;
595
596    my $sth = $self->db->prepare(
597        sprintf(
598            q{
599            select id from request
600            where done is null
601            %s
602            order by apply
603            },
604            defined($due)
605                ? 'and apply ' . ($due ? '<' : '>=') . ' now()'
606                : ''
607        )
608    );
609    $sth->execute;
610    my @ids;
611    while (my $res = $sth->fetchrow_hashref) {
612        push(@ids, $res->{id});
613    }
614
615    @ids
616}
617
618=head2 list_requests_by_submitter ($id)
619
620list user request currently waiting in base ask by user C<$id>
621
622=cut
623
624sub list_requests_by_submitter {
625    my ($self, $id) = @_;
626
627    my $sth = $self->db->prepare(q{
628        select id from request
629        where done is null and "user" = ?
630        order by apply
631    });
632    $sth->execute($id);
633    my @ids;
634    while (my $res = $sth->fetchrow_hashref) {
635        push(@ids, $res->{id});
636    }
637
638    @ids
639}
640
641
642=head2 list_request_by_object ($otype, $id, $req)
643
644Return the list of pending request for a specific object
645
646C<$req> is an optional forms name to limit search
647
648=cut
649
650sub list_request_by_object {
651    my ($self, $otype, $id, $req) = @_;
652
653    my $sth = $self->db->prepare(q{
654        select * from request join
655        accreq on request.name = accreq.name
656        join accreq_attributes on accreq_attributes.okey = accreq.ikey
657        where
658        request.applied is NULL and
659        accreq_attributes.attr = 'oType' and
660        accreq_attributes.value = ?
661        and request.object = ?
662    } .
663    ($req ? ' and request.name = ? ' : '')
664    . q{
665        order by apply
666    });
667    $sth->execute($otype, $id, ($req ? ($req) : ()));
668    my @ids;
669    while (my $res = $sth->fetchrow_hashref) {
670        push(@ids, $res->{id});
671    }
672
673    @ids
674}
675
676=head2 list_pending_requests
677
678List user request to apply
679
680=cut
681
682sub list_pending_requests {
683    my ($self) = @_;
684
685    my $sth = $self->db->prepare(q{
686        select id from request
687        where done is null
688            and apply < now()
689        order by apply
690    });
691    $sth->execute;
692    my @ids;
693    while (my $res = $sth->fetchrow_hashref) {
694        push(@ids, $res->{id});
695    }
696
697    @ids
698}
699
700=head2 list_auto_pending_requests
701
702List automatic request
703
704=cut
705
706sub list_auto_pending_requests {
707    my ($self) = @_;
708
709    my $sth = $self->db->prepare(q{
710        select id from request
711        where done is null
712            and apply < now()
713            and automated = true
714        order by apply
715    });
716    $sth->execute;
717    my @ids;
718    while (my $res = $sth->fetchrow_hashref) {
719        push(@ids, $res->{id});
720    }
721
722    @ids
723}
724
725sub ReportChange {
726    my ($self, $otype, $name, $ref, $changetype, $message, @args) = @_;
727
728    my $sthmodifiedby = $self->db->prepare(q{
729        UPDATE objects set modifiedby = ? where ikey = ?
730    });
731
732    $sthmodifiedby->execute(
733        $self->user || '@Console',
734        $ref,
735    );
736
737    my $sth = $self->db->prepare(q{
738        INSERT into objectslogs (ikey, irev, otype, name, changetype, username, message)
739        VALUES (?,?,?,?,?,?,?)
740        });
741
742    $sth->execute(
743        $ref,
744        $self->current_rev,
745        $otype,
746        $name,
747        $changetype,
748        $self->user || '@Console',
749        sprintf($message, @args),
750    );
751}
752
753=head2 getobjectlogs($otype, $name)
754
755Return logs for object type C<$otype> having C<$name>.
756
757=cut 
758
759sub getobjectlogs {
760    my ($self, $otype, $name) = @_;
761
762    my $sth = $self->db->prepare(q{
763        select ikey from objectslogs where
764            otype = ? and
765            name  = ?
766        group by ikey
767    });
768    $sth->execute($otype, $name);
769    my @ids;
770    while (my $res = $sth->fetchrow_hashref) {
771        push(@ids, $res->{ikey});
772    }
773    @ids or return;
774
775    my $sth2 = $self->db->prepare(sprintf(
776        q{
777            select * from objectslogs where ikey IN (%s)
778            order by logdate asc
779        },
780        join(',', ('?') x scalar(@ids))
781    ));
782
783    $sth2->execute(@ids);
784    my @logs;
785    while (my $res = $sth2->fetchrow_hashref) {
786        push(@logs, $res);
787    }
788
789    return @logs;
790}
791
792=head2 getlogs
793
794Return logs for last year
795
796=cut
797
798sub getlogs {
799    my ($self) = @_;
800    my $sth2 = $self->db->prepare(
801        q{
802            select * from objectslogs
803            where logdate > now() - '1 year'::interval
804            order by logdate asc
805        },
806    );
807
808    $sth2->execute();
809    my @logs;
810    while (my $res = $sth2->fetchrow_hashref) {
811        push(@logs, $res);
812    }
813
814    return @logs;
815}
816
817
8181;
819
820__END__
821
822=head1 SEE ALSO
823
824=head1 AUTHOR
825
826Olivier Thauvin, E<lt>olivier.thauvin@latmos.ipsl.frE<gt>
827
828=head1 COPYRIGHT AND LICENSE
829
830Copyright (C) 2008, 2009 CNRS SA/CETP/LATMOS
831
832This library is free software; you can redistribute it and/or modify
833it under the same terms as Perl itself, either Perl version 5.10.0 or,
834at your option, any later version of Perl 5 you may have available.
835
836
837=cut
Note: See TracBrowser for help on using the repository browser.