Skip to content

Commit

Permalink
cas - adding a sample compo to test password policies
Browse files Browse the repository at this point in the history
  • Loading branch information
pmauduit committed Sep 20, 2024
1 parent e1a88f5 commit 69f20f7
Show file tree
Hide file tree
Showing 6 changed files with 267 additions and 0 deletions.
48 changes: 48 additions & 0 deletions cas/password-expiration/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# About

This docker composition provides a way to test the password rotation policy
from the LDAP, and how it affects the templates on CAS when one to try to log
in with an expired account.

# Ports

* CAS is available on port 8080
* LDAP is available on port 3389

# scenario

a georchestra/ldap is launched along with the environment variable
`SLAPD_PASSWORD_MGT_POLICY` set to `rotation`.

When the LDAP server bootstraps, a `docker-entrypoint.d` init script injects a
new password policy which makes the passwords expiring after 2 seconds, and
affects this new policy to the `testuser` account.

2 seconds is probably enough to have the passwords for the user expired before
being able to log in as `testuser`.

After having launched the composition, if you visit
`http://localhost:8080/cas/login`, and try to connect as `testuser/testuser`,
you should see a message telling you that your password expired, along with a
link to the console (not provided by the composition) so that you could reset
it.

# More technical details

The password policy management in OpenLDAP is performed via an overlay, see
[official documentation](https://www.openldap.org/devel/admin/overlays.html),
chapter 12.10.

Once the overlay is loaded into `slapd`, you can add the `pwdPolicySubentry`
operational attribute to your users, and the attribute has to point onto an
existing policy, loaded under `ou=pwpolicy,dc=georchestra,dc=org`.

geOrchestra provides 2 password policies by default:

* a `cn=pwd-no-expire`, mainly reserved to bot accounts, which does not
define an expiration date for passwords (`pwdMaxAge: 0`).
* a `cn=default`, which makes the user passwords expiring after 6 months.

As the `pwdPolicySubentry` is an operational attribute (managed by slapd and
not complying to the regular LDAP schemas), the attribute won't appear when
listing the users' entry as LDIF, if not requested explicitely.
20 changes: 20 additions & 0 deletions cas/password-expiration/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
services:
ldap:
image: georchestra/ldap:latest
environment:
- SLAPD_PASSWORD_MGT_POLICY=rotation
ulimits:
nofile:
soft: "1024"
hard: "1024"
volumes:
- ./resources/ldap/docker-entrypoint.d/99-update-password-expiration.sh:/docker-entrypoint.d/99-update-password-expiration.sh
ports:
- "3389:389"

cas:
image: georchestra/cas:latest
volumes:
- ./resources/datadir:/etc/georchestra
ports:
- "8080:8080"
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
cas.server.name=http://localhost:8080
cas.server.prefix=${cas.server.name}/cas

logging.config=file:/etc/georchestra/cas/config/log4j2.xml
cas.theme.param-name=georchestra
cas.theme.default-theme-name=georchestra

cas.service-registry.core.init-from-json=false
cas.service-registry.json.location=file:/etc/georchestra/cas/services
#uncomment if getting 302 redirects on cas.{css,js} behind nginx/apache
server.forward-headers-strategy=FRAMEWORK

cas.authn.accept.enabled=false

cas.authn.ldap[0].ldap-url=ldap://ldap:389/
cas.authn.ldap[0].bind-dn=
cas.authn.ldap[0].bind-credential=

cas.authn.ldap[0].base-dn=dc=georchestra,dc=org
cas.authn.ldap[0].subtree-search=true
cas.authn.ldap[0].search-filter=uid={user}
cas.authn.ldap[0].page-size=0

cas.authn.ldap[0].pool-passivator=NONE
cas.authn.ldap[0].connection-strategy=
cas.authn.ldap[0].connect-timeout=PT5S
cas.authn.ldap[0].disable-pooling=false
cas.authn.ldap[0].min-pool-size=3
cas.authn.ldap[0].max-pool-size=10
cas.authn.ldap[0].validate-on-checkout=true
cas.authn.ldap[0].validate-periodically=true
cas.authn.ldap[0].validate-period=PT5M
cas.authn.ldap[0].validate-timeout=PT5S
cas.authn.ldap[0].fail-fast=true
cas.authn.ldap[0].idle-time=PT10M
cas.authn.ldap[0].prune-period=PT2H
cas.authn.ldap[0].block-wait-time=PT3S

cas.authn.ldap[0].use-start-tls=false
cas.authn.ldap[0].response-timeout=PT5S
cas.authn.ldap[0].allow-multiple-dns=false
cas.authn.ldap[0].allow-multiple-entries=false
cas.authn.ldap[0].follow-referrals=false
cas.authn.ldap[0].binary-attributes=jpegPhoto
cas.authn.ldap[0].name=

cas.authn.ldap[0].type=DIRECT
cas.authn.ldap[0].dn-format=uid=%s,ou=users,dc=georchestra,dc=org

cas.authn.oidc.jwks.file-system.jwks-file=file:///tmp/keystore.jwksdown
cas.authn.saml-idp.metadata.file-system.location=file:///tmp/
90 changes: 90 additions & 0 deletions cas/password-expiration/resources/datadir/cas/config/log4j2.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!-- Specify the refresh internal in seconds. -->
<Configuration monitorInterval="5" packages="org.apereo.cas.logging">
<Properties>
<Property name="baseDir">/var/log</Property>
<Property name="cas.log.level">info</Property>
<Property name="spring.webflow.log.level">warn</Property>
<Property name="spring.security.log.level">info</Property>
<Property name="spring.cloud.log.level">warn</Property>
<Property name="spring.boot.admin.log.level">debug</Property>
<Property name="spring.web.log.level">warn</Property>
<Property name="spring.boot.log.level">warn</Property>
<Property name="ldap.log.level">warn</Property>
<Property name="pac4j.log.level">warn</Property>
<Property name="opensaml.log.level">warn</Property>
<Property name="hazelcast.log.level">warn</Property>
</Properties>
<Appenders>
<Console name="console" target="SYSTEM_OUT">
<PatternLayout pattern="%highlight{%d %p [%c] - &lt;%m&gt;}%n"/>
</Console>

<CasAppender name="casConsole">
<AppenderRef ref="console" />
</CasAppender>
</Appenders>
<Loggers>
<!-- If adding a Logger with level set higher than warn, make category as selective as possible -->
<!-- Loggers inherit appenders from Root Logger unless additivity is false -->
<AsyncLogger name="org.apereo" level="${sys:cas.log.level}" includeLocation="true"/>
<AsyncLogger name="org.apereo.services.persondir" level="${sys:cas.log.level}" includeLocation="true"/>
<AsyncLogger name="org.apereo.cas.services" level="warn" includeLocation="true"/>
<AsyncLogger name="org.apereo.cas.web.flow" level="${sys:cas.log.level}" includeLocation="true"/>
<AsyncLogger name="org.apereo.spring" level="${sys:cas.log.level}" includeLocation="true"/>

<AsyncLogger name="org.apache" level="warn" />
<AsyncLogger name="org.apache.http" level="error" />

<AsyncLogger name="org.springframework.boot" level="${sys:spring.boot.log.level" includeLocation="true"/>
<AsyncLogger name="org.springframework.boot.context.embedded" level="info" includeLocation="true" />
<AsyncLogger name="org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration"
level="${sys:spring.security.log.level}" includeLocation="true" />
<AsyncLogger name="org.springframework.boot.autoconfigure.security" level="${sys:spring.security.log.level}" includeLocation="true"/>
<AsyncLogger name="org.springframework.boot.devtools" level="off" includeLocation="true"/>

<AsyncLogger name="org.springframework" level="warn" includeLocation="true" />
<AsyncLogger name="org.springframework.webflow" level="${sys:spring.webflow.log.level}" includeLocation="true"/>
<AsyncLogger name="org.springframework.aop" level="warn" includeLocation="true" />
<AsyncLogger name="org.springframework.web" level="warn" includeLocation="true"/>
<AsyncLogger name="org.springframework.session" level="warn" includeLocation="true"/>
<AsyncLogger name="org.springframework.scheduling" level="info" includeLocation="true"/>
<AsyncLogger name="org.springframework.cloud.vault" level="warn" includeLocation="true" />
<AsyncLogger name="org.springframework.web.client" level="warn" includeLocation="true" />
<AsyncLogger name="org.springframework.security" level="${sys:spring.security.log.level}" includeLocation="true"/>
<AsyncLogger name="org.springframework.cloud" level="${sys:spring.cloud.log.level}" includeLocation="true"/>
<AsyncLogger name="org.springframework.amqp" level="error" />
<AsyncLogger name="org.springframework.integration" level="warn" includeLocation="true"/>
<AsyncLogger name="org.springframework.messaging" level="warn" includeLocation="true"/>
<AsyncLogger name="org.springframework.web" level="${sys:spring.web.log.level}" includeLocation="true"/>
<AsyncLogger name="org.springframework.orm.jpa" level="warn" includeLocation="true"/>
<AsyncLogger name="org.springframework.scheduling" level="warn" includeLocation="true"/>
<AsyncLogger name="org.springframework.context.annotation" level="off" includeLocation="true"/>
<AsyncLogger name="org.springframework.web.socket" level="warn" includeLocation="true"/>

<AsyncLogger name="org.thymeleaf" level="warn" includeLocation="true"/>

<AsyncLogger name="org.pac4j" level="${sys:pac4j.log.level}" includeLocation="true"/>

<AsyncLogger name="org.opensaml" level="${sys:opensaml.log.level}" includeLocation="true"/>
<AsyncLogger name="PROTOCOL_MESSAGE" level="${sys:opensaml.log.level}" includeLocation="true" />

<AsyncLogger name="net.sf.ehcache" level="warn" includeLocation="true"/>
<AsyncLogger name="com.couchbase" level="warn" includeLocation="true"/>
<AsyncLogger name="de.codecentric" level="${sys:spring.boot.admin.log.level}" includeLocation="true"/>
<AsyncLogger name="net.jradius" level="warn" includeLocation="true" />
<AsyncLogger name="org.openid4java" level="warn" includeLocation="true" />
<AsyncLogger name="org.ldaptive" level="${sys:ldap.log.level}" includeLocation="true"/>
<AsyncLogger name="com.hazelcast" level="${sys:hazelcast.log.level}" includeLocation="true"/>


<!-- All Loggers inherit appenders specified here, unless additivity="false" on the Logger -->
<AsyncRoot level="warn">
<!--
For deployment to an application server running as service,
delete the casConsole appender below
-->
<AppenderRef ref="casConsole"/>
</AsyncRoot>
</Loggers>
</Configuration>
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#!/bin/bash

# start slapd in background
echo -n "Starting slapd daemon in background..."
slapd -u ${RUN_AS_UID} -g ${RUN_AS_GID} -h "ldapi:/// ldap://127.0.0.1/"
echo "Started: OK"

echo -n "Waiting for LDAP deamon to start..."
while true; do
sleep 1
ldapsearch -x >/dev/null 2>&1
if [ $? -eq 0 ]; then
break
fi
done;
echo "Waiting: OK"


echo "Adding a 2seconds password expiration policy"

cat <<EOF | ldapadd -Dcn=admin,dc=georchestra,dc=org -w ${SLAPD_PASSWORD} -x
dn: cn=2secondsexpiration,ou=pwpolicy,dc=georchestra,dc=org
objectClass: person
objectClass: pwdPolicy
objectClass: pwdPolicyChecker
cn: default
cn: pwpolicy
pwdAttribute: userPassword
sn: pwpolicy
pwdExpireWarning: 2592000
pwdGraceAuthNLimit: 0
pwdMaxAge: 2
pwdMinAge: 0
EOF

echo "Affecting the 2seconds password expiration policy to testuser"

cat <<EOF | ldapmodify -Dcn=admin,dc=georchestra,dc=org -w ${SLAPD_PASSWORD} -x
dn: uid=testuser,ou=users,dc=georchestra,dc=org
changetype: modify
add: pwdPolicySubentry
pwdPolicySubentry: cn=2secondsexpiration,ou=pwpolicy,dc=georchestra,dc=org
EOF

pkill slapd

# wait for ldap to stop
echo -n "Waiting for LDAP to stop..."
while true; do
sleep 1
pgrep slapd >/dev/null 2>&1
if [ $? -ne 0 ]; then
break
fi
done;
echo "Waiting: OK"

0 comments on commit 69f20f7

Please sign in to comment.