Sunday, June 6, 2010

OpenLDAP Password Permissions Configuration Example

Following my post on OpenLDAP under Ubuntu Linux Lucid Lynx 10.04, I'd like to address a question submitted as a comment submitted by "raerek":

I plan to set up things like this:
-Group A: members can change the password of anyone, except the password of other Group A members.
-Group B: members can change their own passwords, and the passwords of Group C members.
-Group C: members can only change their own password.

Can you outline how to write the "olcAccess: to" lines - If I do not have to, I would not like to set up three OU-s - I'd rather have everyone in the ou=people.

While I cannot guarantee a response to every question, this one caught my interest - especially due to some of the well-advised reasons I'm guessing are behind the stated requirements.

The "Group A" requirement in particular looked rather complicated when I first looked at this, as the "... anyone, except ..." would normally be addressed with compound conditionals - and OpenLDAP doesn't provide for this - at least not as many would expect. As with many technical issues, the best approach here seems to be in simplifying the functional requirements:

  1. Anyone can change their own password.
    • Is specified by both the original "Group A" and "Group B" requirements, and completely satisfies the original "Group C" requirement.
    • "Anyone" should not necessarily apply to "service" or "machine" accounts, unless such an account is specifically known or required to change its own password.
  2. "Group A" members cannot change the passwords of other "Group A" members.
  3. "Group A" members can change the passwords of everyone else (besides other "Group A" members, as listed above in #2).
  4. "Group B" members can change the passwords of "Group C" members.
  5. Unless otherwise specified above, deny any other access to passwords, except for accounts to read their own password (probably not always even necessary) and for anonymous (an account before authenticated) to authenticate against.

Here is my entire proposed solution, with notes to follow. First, an LDIF export of a setup to demonstrate the example:

dn: dc=example,dc=com
dc: Example
description: LDAP Example 
o: Example Organization
objectclass: top
objectclass: dcObject
objectclass: organization

dn: ou=groups,dc=example,dc=com
objectclass: organizationalUnit
ou: groups

dn: cn=groupA,ou=groups,dc=example,dc=com
cn: groupA
description: members can change the password of anyone, except the password 
 of other Group A members.
member: cn=groupOfNamesPlaceHolder,ou=groups,dc=example,dc=com
member: cn=groupA_user1,ou=people,dc=example,dc=com
member: cn=groupA_user2,ou=people,dc=example,dc=com
objectclass: groupOfNames

dn: cn=groupB,ou=groups,dc=example,dc=com
cn: groupB
description: members can change their own passwords, and the passwords of Gr
 oup C members.
member: cn=groupOfNamesPlaceHolder,ou=groups,dc=example,dc=com
member: cn=groupB_user1,ou=people,dc=example,dc=com
member: cn=groupB_user2,ou=people,dc=example,dc=com
objectclass: groupOfNames

dn: cn=groupC,ou=groups,dc=example,dc=com
cn: groupC
description: members can only change their own password.
member: cn=groupOfNamesPlaceHolder,ou=groups,dc=example,dc=com
member: cn=groupC_user1,ou=people,dc=example,dc=com
member: cn=groupC_user2,ou=people,dc=example,dc=com
objectclass: groupOfNames

dn: cn=groupOfNamesPlaceHolder,ou=groups,dc=example,dc=com
cn: groupOfNamesPlaceHolder
objectclass: applicationProcess

dn: ou=people,dc=example,dc=com
objectclass: organizationalUnit
ou: people

dn: cn=groupA_user1,ou=people,dc=example,dc=com
cn: groupA_user1
objectclass: inetOrgPerson
objectclass: simpleSecurityObject
sn: Test
userpassword: secret

dn: cn=groupA_user2,ou=people,dc=example,dc=com
cn: groupA_user2
objectclass: inetOrgPerson
objectclass: simpleSecurityObject
sn: Test
userpassword: secret

dn: cn=groupB_user1,ou=people,dc=example,dc=com
cn: groupB_user1
objectclass: inetOrgPerson
objectclass: simpleSecurityObject
sn: Test
userpassword: secret

dn: cn=groupB_user2,ou=people,dc=example,dc=com
cn: groupB_user2
objectclass: inetOrgPerson
objectclass: simpleSecurityObject
sn: Test
userpassword: secret

dn: cn=groupC_user1,ou=people,dc=example,dc=com
cn: groupC_user1
objectclass: inetOrgPerson
objectclass: simpleSecurityObject
sn: Test
userpassword: secret

dn: cn=groupC_user2,ou=people,dc=example,dc=com
cn: groupC_user2
objectclass: inetOrgPerson
objectclass: simpleSecurityObject
sn: Test
userpassword: secret

Note that the userpassword's are in clear text for the purposes of this example. They should normally be stored using a cryptographic hash such as salted SHA (SSHA) for proper security.

Next are the olcAccess rules:

# Req. #1 - Any user in the people OU can change their own password.
olcaccess: to dn.subtree="ou=people,dc=example,dc=com"
  attrs=userPassword
  by self write
  by * break
# Req. #2 - "Group A" members cannot change the passwords of other "Group A" members.
olcaccess: to filter=(memberof=cn=groupA,ou=groups,dc=example,dc=com)
  attrs=userPassword
  by group.exact="cn=groupA,ou=groups,dc=example,dc=com" none
  by * break
# Req. #3 - "Group A" members can change the passwords of everyone else.
olcaccess: to attrs=userPassword
  by group.exact="cn=groupA,ou=groups,dc=example,dc=com" write
  by * break
# Req. #4 - "Group B" members can change the passwords of "Group C" members.
olcaccess: to filter=(memberof=cn=groupC,ou=groups,dc=example,dc=com)
  attrs=userPassword
  by group.exact="cn=groupB,ou=groups,dc=example,dc=com" write
  by * break
# Req. #5 - Unless otherwise specified above, deny any other access to passwords,
#   except for accounts to read their own password (probably not always even necessary)
#   and for anonymous (an account before authenticated) to authenticate against.
olcaccess: to attrs=userPassword
  by self read
  by anonymous auth
  by * none

First, note that this requires use of the "memberof" overlay, as detailed in section "12.8. Reverse Group Membership Maintenance" in the OpenLDAP Administrator's Guide. Without it, the "memberof" attributes will not exist, and the required filters will not work. Also note that as provided by the current version of the overlay, the "memberof" attributes are stored rather than simply calculated when requested (probably for performance reasons), and are only updated when the entry is involved in a modification of group membership. As such, if the overlay is added to a database with existing populated groups, the groups will likely have to be re-populated for the "memberof" attribute to reflect the current and expected results. In testing, any update to the "member" attribute on a group - even only the addition or removal of one member - will cause the overlay to update the "memberof" attribute on all other members in the group. Also related to performance, make sure that "memberof" is indexed after enabling the overlay.

Next, note that most rules are terminated by a "by * break" clause. This allows following rules to be considered, without first terminating with the "by * none" clause that is implicitly added at the end of every rule, as detailed in section "8.2.4. Access Control Evaluation" in the OpenLDAP Administrator's Guide.

No work is complete without a good test script. This is also a good reference for how to change account passwords using ldapmodify. Here is the test source as a bash shell script:

#!/bin/bash

# Mark A. Ziesemer, http://www.ziesemer.com
# 2010-06-06

_cpw(){
  # "Change PassWord"
  local bindUser=$1
  local bindPW=$2
  local changeUser=$3
  local newPW=$4
  declare -i expected=$5 # optional, defaults to 0

  declare -i result=0
  echo "% _cpw $*"
  echo "
    dn: $changeUser
    changetype: modify
    replace: userpassword
    userpassword: $newPW" | ldapmodify -D "$bindUser" -xw "$bindPW" || result=$?
  if [ $result -ne $expected ]; then
    echo -e "\t* Test FAILED, exit code: $result"
    if [ $result -gt 0 ]; then return $result; fi
    return 1
  fi
  echo -e "\t* Test passed, exit code: $result"
  return 0
}

_testTitle(){
  echo -e "\n*** $1\n"
}

_defSuffix="dc=example,dc=com"
_peopleOU="ou=people,${_defSuffix}"
_groupA_user1="cn=groupA_user1,${_peopleOU}"
_groupA_user2="cn=groupA_user2,${_peopleOU}"
_groupB_user1="cn=groupB_user1,${_peopleOU}"
_groupB_user2="cn=groupB_user2,${_peopleOU}"
_groupC_user1="cn=groupC_user1,${_peopleOU}"
_groupC_user2="cn=groupC_user2,${_peopleOU}"

_testTitle "Testing requirement #1: Anyone can change their own password."
_cpw "${_groupA_user1}" "secret" "${_groupA_user1}" "secret1"
_cpw "${_groupA_user1}" "secret1" "${_groupA_user1}" "secret"
_cpw "${_groupB_user1}" "secret" "${_groupB_user1}" "secret1"
_cpw "${_groupB_user1}" "secret1" "${_groupB_user1}" "secret"
_cpw "${_groupC_user1}" "secret" "${_groupC_user1}" "secret1"
_cpw "${_groupC_user1}" "secret1" "${_groupC_user1}" "secret"

_testTitle "Testing requirement #2: \"Group A\" members cannot change the passwords of other \"Group A\" members."
_cpw "${_groupA_user1}" "secret" "${_groupA_user2}" "secret1" 50

_testTitle "Testing requirement #3: \"Group A\" members can change the passwords of everyone else."
_cpw "${_groupA_user1}" "secret" "${_groupB_user1}" "secret1"
_cpw "${_groupA_user1}" "secret" "${_groupB_user1}" "secret"
_cpw "${_groupA_user1}" "secret" "${_groupC_user1}" "secret1"
_cpw "${_groupA_user1}" "secret" "${_groupC_user1}" "secret"

_testTitle "Testing requirement #4: \"Group B\" members can change the passwords of \"Group C\" members."
_cpw "${_groupB_user1}" "secret" "${_groupC_user1}" "secret1"
_cpw "${_groupB_user1}" "secret" "${_groupC_user1}" "secret"
_cpw "${_groupB_user1}" "secret" "${_groupB_user2}" "secret1" 50

_testTitle "Testing requirement #5: Deny other access."
_cpw "${_groupC_user1}" "secret" "${_groupA_user1}" "secret1" 50
_cpw "${_groupC_user1}" "secret" "${_groupB_user1}" "secret1" 50
_cpw "${_groupC_user1}" "secret" "${_groupC_user2}" "secret1" 50

With the associated output from a successful run:

*** Testing requirement #1: Anyone can change their own password.

% _cpw cn=groupA_user1,ou=people,dc=example,dc=com secret cn=groupA_user1,ou=people,dc=example,dc=com secret1
modifying entry "cn=groupA_user1,ou=people,dc=example,dc=com"

        * Test passed, exit code: 0
% _cpw cn=groupA_user1,ou=people,dc=example,dc=com secret1 cn=groupA_user1,ou=people,dc=example,dc=com secret
modifying entry "cn=groupA_user1,ou=people,dc=example,dc=com"

        * Test passed, exit code: 0
% _cpw cn=groupB_user1,ou=people,dc=example,dc=com secret cn=groupB_user1,ou=people,dc=example,dc=com secret1
modifying entry "cn=groupB_user1,ou=people,dc=example,dc=com"

        * Test passed, exit code: 0
% _cpw cn=groupB_user1,ou=people,dc=example,dc=com secret1 cn=groupB_user1,ou=people,dc=example,dc=com secret
modifying entry "cn=groupB_user1,ou=people,dc=example,dc=com"

        * Test passed, exit code: 0
% _cpw cn=groupC_user1,ou=people,dc=example,dc=com secret cn=groupC_user1,ou=people,dc=example,dc=com secret1
modifying entry "cn=groupC_user1,ou=people,dc=example,dc=com"

        * Test passed, exit code: 0
% _cpw cn=groupC_user1,ou=people,dc=example,dc=com secret1 cn=groupC_user1,ou=people,dc=example,dc=com secret
modifying entry "cn=groupC_user1,ou=people,dc=example,dc=com"

        * Test passed, exit code: 0

*** Testing requirement #2: "Group A" members cannot change the passwords of other "Group A" members.

% _cpw cn=groupA_user1,ou=people,dc=example,dc=com secret cn=groupA_user2,ou=people,dc=example,dc=com secret1 50
modifying entry "cn=groupA_user2,ou=people,dc=example,dc=com"
ldap_modify: Insufficient access (50)

        * Test passed, exit code: 50

*** Testing requirement #3: "Group A" members can change the passwords of everyone else.

% _cpw cn=groupA_user1,ou=people,dc=example,dc=com secret cn=groupB_user1,ou=people,dc=example,dc=com secret1
modifying entry "cn=groupB_user1,ou=people,dc=example,dc=com"

        * Test passed, exit code: 0
% _cpw cn=groupA_user1,ou=people,dc=example,dc=com secret cn=groupB_user1,ou=people,dc=example,dc=com secret
modifying entry "cn=groupB_user1,ou=people,dc=example,dc=com"

        * Test passed, exit code: 0
% _cpw cn=groupA_user1,ou=people,dc=example,dc=com secret cn=groupC_user1,ou=people,dc=example,dc=com secret1
modifying entry "cn=groupC_user1,ou=people,dc=example,dc=com"

        * Test passed, exit code: 0
% _cpw cn=groupA_user1,ou=people,dc=example,dc=com secret cn=groupC_user1,ou=people,dc=example,dc=com secret
modifying entry "cn=groupC_user1,ou=people,dc=example,dc=com"

        * Test passed, exit code: 0

*** Testing requirement #4: "Group B" members can change the passwords of "Group C" members.

% _cpw cn=groupB_user1,ou=people,dc=example,dc=com secret cn=groupC_user1,ou=people,dc=example,dc=com secret1
modifying entry "cn=groupC_user1,ou=people,dc=example,dc=com"

        * Test passed, exit code: 0
% _cpw cn=groupB_user1,ou=people,dc=example,dc=com secret cn=groupC_user1,ou=people,dc=example,dc=com secret
modifying entry "cn=groupC_user1,ou=people,dc=example,dc=com"

        * Test passed, exit code: 0
% _cpw cn=groupB_user1,ou=people,dc=example,dc=com secret cn=groupB_user2,ou=people,dc=example,dc=com secret1 50
modifying entry "cn=groupB_user2,ou=people,dc=example,dc=com"
ldap_modify: Insufficient access (50)

        * Test passed, exit code: 50

*** Testing requirement #5: Deny other access.

% _cpw cn=groupC_user1,ou=people,dc=example,dc=com secret cn=groupA_user1,ou=people,dc=example,dc=com secret1 50
modifying entry "cn=groupA_user1,ou=people,dc=example,dc=com"
ldap_modify: Insufficient access (50)

        * Test passed, exit code: 50
% _cpw cn=groupC_user1,ou=people,dc=example,dc=com secret cn=groupB_user1,ou=people,dc=example,dc=com secret1 50
modifying entry "cn=groupB_user1,ou=people,dc=example,dc=com"
ldap_modify: Insufficient access (50)

        * Test passed, exit code: 50
% _cpw cn=groupC_user1,ou=people,dc=example,dc=com secret cn=groupC_user2,ou=people,dc=example,dc=com secret1 50
modifying entry "cn=groupC_user2,ou=people,dc=example,dc=com"
ldap_modify: Insufficient access (50)

        * Test passed, exit code: 50

Additional references:

2 comments:

raerek said...

Hi Mark,
I would like to express my gratitude for not only simply answering my question, but doing it by taking a deep dive in the subject.
(By the way, this time you helped out a sencondary school teacher of informatics from Hungary, and next year a whole school will benefit from the time you've spent on your LDAP articles - although I'll be the only one who cares:))
Thanks again.
raerek

Daniel said...

Compound conditionals can be expressed through sets. For example, the following rule means "if the user is the manager of the owner of the object, and he belongs to either Marketing or Sales":

this/owner/manager & ([cn=Marketing]/member* | [cn=Sales]/member*) & user