Checking User Groups in Perl Net::LDAP - perl

I have a Perl Subversion pre-commit hook that allows me to verify whether or not a user has permissions to change or add to a particular point in a Subversion repository. It uses a Control file that looks like this:
[GROUP SERVER]
users = bob, ted, carol, alice
[GROUP CLIENT]
users = tom, dick, harry
[FILE Client Developers don't touch the Server]
file = proj/server
users = #CLIENT
permission = read-only
[FILE Server people don't touch the Client]
file = proj/client
users = #SERVER
permission = read-only
[FILE Let Tom Do everything]
file = .*
users = tom
permission = read-write
As you can see, I can define groups and use groups when setting permissions. I thought it would be a hoot if I could use the LDAP groups to do the same. That way, our Windows administrators can figure out who's in what group which gives me more time to keep my Facebook status up to date.
I have LDAP configured as thus in Subversion:
<Location /mfx>
DAV svn
SVNParentPath /subversion/svn_repos
AuthType basic
AuthName "Source Repository"
AuthBasicProvider ldap
AuthzLDAPAuthoritative off
AuthLDAPURL "ldap://ldapserver:3268/dc=mycompany,dc=com?sAMAccountName" NONE
AuthLDAPBindDN "CN=SubVersion,OU=Users,OU=Accounts,DC=mycompany,DC=com"
AuthLDAPBindPassword "Swordfish"
Require valid-user
</Location>
I've got the connection to our LDAP server working fine, but now, I need to find out what groups that user is in. I have the user's Subversion name in $svnUser, and now I need to find that user in our LDAP database, and verify the various groups they're in (which is the memberOf value in their LDAP record). However, I have no idea how to go about this.
So far, my code looks like this:
#! /usr/bin/env perl
#
use strict;
use warnings;
use feature qw(say);
use constant {
LDAP_URL => "ldapserver",
LDAP_PORT => 3268,
LDAP_SCHEME => "ldap",
BIND_DN => "CN=SubVersion,OU=Users,OU=Accounts,DC=mycompany,DC=com",
BIND_PWORD => "Swordfish",
USER_DN => "sAMAccountName",
};
use Net::LDAP;
#
# Create LDAP Connection
#
my $ldap = Net::LDAP->new(LDAP_URL, port=> LDAP_PORT, scheme=> LDAP_SCHEME);
my $message;
$message = $ldap->bind(BIND_DN, password => BIND_PWORD);
if ($message->code != 0) {
die qq(Error in LDAP Binding: ) . $message->error_desc;
}
Now, I need to do $ldap->search, but on what? I'm just befuddled by the syntax.

All right, it took me a while, but I figured it out...
It would have been helpful if there was some sample code, but after reading a few LDAP documents, I found out I could do something like this:
(sAMAccountName=$user)
So I tried this:
my $results = $ldap->search(filter => USER_DN . "=$svnUser",
attrs => "memberOf");
I thought this would return only the attributes of memberOf, but didn't. In fact, it returned an array of a single member although I knew this particular user was a member of three groups.
It took me a while to realize that it was returning a Net::LDAP::Search object which meant I had to look up that module to find the methods. From there, I found that I could use the pop_entry method to retrieve a Net::LDAP::Entry object. Okay, another CPAN page to find.
From there, I can do a get_value method on the sMAAccountName, and get an array of DNs that represent the group that person belongs to. I can now parse those names for the groups that Subversion will use.
This means I can now use Windows Groups in my pre-commit script to set write permissions in my repository. This makes it much, much easier to maintain.

Related

Is it possible displaying ldap folder structure with Perl?

I will retrieve user informations from a LDAP server. Customer sent me following 3 data:
LDAP Server IP
Username to authenticate
Password to authenticate
This is the 1st time I am working with LDAP so I studied on it. I see that I must use some parameters like cn, dc, ou. Should not customer give this info either? Or customer's info is enough and can I find these parameters by a code displaying folder structure?
The structure you want to see is called the directory tree and is made of LDAP "entries" (not folders).
You should be able to collect entries from your directory provided that the credentials you've been given are sufficient. For that you need a client library, you can use Perl LDAP.
Using that library you would do something like :
use Net::LDAP;
# Init connection and bind to the directory.
$ldap = Net::LDAP->new('ldaphost.example.com') or die "$#";
$mesg = $ldap->bind ($binddn, password => $password);
# Perform a search on the whole tree below (and including) the base object.
$mesg = $ldap->search(base => $basedn);
# Handle error
$mesg->code && die $mesg->error;
# Display results
foreach $entry ($mesg->entries) { $entry->dump; }
$mesg = $ldap->unbind;
The code above just print entries to stdout ($entry->dump).
$binddn is the dn to authenticate with. If you've been given a simple username that is not a dn, like 'username' instead of something like uid=username,dc=example,dc=com, it would probably mean that you are dealing with Active Directory. In this case you can try to bind with a binddn that corresponds to the following pattern : 'username#example.com'.
basedn is the dn of the base object entry, the search is performed on the whole tree below (and including) the base object. You will have to specify a valid base dn.
You can try to guess which base dn to use. Domain components (dc) usually match the FQDN but it is not guaranteed it results in a valid base dn (e.g. 'ldaphost.example.com' would give 'dc=example,dc=com'). However you can query the server for rootDSE information and retrieve the namingContexts (or defaultNamingContext if any) and use one of them as the base dn :
$ldap->search(base => '', attrs => 'namingContexts');
That said, if you really want to display the directory tree, you have better to go with an LDAP explorer like Apache Directory Studio.
Note that LDAP is not limited to storing information in strict "tree" structures, it just have to respects a Directory Information Tree (DIT) nomenclature.

Perl Net::LDAP - Fetch DN without Search?

I have the DN of the LDAP entry. I know I could search for it: Doing something like this:
my $search = $ldap->search(
base => $dn,
scope => "base",
filter => "(objectclass=*)",
);
But, I don't need to do a search. I have the DN. I simply want to pull up the DN entry and do my operations directly on that. Something like this:
my $dn_entry = $ldap->get( $dn );
Is there a method to get the DN entry from the DN string itself, or do you have to search for the entry even if you know the DN itself?
Using LDAP, clients must always search or use an extended operation to get data. If you're interested in all the attributes associated with an entry, and the DN is known, use the following parameters in a search request:
baseObject: the DN that is known
search scope: base
filter: either (&) or (objectClass=*)
the list of attributes to be returned. Some APIs use * for all user attributes and + for all operational attributes.
What it sounds like you are saying is that you have stored the "Distinguished Name" (a string) rather than the DN entry (a Net::LDAP::Entry object). If this is the case, I believe you have to create a new Net::LDAP::Entry object from the DN. The documentation indicates that you can apply operations directly to such an object without synchronizing with the server, but this won't supply all the data for the given DN. If you need the server's data, you need to get it via $ldap->search(...).
Have you considered using the Net::LDAP::LDIF mechanism for storing DN data locally?

How to do a search-by-DN using Net::LDAP

I want to check if a given DN exists in the LDAP directory, using Perl and Net::LDAP. So, I figured I'd do something like this:
my $dn = 'uid=foo,ou=bar,ou=baz';
$ldap->search(base => $dn, scope => 'base', attrs => ['dn']);
However, that results in a Bad filter error. I can get it to work by adding filter => '(objectClass=*)', but that seems a little klugey.
Is this how I'm supposed to do this, or have I missed something? I'm new to Net::LDAP.
An LDAP client must supply a valid search filter to a search request. Try using (&) for the filter. Note that some broken directory servers do not accept the legal filter (&). If your server is broken in this way, use the present filter (objectClass=*) instead.

How to verify a login through LDAP in Perl?

I writed a small script that binds to an LDAP server and retrieves all users and user informations. Now I'd like to write another one that binds to the LDAP server and then tests a given login. How can I do that?
my $ldap = ldapConnect();
my $user = 'user';
my $pwd = 'pwd';
# TEST USER AND PWD BUT HOW?
sub ldapConnect {
my $ldap = Net::LDAP->new('192.168.*.*');
my $password = '***';
$ldap->bind('cn=Administrator,cn=Users,DC=***,DC=***', password=> $password);
return $ldap;
}
my $ldap = ldapConnect(); # Connect
my $search = $ldap->search( # Search for the user
base => 'DC=***,DC=***',
scope => 'sub',
filter => "(&(uid=$user))",
attrs => ['dn']
);
die "not found" if not $search->count;
# Get the user's dn and try to bind:
my $user_dn = $search->entry->dn;
$ldap->bind( $user_dn, password => $pass );
print +($ldap->error ? "Bad credentials" : "Success!"), "\n";
Instead of binding as an LDAP administrator, just bind as the user you want to test. If the bind succeeds, the login tests fine. If it fails, it doesn't. This way, you don't have to worry about re-implementing all the authentication logic the LDAP server does in Perl.
Alternatively, as David W. points out, if you need to search for the DN for the user (because the user name isn't the DN), you can first bind either anonymously (if the LDAP server is configured to accept that) or as a known user, search for the DN, then rebind as the user whose account you're trying to check. I suggest using a non-privileged user for the initial search, but of course your administrative user would work too.

"su" Equivalent for Web Application Auth, Design Question

I develop and maintain a customer portal, written in Perl/Catalyst. We make use of the Catalyst authentication plugins (w/ an LDAP storage backend, coupled with a few deny_unless rules to ensure the right people have the right group membership).
It's often that in managing a customer's permissions, we have the need to test out a user's settings before we hand things over. Currently, our only recourse is to reset a user's password and log in ourselves, but this is less than ideal, particularly if the user has already set their own passwords, etc.
My question is this: for Catalyst, has anyone come across a method of impersonating a user account such that, given the correct super-admin privileges, one could impersonate another account temporarily while testing out a setting, and then back out once done?
If not in Catalyst, then how have people approached this in other frameworks, or their own custom solutions? Admittedly, this is something that introduces a potentially egregious attack vector for a web application, but if forced to implement, how have people approached design for this? Perhaps some serious cookie-session-fu? Or possibly an actualID/effectiveID system?
We use a custom authenticator controller, a custom user class (MyApp::Core::User) and several realms:
package MyApp::Controller::Auth;
...
sub surrogate : Local {
my ( $self, $c ) = #_;
my $p = $c->req->params;
my $actual_user = $c->user; # save it for later
try {
$c->authenticate({ id=>$p->{surrogate_id} }, 'none');
$c->session->{user} = new MyApp::Core::User(
active_user => $actual_user,
effective_user => $c->user );
$c->stash->{json} = { success => \1, msg => "Login Ok" };
} catch {
$c->stash->{json} = { success => \0, msg => "Invalid User" };
};
$c->forward('View::JSON');
}
In myapp.conf I use something like this:
<authentication>
default_realm ldap
<realms>
<ldap>
# ldap realm config stuff here
</local>
<none>
<credential>
class Password
password_field password
password_type none
</credential>
<store>
class Null
</store>
</none>
</realms>
</authentication>
That way we're creating a normal Catalyst user object, but wrapping it around our custom user class for more control. I probably could have created an specialized realm for surrogating, but I've chosen using my own user class instead. It was done a while back and I can recall why we did it that way.