From 50bf84527e946c3bb6ef429376df1f0989a9bb96 Mon Sep 17 00:00:00 2001 From: Martin Haase <martin.haase@daasi.de> Date: Mon, 27 Dec 2010 11:49:22 +0000 Subject: [PATCH] move completed, via three towns git-svn-id: https://textgridlab.org/svn/textgrid/trunk/middleware/tgauth@8232 7c539038-3410-0410-b1ec-0f2a7bf1c452 --- .../00Readme.txt | 1 + .../etc/syncFromRBAC.conf-dist | 15 + .../etc/syncFromRBAC.sys | 118 +++++ .../last_LDAP_sync_timestamp.txt | 1 + .../syncFromRBAC | 459 ++++++++++++++++++ 5 files changed, 594 insertions(+) create mode 100644 info.textgrid.middleware.tgauth.pdp2acl/00Readme.txt create mode 100644 info.textgrid.middleware.tgauth.pdp2acl/etc/syncFromRBAC.conf-dist create mode 100644 info.textgrid.middleware.tgauth.pdp2acl/etc/syncFromRBAC.sys create mode 100644 info.textgrid.middleware.tgauth.pdp2acl/last_LDAP_sync_timestamp.txt create mode 100755 info.textgrid.middleware.tgauth.pdp2acl/syncFromRBAC diff --git a/info.textgrid.middleware.tgauth.pdp2acl/00Readme.txt b/info.textgrid.middleware.tgauth.pdp2acl/00Readme.txt new file mode 100644 index 0000000..95ce22c --- /dev/null +++ b/info.textgrid.middleware.tgauth.pdp2acl/00Readme.txt @@ -0,0 +1 @@ +synchronizes Access Policy from openRBAC into POSIX ACL entries on a Grid Host. To be called by CRON with root access regularly. diff --git a/info.textgrid.middleware.tgauth.pdp2acl/etc/syncFromRBAC.conf-dist b/info.textgrid.middleware.tgauth.pdp2acl/etc/syncFromRBAC.conf-dist new file mode 100644 index 0000000..531b46e --- /dev/null +++ b/info.textgrid.middleware.tgauth.pdp2acl/etc/syncFromRBAC.conf-dist @@ -0,0 +1,15 @@ +ldap_conf_host = ldap.example.org +ldap_conf_port = 389 +ldap_conf_binddn = "cn=manager,dc=example,dc=org" +ldap_conf_bindpw = secret +ldap_conf_is_tls = 1 +ldap_conf_tls_cafile = /path/to/cacert/directory/chain.pem +ldap_conf_tls_verify = require +ldap_conf_tls_cypher = AES256-SHA +ldap_conf_basedn = dc=example,dc=org +ldap_conf_scope = sub + +gridmapfilepath = /etc/grid-security/grid-mapfile +groupfilepath = /etc/group + +slcs_dn_prefix = /C=DE/O=DFN-Verein/OU=DFN-PKI/OU=SLCS/ diff --git a/info.textgrid.middleware.tgauth.pdp2acl/etc/syncFromRBAC.sys b/info.textgrid.middleware.tgauth.pdp2acl/etc/syncFromRBAC.sys new file mode 100644 index 0000000..cb5fc25 --- /dev/null +++ b/info.textgrid.middleware.tgauth.pdp2acl/etc/syncFromRBAC.sys @@ -0,0 +1,118 @@ +progname = "syncFromRBAC" + +version = 0.2 + +date = "2010-12-06" +<author> + name = "Martin Haase" + org= "DAASI International GmbH" + mail = "martin.haase@daasi.de" +</author> + +<copyright> +text1 = Copyright (c) 2010 DAASI International GmbH +text2 = This library is free software; you can redistribute it and/or \ +modify it under the same terms as Perl itself. +</copyright> + +progshortdescr = "Synchronize RBAC rules into POSIX ACLs" + +<progdescription> +text01 = Syncs an external PDP's policy into extendeded Access Control entries +text02 = in the resource's file system. The PDP for this proof-of-concept system +text03 = is an RBAC implementation (openRBAC), which is part of TextGrid's +text04 = TG-auth* infrastructure. +</progdescription> + +<bugs> +text = Please report bugs to martin.haase@daasi.de +</bugs> + +<additions example> +text1 = "For getting this manpage: " +text2 = " syncFromRBAC.pl -h" +</additions> + +<additions requirements> +text1 = "Following modules are required: " +text2 = Data::Dump +text3 = Net::LDAP +text4 = Set::Scalar +text5 = DAASIlib::CONF +text5 = DAASIlib::DATA +</additions> + +<options loglevel> + key = "l" + must = 0 + description = "Loglevel for controlling logmessages. Currently unused, use -d." + arg = 1 + argtype = "skalar" + values = "debug, info" + default = "info" +</options> + +<options logfile> + key = "f" + must = 0 + description = "Name of the logfile with absolute or relative path. " + arg = 1 + argtype = "filename_add_subdir_log" + default = "./log/syncFromRBAC.log" +</options> + +<options debugmode> + key = "d" + must = 0 + description = "sets debug mode to on" + arg = 0 + default = 0 +</options> + +<options write_to_system> + key = "w" + must = 0 + description = "does not make a dry run. Unless -w is specified, nothing is really written into the system" + arg = 0 + default = 0 +</options> + +<options printhelp> + key = "h" + must = 0 + description = "prints out the manpage" + arg = 0 +</options> + +<options helpfeature> + key = "H" + must = 0 + description = "prints out description of the feature referenced by \ + commandline flag or config file token. " + arg = 1 +</options> + +<options alternative_last_modify_timestamp> + key = "t" + must = 0 + description = "Instead of reading from a file (see option -m), use this option to specify a timestamp directly on the command line in the same format." + arg = 1 +</options> + +<options last_modifyTimestamp_path> + key = "m" + must = 0 + description = "File to read timestamp from when to start synchronizing. Format: YYYYMMDDhhmmssZ on first line of file. Needs to be Z time, aka GMT." + arg = 1 + argtype = "filename_exist_subdir_etc" + default = "./last_LDAP_sync_timestamp.txt" +</options> + +<options configfile> + key = "c" + must = 0 + description = Name of the user config file with absolute or relative path. + arg = 1 + argtype = "filename_exist_subdir_etc" + default = "./etc/syncFromRBAC.conf" +</options> diff --git a/info.textgrid.middleware.tgauth.pdp2acl/last_LDAP_sync_timestamp.txt b/info.textgrid.middleware.tgauth.pdp2acl/last_LDAP_sync_timestamp.txt new file mode 100644 index 0000000..cd25fb3 --- /dev/null +++ b/info.textgrid.middleware.tgauth.pdp2acl/last_LDAP_sync_timestamp.txt @@ -0,0 +1 @@ +20101231235959Z diff --git a/info.textgrid.middleware.tgauth.pdp2acl/syncFromRBAC b/info.textgrid.middleware.tgauth.pdp2acl/syncFromRBAC new file mode 100755 index 0000000..68b4866 --- /dev/null +++ b/info.textgrid.middleware.tgauth.pdp2acl/syncFromRBAC @@ -0,0 +1,459 @@ +#!/usr/bin/perl -w +############################################ +# Syncs an external PDP's policy (here: the contents of the LDAP Database +# of a TextGrid TG-auth* openRBAC instance) into group and extendeded Access +# Control entries in the Grid resource's file system. +# +# Author: Martin Haase / DAASI International GmbH / Gap-SLC +# +### Detailed function: +# * Every role in a TextGrid project will be mapped to a system group entry. +# * Every performer of this role will be added as a member to this group. +# * A TextGrid project is mapped to a directory under <project creator's home>/.textgrid/ +# * A TextGrid object is mapped to a file in a project directory +# * Extended POSIX ACLs will be used to ensure each resource can be accessed by the right project members. +# +### Some remarks: + +### A user's account must exist in the system's grid-mapfile. You can +#use the companion gap-SLC script, Pseudo_dgridmap.perl, to accomplish +#this. + +### Currently only a subset of the accounts in the gri-mapfile is +#honored, i.e. only those which are issued by the Short-Lived +#Certificate Service (SLCS). Why? We need a mapping from the PDP. A +#user's identity in TG-auth* is by reference to his +#eduPersonPrincipalName, and the SLCS DNs contain exactly the +#ePPN. This way the mapping can be established. + +### Permissions for roles are still hard-wired, as they are in +#TG-auth* as well + +### we contact the LDAP database of TG-auth*/openRBAC +#directly. Re-implementors might wish to create dedicated accessor +#functions in the PDP they wish to contact. + +### see syncFromRBAC -h for the man page +# +# +# History: +# version 0.1 2010-07-13 first proof-of-concept +# version 0.2 2010-12-06 separated configuration and code, documentation +# version 0.2b 2010-12-23 error handling, configuration with DAASIlib, first check-in to SVN +# version 0.2c 2010-12-27 further documentation + +### imports =========================================== +use Data::Dump qw(dump); +use Net::LDAP qw(LDAP_SUCCESS LDAP_TIMELIMIT_EXCEEDED LDAP_SIZELIMIT_EXCEEDED LDAP_NO_SUCH_OBJECT LDAP_ALREADY_EXISTS ); +use Set::Scalar; + +use DAASIlib::CONF qw (is_debug); +use DAASIlib::Data; +# these two need: IO::Prompt, Log::Log4perl, DBI, DAASIlib::Gettext, Config::General + + +### configuration management =========================== +my $data = new DAASIlib::Data; +my ($progname, $progpath, $etcdir, $sysconfig) = $data->getProgramFiles($0); + +my $conf = new DAASIlib::CONF; +$conf->loadConfig($sysconfig, $progpath, $etcdir); + + + +# GLOBAL CONSTANTS ====================================== +$DEBUG = 0; +$DRY_RUN = 1; +if (defined $conf->{data}->{debugmode} && $conf->{data}->{debugmode}) { $DEBUG = 1 } +if (defined $conf->{data}->{write_to_system} && $conf->{data}->{write_to_system}) { $DRY_RUN = 0 } + +$gridmapfilepath = $conf->{data}->{gridmapfilepath}; +$groupfilepath = $conf->{data}->{groupfilepath}; +$logfilepath = $conf->{data}->{logfile}; + +$last_modifyTimestamp_path = $conf->{data}->{last_modifyTimestamp_path}; +$alternative_last_modify_timestamp = $conf->{data}->{alternative_last_modify_timestamp}; + +# these roles are currently hard-wired in TG-auth* +@standardroles = ( + {rolename => "Projektleiter", projectperms => "", resourceperms => ""}, + {rolename => "Administrator", projectperms => "wx", resourceperms => ""}, + {rolename => "Bearbeiter", projectperms => "rwx", resourceperms => "rw"}, + {rolename => "Beobachter", projectperms => "rx", resourceperms => "r"}, + ); + +my %ldap_config; +foreach $k (keys %{$conf->{data}}) { + if ($k =~ /^ldap_conf_(\S+)/) { + $ldap_config{$1} = $conf->{data}{$k}; + } +} +$ldap_config{attribs} = ['*', 'modifyTimestamp']; + +if (defined $alternative_last_modify_timestamp ) { + $last_modifyTimestamp = $alternative_last_modify_timestamp; +} else { + open LMTS, $last_modifyTimestamp_path; + $last_modifyTimestamp = <LMTS>; + chomp $last_modifyTimestamp; + close LMTS; +} + +open LOG, ">>$logfilepath"; # used by logg(); + +if ($last_modifyTimestamp !~/^\d{4}[01]\d[0123]\d[012]\d[0-5]\d[0-5]\dZ$/) { + logg ("invalid timestamp ($last_modifyTimestamp) specified, should be YYYYMMDDhhmmssZ."); + exit 0; +} +logg ("Start synchronizing LDAP from timestamp ($last_modifyTimestamp)"); + + +############## MAIN =========================================== + +# user must exist in grid-mapfile, and we only take those DNs into account that are issued by DFN's SLCS +$userhash = parse_gridmap ( $gridmapfilepath, $conf->{data}->{slcs_dn_prefix}); +$grouphash = parse_groupfile ($groupfilepath); + +debugg( dump $userhash); +debugg ( "Log file is $logfilepath"); + + +# query BEFORE ldapsearch, to ldap write operations in this second are accounted for +# doesnt matter if one entry gets processed twice +$now_timestamp = construct_timestamp (gmtime()); + +$result = getNewEntriesFromLDAP ( $last_modifyTimestamp, \%ldap_config); + +foreach $e ($result->entries) { + + if (is_role($e)) { + logg ("found role, ". $e->dn()); + handle_role($e); + } elsif (is_resource($e)) { + logg ("found resource, ". $e->get_value("TGResourceURI")); + handle_resource($e); + } else { + # do nothing + } +} + + +if (not $DRY_RUN) { + logg ("write system timestamp of last ldapsearch ($now_timestamp) to file for next run"); + open LMTS, ">$last_modifyTimestamp_path"; + print LMTS $now_timestamp; + close LMTS; +} else { + logg ("This has been a DRY RUN, did NOT alter anything on the system nor write timestamp of last ldapsearch ($now_timestamp) to file. After verifying from the logs that everything is correct, you can force this with -w."); +} + +close LOG; +exit 0; + +# FUNCTIONS ====================================== + +# this function handles ordinary TextGridObjects. As CRUD has written +# them already, including the project directory they live in, no mkdir +# has to be issued. +sub handle_resource { + my $e = shift; + my ($tgpr, $dir, $uuid) = parse_uuid ($e->get_value("TGResourceUUID")); + + + logg ("Now setting ACLs of $dir and ./$uuid such that the right ones can access them."); + + if (not exists $grouphash->{$tgpr."_Projektleiter"}) { + logg ( "WARNING: It seems that project $tgpr has not been created although you try to add resources! I will create it for you now..."); + create_project ($tgpr); + } + + sssys ("chgrp ${tgpr}_Projektleiter $dir $dir/$uuid $dir/${uuid}.meta"); + sssys ("setfacl -m m::rwx $dir; setfacl -m m::rw $dir/$uuid $dir/${uuid}.meta"); + sssys ("chmod 700 $dir; chmod 600 $dir/$uuid $dir/${uuid}.meta"); + + # TODO: query actual permissions instead of using the standard roles. However, this has no use as long as there is no Lab frontend for it... + foreach $r (@standardroles) { + + if ($r->{projectperms}) { + sssys ( "setfacl -m g:${tgpr}_".$r->{rolename}.":".$r->{projectperms} ." $dir"); + } + if ($r->{resourceperms}) { + sssys ( "setfacl -m g:${tgpr}_".$r->{rolename}.":".$r->{resourceperms} ." $dir/$uuid $dir/${uuid}.meta"); + } + } +} + +sub parse_uuid { + my $u = shift; + # format: + # gsiftp://ingrid.sub.uni-goettingen.de//home/ttest/.textgrid/TGPR29/a084e9c0-e7d4-3802-b16d-37a223414c82 + if ($u =~ /gsiftp:\/\/ingrid.sub.uni-goettingen.de\/(\/home\/[^\/]+\/.textgrid)\/([^\/]+)\/(\S+)/) { + return ($2, "$1/$2", $3); # i.e. TGPR29, /home/ttest/.textgrid/TGPR29, a084e9c0-e7d4-3802-b16d-37a223414c82 + } else { + return (0,0,0) + } +} + +# this functions handles any change in a project role entry, either +# the project itself (it is a role as well in TG-auth*) or sub-roles. +# takes existing group entries into account and only makes a diff + +sub handle_role { + my $e = shift; + my ($tgpr, $role) = parse_role_dn ($e->dn()); + return if not $tgpr; + if (not $role) { + create_project($tgpr); + return; + } + my @eppnperformers = $e->get_value("rbacPerformer"); + my @uidperformers = (); + foreach $eppn (@eppnperformers) { + if (exists $userhash->{$eppn} ) { + push @uidperformers, $userhash->{$eppn}; + } else { + logg ("Warning: User $eppn not yet in Gridmap File"); + } + } + my $newperformers = new Set::Scalar(@uidperformers); + + if (not exists $grouphash->{$tgpr."_".$role}) { + logg ( "WARNING: It seems that project $tgpr has not been created although you try to add members to its $role role! I will create it for you now..."); + create_project ($tgpr); + } + my $oldperformers = $grouphash->{$tgpr."_".$role}; + debugg ("Old members in $tgpr: ". join ",", $oldperformers->members); + + foreach $out ($oldperformers->difference($newperformers)->members) { + logg ("Throwing out $out of $role in $tgpr"); + sssys ("/usr/sbin/groupmod -R $out ${tgpr}_".$role); + } + foreach $in ($newperformers->difference($oldperformers)->members) { + logg ("Adding $in to $role in $tgpr"); + sssys ("/usr/sbin/groupmod -A $in ${tgpr}_".$role); + } + $grouphash->{$tgpr."_".$role} = $newperformers; +} + + +sub create_project { + $p = shift; + logg ("Creating role groups in $p IF they didnt exist before..."); + foreach $r (@standardroles) { + if (not exists $grouphash->{$p."_".$r->{rolename}}) { + sssys("/usr/sbin/groupadd ${p}_".$r->{rolename}); + $grouphash->{$p."_".$r->{rolename}} = new Set::Scalar(()); + } + } +} + +sub parse_role_dn { + my $dn = shift; + # format: + # rbacName=Bearbeiter,rbacName=TGPR29,rbacName=Projekt-Teilnehmer,ou=roles,dc=rbac,dc=textgrid,dc=de + if ($dn =~ m/rbacName=([^,]+),rbacName=([^,]+),rbacName=Projekt-Teilnehmer,ou=roles,dc=rbac,dc=textgrid,dc=de/) { + return ($2, $1); + } elsif ($dn =~ m/rbacName=([^,]+),rbacName=Projekt-Teilnehmer,ou=roles,dc=rbac,dc=textgrid,dc=de/) { + return ($1,0); + } else { + return (0,0); + } +} + +sub is_role { + my $e = shift; + my @oc = $e->get_value("objectClass"); + return in_array("rbacRole",@oc); +} + +sub is_resource { + my $e = shift; + my @oc = $e->get_value("objectClass"); + return in_array("TextGridResource",@oc) +} + +sub in_array { + my $x = shift; + foreach $e (@_) { + return 1 if ($x eq $e); + } + return 0; +} + +sub logg { + if ($DRY_RUN) { + print "\n".scalar localtime(). " LOG: "; + print shift; + print "\n"; + } else { + print LOG "\n".scalar localtime(). " LOG: "; + print LOG shift; + print LOG "\n"; + } +} + +sub debugg { + return unless $DEBUG; + print "\n".scalar localtime(). " DEBUG: "; + print shift; + print "\n"; +} + +sub errorExit { + my $msg = shift; + logg ($msg); + exit 0; +} + +sub sssys { + my $cmd = shift; + if ($DRY_RUN) { + print "\nSYSTEM CALL: >>>"; + print $cmd; + print "<<<\n"; + } else { + logg ($cmd); + system ($cmd); + } +} + +sub parse_gridmap { + my ($path, $only) = @_; + + my %h = (); + open F, $path; + while (<F>) { + # Format: + # "/C=DE/O=DFN-Verein/OU=DFN-PKI/OU=SLCS/OU=DAASI International GmbH/CN=Tanja Test - tanja.test@idp01.nds.daasi.de" ttest + if (m/$only/) { + m/ - (\S+)\" (\S+)/; + $h{$1} = $2; # eppn => uid + } + } + close F; + return \%h; +} + +sub parse_groupfile { + my ($path) = shift; + + my %h = (); + open F, $path; + while (<F>) { + # TGPR29_Bearbeiter:!:1008:aausprobier,bbenutzer + if (/(TGPR[^:]+):[^:]+:\d+:(.*)/) { + debugg ("found group $1, with members $2"); + $h{$1} = new Set::Scalar(split ",", $2); + } + } + close F; + return \%h; +} + +sub getNewEntriesFromLDAP { + my ($since, $rh_ldap_config) = @_; + + my $ldap = ldapConnect($rh_ldap_config); + + my $filter = "(modifyTimestamp>=$since)"; + + my $mesg = ldapSearch($ldap, $filter, $rh_ldap_config); + # ingrid:~ # ldapsearch -x -H ldap://:5389 -b dc=rbac,dc=textgrid,dc=de -D cn=manager,dc=rbac,dc=textgrid,dc=de -w XhNtmqigeXz61pI9jfwmLyqUOTeJ "*" "+" | less + + foreach $entry ($mesg->entries) { +# $entry->dump; + debugg (dump $entry); + } + + logg "Found ". $mesg->count() . " new entries since last call."; + + return $mesg; +} + +sub ldapConnect { + my ($rh_ldapdef) = @_; + + my $uri = 'ldap://'.$rh_ldapdef->{host}.':'.$rh_ldapdef->{port}; + + my $ldap = Net::LDAP->new( $uri); + + + if ( !$ldap ) { + logg ("ERROR while creating connection to ldap server ". + "$$rh_ldapdef{host}:$$rh_ldapdef{port}"); + errorExit ( " ERROR while creating connection to ldap server ". + "$$rh_ldapdef{host}:$$rh_ldapdef{port} \n"); + return(undef); + } + + if ( $rh_ldapdef->{is_tls} ) { + debugg("starting TLS "); + debugg("verify: $rh_ldapdef->{tls_verify}, cafile: $rh_ldapdef->{tls_cafile} cypher: $rh_ldapdef->{tls_cypher}"); + my $tlsmesg = $ldap->start_tls ( verify => $rh_ldapdef->{tls_verify}, + cafile => $rh_ldapdef->{tls_cafile}, + ciphers => $rh_ldapdef->{tls_cypher}); + + if ( $tlsmesg->code != Net::LDAP::LDAP_SUCCESS ) { + logg ( "ERROR in ldap->start_tls: " . $tlsmesg->error ); + errorExit( "ERROR in ldap->start_tls: " . $tlsmesg->error) ; + } + else { + debugg ( "Start_TLS operation succeeded"); + debugg ( "tls cipher: " . $ldap->cipher . "\n" ); + debugg ( "tls certificate: " . $ldap->certificate . "\n" ); + } + } + + my $mesg = $ldap->bind( $rh_ldapdef->{binddn}, + password => $rh_ldapdef->{bindpw} ); + + unless ( $mesg->code == Net::LDAP::LDAP_SUCCESS ) { + logg ("Returning undef instead of ldap connection"); + $ldap = undef; + errorExit( "Error in LDAP-Bind: ".$mesg->error); + } + + return $ldap; +} + + + +sub ldapSearch { + my ( $ldap, $filter, $rh_ldapdef ) = @_; + + if ( ! $$rh_ldapdef{filter}) { + $$rh_ldapdef{filter}="(objectclass=*)"; + } + + my $mesg = $ldap->search( + base => $$rh_ldapdef{basedn}, + scope => $$rh_ldapdef{scope}, + filter => $filter, + attrs => $$rh_ldapdef{attribs}, +# deref => $$rh_ldapdef{deref}, +# sizelimit => $$rh_ldapdef{sizelimit}, +# timelimit => $$rh_ldapdef{timelimit}, +# typesonly => $$rh_ldapdef{is_typesonly}, +# callback => $$rh_ldapdef{callback}, + ); + + if ( $mesg->code == Net::LDAP::LDAP_SUCCESS ) { + debugg("search performed without error" ); + } else { + errorExit ( "Error in search: ". $mesg->error); + } + + return ($mesg); +} + +### takes time as from localtime() +### returns ldap timstamp as in attribute modifyTimestamp +sub construct_timestamp { + my ($nsec,$nmin,$nhours,$nmday,$nmon,$nyear) = @_; + my $ntime = sprintf("%04d%02d%02d%02d%02d%02dZ", + ($nyear+1900),($nmon+1), + $nmday,$nhours,$nmin,$nsec); + return $ntime; +} + + -- GitLab