# # AutoCreateFromExternalUserInfo overlay # # author: Robin Battey # # based in part by the LookupExternalUserInfo function # written by Jeff Hoover # # This Overlay is useful if you want the following behavior: # * Whenever RT tries -- and fails -- to load a user from the # RT user database, it queries an Ldap server for the account # information and creates it on the fly (for both email lookups # and web logins) # * RT behaves as though the account has always been there -- i.e. # there is no reason to give "Everyone" the "Create Ticket" right, # because the account has been there all along ... # * NO synchronization with the Ldap server once the account has # been created, so any changes in RT stay in RT and any changes # in Ldap stay in Ldap (unless you run some other script, of course) # # The situation I had at my place of employment, which caused me # to write this, is we have an Active Directory server with a fairly # large number of users on it, with a fair bit of adding/deleting of # users going on. I wanted a way for users to send a request to the # help queue, and then immediately be able to see the ticket, the # ticket's status, etc using their Active Directory (i.e. windows # domain) username and password. I wrote this overlay for the account # generation, and used the LdapOverlay available in the "Contributions" # section of the RT3 Wiki (http://wiki.bestpractical.com) for the # authentication. This overlay was therefore designed to work well # with the LdapOverlay, and actually uses (some of) the same Ldap # variables. # # I spent a bit of effort making sure this was modular, particularly # in the case of a different external user info source. It currently # works with LDAP (and, of course, Active Directory), but there's no # good reason why you can't just replace the LookupExternalUserInfo # function to get your data from somewhere else. no warnings qw(redefine); # {{{ sub CreateFromExternalUserInfo =head2 CreateFromExternalUserInfo Calls LookupExternalUserInfo and creates a user from the results =cut sub CreateFromExternalUserInfo { my $self = shift; my @lookupargs = @_; $RT::Logger->debug( "CreateFromExternalUserInfo: entered with args: ", @lookupargs, "\n"); my ($found, %params) = $self->LookupExternalUserInfo (@lookupargs); $RT::Logger->debug( "CreateFromExternalUserInfo: params hash: ", %params, "\n"); unless ($found) { $RT::Logger->info ("CreateFromExternalUserInfo: failed to find user with args: ", @lookupargs, "\n"); return undef; } my $UserObj = RT::User->new(RT::CurrentUser->new('root')); my ($val, $msg) = $UserObj->Create( %{ref($RT::AutoCreate) ? $RT::AutoCreate : {}}, %params ); unless ($val) { $RT::Logger->info ("CreateFromExternalUserInfo: failed to create user with args: ", @lookupargs, "\n"); return undef; } $RT::Logger->info ("CreateFromExternalUserInfo: created user $val (\"$msg\")\n"); $self->Load($val); } # }}} # {{{ sub LoadByEmail =head2 LoadByEmail Loads a User into this CurrentUser object. Takes the email address of the user to load. =cut sub LoadByEmail { my $self = shift; my $identifier = shift; $identifier = RT::User::CanonicalizeEmailAddress(undef, $identifier); $self->LoadByCol("EmailAddress",$identifier); if (!$self->Id and $RT::AutoCreateFromExternalUserInfo) { $self->CreateFromExternalUserInfo ("mail", $identifier); } } # }}} # {{{ sub LoadByName =head2 LoadByName Loads a User into this CurrentUser object. Takes a Name. =cut sub LoadByName { my $self = shift; my $identifier = shift; $self->LoadByCol("Name",$identifier); if (!$self->Id and $RT::AutoCreateFromExternalUserInfo) { $self->CreateFromExternalUserInfo ("uid", $identifier); $self->LoadByCol("Name",$identifier); } } # }}} # {{{ sub Load =head2 Load Loads a User into this CurrentUser object. Takes either an integer (users id column reference) or a Name The latter is deprecated. Instead, you should use LoadByName. Formerly, this routine also took email addresses. =cut sub Load { my $self = shift; my $identifier = shift; #if it's an int, load by id. otherwise, load by name. if ($identifier !~ /\D/) { $self->SUPER::LoadById($identifier); } elsif (UNIVERSAL::isa($identifier,"RT::User")) { # DWIM if they pass a user in $self->SUPER::LoadById($identifier->Id); } else { # This is a bit dangerous, we might get false authen if somebody # uses ambigous userids or real names: $self->LoadByName($identifier); } } # }}} # {{{ sub LookupExternalUserInfo =head2 LookupExternalUserInfo LookupExternalUserInfo takes a key/value pair, looks it up in LDAP, and returns a (sparse) params hash suitable for creating a User object =cut sub LookupExternalUserInfo { my $self = shift; my $key = shift; my $value = shift; # I have dreams of this function using an $LdapAttrMap hash fpr # Ldap -> RT account creation, in the form of: # # %RT::LdapAttrMap = ( Name=> 'uid', # RealName=> 'cn', # EmailAdress=> 'mail', # [anyUserAttr]=> [anyLdapAttr]); # # But until then, you better have the following variables defined # in your RT_SiteConfig.pm: # # Set($LdapServer, "ldap.domain.tld"); # Set($LdapBase, "cn=Users,dc=domain,dc=tld"); # Set($LdapUidAttr, "uid"); # Set($LdapNameAttr, "cn"); # Set($LdapMailAttr, "mail"); # Set($LdapFilter, "(objectclass=person)"); # Set($AutoCreateFromExternalUserInfo, 1); # # enjoy! my $FoundInExternalDatabase = 0; my %params = ( Name=> undef, EmailAddress=> undef, RealName=> undef); $RT::Logger->debug("LookupExternalUserInfo: Entered with:\n", "\tkey = $key\n", "\tvalue = $value\n"); use Net::LDAP; use Net::LDAP::Constant qw(LDAP_SUCCESS LDAP_PARTIAL_RESULTS); my $ldap = new Net::LDAP($RT::LdapServer) or $RT::Logger->critical("LookupExternalUserInfo: Cannot connect to ", "LDAP'\n"), return ($FoundInExternalDatabase, %params); my $mesg = $ldap->bind(); if ($mesg->code != LDAP_SUCCESS) { $RT::Logger->critical("LookupExternalUserInfo: Cannot bind anonymously ", "to LDAP:", $mesg->code, "\n"); #$params{'RealName'} = "\"$params{'RealName'}\""; return ($FoundInExternalDatabase, %params); } my $filter = "@{[ $key ]}=$value"; $RT::Logger->debug("LookupExternalUserInfo: First search filter ", "'$filter'\n"); $mesg = $ldap->search(base => $RT::LdapBase, filter => $filter, attrs => [ $RT::LdapUidAttr, $RT::LdapMailAttr, $RT::LdapNameAttr ]); if ($mesg->code != LDAP_SUCCESS and $mesg->code != LDAP_PARTIAL_RESULTS) { $RT::Logger->critical("LookupExternalUserInfo: Could not search for ", "$filter: ", $mesg->code, "\n"); $params{'RealName'} = "\"$params{'RealName'}\""; return ($FoundInExternalDatabase, %params); } $RT::Logger->debug("LookupExternalUserInfo: Search produced ", $mesg->count, " results\n"); # The search succeeded with just one match if ($mesg->count == 1) { $params{'Name'} = ($mesg->first_entry->get_value($RT::LdapUidAttr))[0]; $params{'EmailAddress'} = ($mesg->first_entry->get_value($RT::LdapMailAttr))[0]; $params{'RealName'} = ($mesg->first_entry->get_value($RT::LdapNameAttr))[0]; #$params{'RealName'} = "\"$params{'RealName'}\""; $FoundInExternalDatabase = 1; } $mesg = $ldap->unbind(); if ($mesg->code != LDAP_SUCCESS) { $RT::Logger->critical("LookupExternalUserInfo: Could not unbind from ", "LDAP: ", $mesg->code, "\n"); } undef $ldap; undef $mesg; $RT::Logger->debug("LookupExternalUserInfo: Leaving LDAP examination ", "with:\n", "\tName = $params{'Name'}\n", "\tEmailAddress = $params{'EmailAddress'}\n", "\tRealName = $params{'RealName'}\n", "\tFound = $FoundInExternalDatabase\n"); return ($FoundInExternalDatabase, %params); } # }}} 1;