How to notify users before password expires?

ucs-4-3
password

#1

I have a UCS 4.3 PDC and BDC with LinuxMint and MacOS clients joined into the domain.
So far I have not found a way to notify users e.g. via email that their password will soon expire and they should change it.

Anybody solved this already? I did not find anything in that regard in the documentation or the forum.

BR,
Jörn


#2

Hey,

In all my time I haven’t come across a tool/script/solution that implements what you want. UCS itself certainly doesn’t contain one. You’ll have to whip something up yourself if you really need such a feature, e.g. by searching through the LDAP content and checking the corresponding attributes.

Kind regards
mosu


#3

Hello Moritz,
Thanks for your reply. In Windows Domains in corporate environments that is pretty standard. Whenever a password expiry policy exists you get reminders to change your password before it expires.

The environment mention in my first post I did build for a customer. We did set the password expiry policy in UCS to 183 days. And suddenly after that period the customer was no longer able to log-in anymore.

Since I also did not find a way a user could check how long the password is still valid in UMC I was really irritated. What sense does it make to set a password expiry policy if the user does not get a reminder to change it or can see the password age somehow?

Nevertheless I am willing to implement this. But I would like to get some hints which direction I should start looking. Sure I could just write a shell script that I add to crontab. But I’d prefer to get some pointers tom implement it closer to UCS standards.

So how is the password expiry policy implemented? Would it be possible to extend that?
Does UCS have a way to run periodic tasks (besides cronjobs)?

BR,
Jörn


#4

Hey,

For Windows clients this is actually a feature of the client operating system, not of the server side. The client simply evaluates the data present in the Active Directory LDAP and acts accordingly. You usually configure them to show such a prompt via a group policy (see e.g. this article.

In a UCS AD domain that password expiry data is definitely present in the AD LDAP, and that’s pretty much about all Univention can do at that point as neither LinuxMint nor macOS are Univention’s own products (obviously).

The use is to have the server change the password. As far as I remember the web-based Self Service provided by Univention should still allow changing the password even if it’s expired if I’m not mistaken (but don’t take my word).

For this particular type of operation there isn’t that much UCS-specific to observe. Personally I’d write a script in Perl or Ruby (as those are generally my preferred scripting languages) as working with LDAP is easier in “real” scripting languages compared to shell scripts.

Nothing UCS-specific, no, just the standard Linux tools. You can execute that as a cron job or via a systemd timer unit, whatever you prefer (personally I tend to use systemd timer units as listing all available jobs and when they’re going to be executed with systemctl list-timers can be rather helpful).

One thing to note is that in OpenLDAP and Samba4 LDAP have different ways of storing the password policy settings. If you’re working with the OpenLDAP side, you can use the univention-policy-result tool in order to find out the UCS password policy applying to a particular user. Here’s an example:

[0 root@master ~] univention-policy-result -D $(ucr get ldap/hostdn) -y /etc/machine.secret uid=passwordexpiry,cn=users,$(ucr get ldap/base)
DN: uid=passwordexpiry,cn=users,dc=mbu-test,dc=intranet

POLICY uid=passwordexpiry,cn=users,dc=mbu-test,dc=intranet

…
Policy: cn=expiry-test,cn=policies,dc=mbu-test,dc=intranet
Attribute: univentionPWExpiryInterval
Value: 90
…

You can use that information together with the date when the password was last set (OpenLDAP attribute sambaPwdLastSet).

In the AD LDAP part things are stored differently. I don’t know the details from the top of my head, but as UCS provides a standard AD LDAP scheme you should have no difficulties finding out the way they’re stored using your favorite search engine.

Kind regards
mosu


#5

perhaps this project could be the solution: https://github.com/sttts/ldap-notify


#6

That looks like a good starting point. Thanks a lot!


#7

I had a closer look at ldap-notify now. I some points it is not compatible with UCS.

Therefore I created a fork and started working on changes to make it work with UCS. In the issues on GitHub I started to describe the identified issues.

The first commit on branch ucs44 already enables reading and sending mails but storing the notification date requires write permission to an attribute in the LDAP.

To read from the LDAP I created a simple authentication account in UCS. To write I assigned an univentionFreeAttribute to the User object and created an ACL to allow the user I created to write to that univentionFreeAttribute.

Since ldap-notify is written in Python there should be a way to import the Univention modules and gain access that way.
@Moritz_Bunkus, can you give me a pointer how to integrate that?

As I stated before a feature like “Email notification before password expires” would be great out of the box in UCS.


#8

Hey,

you can indeed use Univention’s Python modules for accessing the LDAP objects & make changes to them. The developer documentation contains quite a sizeable amount of information, e.g. how to access the config registry from Python. Unfortunately it doesn’t really go into details how to write command-line programs using the UDM modules, even though that is stupid easy. Here’s an excerpt from one of my scripts that searches for and modifies/creates a DNS A record (“host record” in UDM parlance):

#!/usr/bin/python2.7

import univention.udm
from univention.udm import UDM
from univention.config_registry import ConfigRegistry

ucr = ConfigRegistry()
ucr.load()

def addHostRecord(ip, fwdZone, hostName):
    udmHost       = UDM.admin().version(1).get('dns/host_record')
    udmFwdZone    = UDM.admin().version(1).get('dns/forward_zone')

    baseDn        = ucr['ldap/base']
    forwardZoneDn = "zoneName=" + fwdZone + ",cn=dns," + baseDn

    try:
        forwardZone = udmFwdZone.get(forwardZoneDn)
    except:
        print("ERROR: Forward Zone " + fwdZone + " does not exist")
        return

    hostRecordDn = "relativeDomainName=" + hostName + ",zoneName=" + fwdZone + ",cn=dns," + baseDn
    try:
        hostRecord = udmHost.get(hostRecordDn)
        hostRecord.props.a = ip
        hostRecord.props.name = hostName
        hostRecord.save()
        print("Updated existing host record: Name=" + hostName + " IP=" + ip + " FWD_Zone=" + fwdZone)

    except:
        hostRecord = udmHost.new(hostRecordDn)
        hostRecord.props.a = ip
        hostRecord.props.name = hostName
        hostRecord.save()
        print("Added new host record: Name=" + hostName + " IP=" + ip + " FWD_Zone=" + fwdZone)

That function looks for a forward zone, for an existing host entry in it & either updates its IP address & host name if the entry exists or creates a new one. You can easily modify custom attributes that way, too, simply by assigning values to them, e.g. hostRecord.props.my_custom_attribute = 42. It’s really that simple.

The UDM interface modules automatically use the LDAP admin user account with /etc/ldap.secret, meaning they’ll have to run as root. However, I’m pretty sure you can specify your own credentials somehow.

If you need to run your scripts from a different server, you cannot use the modules directly if I’m not mistaken. However, UCS does offer an https/JSON based interface to all things UDM: it’s the same interface the web frontend (the management console) uses. There’s a whole section about that interface in the developer documentation.

Last but not least Univention announced a REST API a couple of days ago. It’s still in beta but might be worth a look, too.

For anything you intend to run on your DC Master or DC Backup, I’d simply use the Python modules as they’re dead easy to use.

Kind regards,
mosu


#9

Addendum after reading your bug reports:

First of all, thanks for doing this & making it Open Source! I’m sure there are other people who’ll appreciate it.

About LDAP write permissions: normal user accounts may only update a few of their own attributes, nothing else. By default there are only a small number of accounts that may actually modify the whole LDAP:

  1. The LDAP admin account cn=admin,$ldap_base (corresponding password is found in /etc/ldap.secret on your DC Master & DC Backups)
  2. The administrator user account

Other accounts such as the machine accounts for DC Masters & DC Backups (see ucr get ldap/hostdn with corresponding password in /etc/machine.secret) or generally members of the Domain Admins group only have selective write access. See the ACLs in /etc/ldap/slapd.conf for details.

About storing the last notification date in LDAP: this type of data might be better of stored outside of the LDAP tree, e.g. in a file somewhere beneath /var/lib/account-expiry-notify. In general LDAP servers are supposed to store data that is needed by multiple servers & multiple programs. This particular piece of information doesn’t fit either criteria: only your script needs the information, and your script will only ever be run on a single server. Storing the information outside the LDAP server would also get rid of the need for LDAP write access and for a custom attribute to store the information in.


#10

Thank you very much for providing some direction.

That is exacly my intention. I should run DC Master or DC Backup. No need to go through the hassel of remote access to the LDAP.

Good point!
I am not that used to Python, but willing to learn. What is the easiest way store entries with Python in a file? Sqlite?
Its 3 values for each entry: user, rule, timestamp
user + rule is unique.


#11

Hey,

you’re quite welcome.

Probably JSON or YAML. You’ll probably end up with an array of hashes containing your data in-memory. Storage & retrieval would then be: on startup read whole file with all information into memory; before exit write the whole in-memory structure to the file. Here’s an example:

#!/usr/bin/python2.7

import json

dataFileName = '/var/lib/mydata.json'

def readData():
    try:
        with open(dataFileName, 'r') as dataFile:
            return json.load(dataFile)
    except:
        # File doesn't exist yet
        return []

def writeData(data):
    with open(dataFileName, 'w') as dataFile:
        json.dump(data, dataFile)

data = readData()

# do things with data

writeData(data)

This works fine if…

  • the amount of data is manageable (e.g. not bigger than tens of megabytes; it won’t scale if you’re reading & writing hundreds of MB or even GB with each script invocation) and
  • your script is only running for a short amount of time (instead of running all the time as a daemon)

If any of these isn’t met, a method of storing data that allows direct access to each piece of data is preferable as it allows you to keep memory usage down as well as only updating small amounts of data one at a time. SQLite would then be an appropriate choice. You’d need more code for that, obviously.


#12

First step is done. ldap-notify is no longer writing to LDAP. Sent notifications are now stored in a file.
Basically it already works that way.

By integrating the Univention modules in the next step the requirement for a LDAP account can be removed.