ssh-authkeys-split: do not zap keys without trailing newlines
[gitolite.git] / contrib / utils / ipa_groups.pl
blob9cffa400101286fbed7d6bca2a6963f60b8404ef
1 #!/usr/bin/env perl
3 # ipa_groups.pl
5 # See perldoc for usage
7 use Net::LDAP;
8 use Net::LDAP::Control::Paged;
9 use Net::LDAP::Constant qw(LDAP_CONTROL_PAGED);
10 use strict;
11 use warnings;
13 my $usage = <<EOD;
14 Usage: $0 \$uid
15 This script returns a list of groups that \$uid is a member of
16 EOD
18 my $uid = shift or die $usage;
20 ## CONFIG SECTION
22 # If you want to do plain-text LDAP, then set ldap_opts to an empty hash and
23 # then set protocols of ldap_hosts to ldap://
24 my @ldap_hosts = [
25 'ldaps://auth-ldap-001.prod.example.net',
26 'ldaps://auth-ldap-002.prod.example.net',
28 my %ldap_opts = (
29 verify => 'require',
30 cafile => '/etc/pki/tls/certs/prod.example.net_CA.crt'
33 # Base DN to search
34 my $base_dn = 'dc=prod,dc=example,dc=net';
36 # User for binding to LDAP server with
37 my $user = 'uid=svc_gitolite_bind,cn=sysaccounts,cn=etc,dc=prod,dc=example,dc=net';
38 my $pass = 'reallysecurepasswordstringhere';
40 ## Below variables should not need to be changed under normal circumstances
42 # OU where groups are located. Anything return that is not within this OU is
43 # removed from results. This OU is static on FreeIPA so will only need updating
44 # if you want to support other LDAP servers. This is a regex so can be set to
45 # anything you want (E.G '.*').
46 my $groups_ou = qr/cn=groups,cn=accounts,${base_dn}$/;
48 # strip path - if you want to return the full path of the group object then set
49 # this to 0
50 my $strip_group_paths = 1;
52 # Number of seconds before timeout (for each query)
53 my $timeout=5;
55 # user object class
56 my $user_oclass = 'person';
58 # group attribute
59 my $group_attrib = 'memberOf';
61 ## END OF CONFIG SECTION
63 # Catch timeouts here
64 $SIG{'ALRM'} = sub {
65 die "LDAP queries timed out";
68 alarm($timeout);
70 # try each server until timeout is reached, has very fast failover if a server
71 # is totally unreachable
72 my $ldap = Net::LDAP->new(@ldap_hosts, %ldap_opts) ||
73 die "Error connecting to specified servers: $@ \n";
75 my $mesg = $ldap->bind(
76 dn => $user,
77 password => $pass
80 if ($mesg->code()) {
81 die ("error:", $mesg->code(),"\n",
82 "error name: ",$mesg->error_name(),"\n",
83 "error text: ",$mesg->error_text(),"\n");
86 # How many LDAP query results to grab for each paged round
87 # Set to under 1000 to limit load on LDAP server
88 my $page = Net::LDAP::Control::Paged->new(size => 500);
90 # @queries is an array or array references. We initially fill it up with one
91 # arrayref (The first LDAP search) and then add more during the execution.
92 # First start by resolving the group.
93 my @queries = [ ( base => $base_dn,
94 filter => "(&(objectClass=${user_oclass})(uid=${uid}))",
95 control => [ $page ],
96 ) ];
98 # array to store groups matching $groups_ou
99 my @verified_groups;
101 # Loop until @queries is empty...
102 foreach my $queryref (@queries) {
104 # set cookie for paged querying
105 my $cookie;
106 alarm($timeout);
107 while (1) {
108 # Perform search
109 my $mesg = $ldap->search( @{$queryref} );
111 foreach my $entry ($mesg->entries) {
112 my @groups = $entry->get_value($group_attrib);
113 # find any groups matching $groups_ou regex and push onto $verified_groups array
114 foreach my $group (@groups) {
115 if ($group =~ /$groups_ou/) {
116 push @verified_groups, $group;
121 # Only continue on LDAP_SUCCESS
122 $mesg->code and last;
124 # Get cookie from paged control
125 my($resp) = $mesg->control(LDAP_CONTROL_PAGED) or last;
126 $cookie = $resp->cookie or last;
128 # Set cookie in paged control
129 $page->cookie($cookie);
130 } # END: while(1)
132 # Reset the page control for the next query
133 $page->cookie(undef);
135 if ($cookie) {
136 # We had an abnormal exit, so let the server know we do not want any more
137 $page->cookie($cookie);
138 $page->size(0);
139 $ldap->search( @{$queryref} );
140 # Then die
141 die("LDAP query unsuccessful");
144 } # END: foreach my $queryref (...)
146 # we're assuming that the group object looks something like
147 # cn=name,cn=groups,cn=accounts,dc=X,dc=Y and there are no ',' chars in group
148 # names
149 if ($strip_group_paths) {
150 for (@verified_groups) { s/^cn=([^,]+),.*$/$1/g };
153 foreach my $verified (@verified_groups) {
154 print $verified . "\n";
157 alarm(0);
159 __END__
161 =head1 NAME
163 ipa_groups.pl
165 =head2 VERSION
167 0.1.1
169 =head2 DESCRIPTION
171 Connects to one or more FreeIPA-based LDAP servers in a first-reachable fashion and returns a newline separated list of groups for a given uid. Uses memberOf attribute and thus supports nested groups.
173 =head2 AUTHOR
175 Richard Clark <rclark@telnic.org>
177 =head2 FreeIPA vs Generic LDAP
179 This script uses regular LDAP, but is focussed on support for FreeIPA, where users and groups are generally contained within single OUs, and memberOf attributes within the user object are enumerated with a recursive list of groups that the user is a member of.
181 It is mostly impossible to provide generic out of the box LDAP support due to varying schemas, supported extensions and overlays between implementations.
183 =head2 CONFIGURATION
185 =head3 LDAP Bind Account
187 To setup an LDAP bind user in FreeIPA, create a svc_gitolite_bind.ldif file along the following lines:
189 dn: uid=svc_gitolite_bind,cn=sysaccounts,cn=etc,dc=prod,dc=example,dc=net
190 changetype: add
191 objectclass: account
192 objectclass: simplesecurityobject
193 uid: svc_gitolite_bind
194 userPassword: reallysecurepasswordstringhere
195 passwordExpirationTime: 20150201010101Z
196 nsIdleTimeout: 0
198 Then create the service account user, using ldapmodify authenticating as the the directory manager account (or other acccount with appropriate privileges to the sysaccounts OU):
200 $ ldapmodify -h auth-ldap-001.prod.example.net -Z -x -D "cn=Directory Manager" -W -f svc_gitolite_bind.ldif
202 =head3 Required Configuration
204 The following variables within the C<## CONFIG SECTION ##> need to be configured before the script will work.
206 C<@ldap_hosts> - Should be set to an array of URIs or hosts to connect to. Net::LDAP will attempt to connect to each host in this list and stop on the first reachable server. The example shows TLS-supported URIs, if you want to use plain-text LDAP then set the protocol part of the URI to LDAP:// or just provide hostnames as this is the default behavior for Net::LDAP.
208 C<%ldap_opts> - To use LDAP-over-TLS, provide the CA certificate for your LDAP servers. To use plain-text LDAP, then empty this hash of it's values or provide other valid arguments to Net::LDAP.
210 C<%base_dn> - This can either be set to the 'true' base DN for your directory, or alternatively you can set it the the OU that your users are located in (E.G cn=users,cn=accounts,dc=prod,dc=example,dc=net).
212 C<$user> - Provide the full Distinguished Name of your directory bind account as configured above.
214 C<$pass> - Set to password of your directory bind account as configured above.
216 =head3 Optional Configuration
218 C<$groups_ou> - By default this is a regular expression matching the default groups OU. Any groups not matching this regular expression are removed from the search results. This is because FreeIPA enumerates non-user type groups (E.G system, sudoers, policy and other types) within the memberOf attribute. To change this behavior, set C<$groups_ou> to a regex matching anything you want (E.G: '.*').
220 C<$strip_group_paths> - If this is set to perl boolean false (E.G '0') then groups will be returned in DN format. Default is true, so just the short/CN value is returned.
222 C<$timeout> - Number of seconds to wait for an LDAP query before determining that it has failed and trying the next server in the list. This does not affect unreachable servers, which are failed immediately.
224 C<$user_oclass> - Object class of the user to search for.
226 C<$group_attrib> - Attribute to search for within the user object that denotes the membership of a group.
228 =cut