Skip to content

Commit

Permalink
Merge branch 'release/v7.0.8-4'
Browse files Browse the repository at this point in the history
  • Loading branch information
nfranzeck authored and cesmarvin committed Nov 13, 2024
2 parents 46ef734 + 5a30a78 commit 2e4defd
Show file tree
Hide file tree
Showing 39 changed files with 1,004 additions and 204 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [v7.0.8-4] - 2024-11-13
### Added
- Replicate users from delegated authentication into LDAP [#224]
- delegated authentication currently only works when using the embedded LDAP
- Disclaimer for legal_urls without protocol [#230]

### Fixed
- Fix configuration for delegated authentication with OIDC [#222]

## [v7.0.8-3] - 2024-10-11
### Changed
- Use flat instead of nested attributes for OAuth user profile. [#219]
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ RUN apk update && apk add wget && wget -O "apache-tomcat-${TOMCAT_VERSION}.tar.
FROM registry.cloudogu.com/official/java:21.0.4-1

LABEL NAME="official/cas" \
VERSION="7.0.8-3" \
VERSION="7.0.8-4" \
maintainer="[email protected]"

ARG TOMCAT_VERSION
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
MAKEFILES_VERSION=9.2.1
MAKEFILES_VERSION=9.3.2

.DEFAULT_GOAL:=dogu-release

Expand Down
3 changes: 1 addition & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ dependencies {
implementation "org.apereo.cas:cas-server-support-oauth-webflow"

implementation "org.apereo.cas:cas-server-support-pac4j-webflow"
implementation "org.apereo.cas:cas-server-support-pac4j-api"

implementation "org.apereo.cas:cas-server-support-pm"
implementation "org.apereo.cas:cas-server-support-pm-core"
Expand All @@ -163,8 +164,6 @@ dependencies {
implementation 'org.mousio:etcd4j:2.18.0'
implementation 'com.googlecode.json-simple:json-simple:1.1.1'
implementation 'org.apache.logging.log4j:log4j-core:2.23.1'
implementation 'org.pac4j:pac4j-core:5.3.1'
implementation 'org.pac4j:pac4j-jee:5.3.1'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
testImplementation 'org.mockito:mockito-core:4.7.0'
testImplementation 'org.hamcrest:hamcrest-all:1.3'
Expand Down
36 changes: 19 additions & 17 deletions app/etc/cas/config/cas.properties
Original file line number Diff line number Diff line change
Expand Up @@ -179,46 +179,48 @@ cas.ticket.tgt.primary.time-to-kill-in-seconds=36000
########################################################################################################################

########################################################################################################################
# OIDC
# OIDC - Provider Mode
# Configuration guide:
# Properties: https://apereo.github.io/cas/6.1.x/configuration/Configuration-Properties-Common.html#delegated-authentication-openid-connect-settings
# Properties: https://apereo.github.io/cas/7.0.x/integration/Delegate-Authentication-Generic-OpenID-Connect.html
# ----------------------------------------------------------------------------------------------------------------------
### path to the discovery url of the provider
cas.authn.pac4j.oidc[0].generic.discovery-uri=https://staging-account.cloudogu.com/auth/realms/Cloudogu/.well-known/openid-configuration

### required configuration
cas.authn.pac4j.oidc[0].generic.useNonce=true
cas.authn.pac4j.oidc[0].generic.enabled=true

### path to the discovery url of the provider
cas.authn.pac4j.oidc[0].generic.discovery-uri=https://staging-account.cloudogu.com/auth/realms/Cloudogu/.well-known/openid-configuration

### name and secret for the client to identify itself by the provider
cas.authn.pac4j.oidc[0].generic.id=my-client-id
cas.authn.pac4j.oidc[0].generic.secret=<addsecret_here>

### required configuration
cas.authn.pac4j.oidc[0].generic.client-authentication-method=client_secret_basic
cas.authn.pac4j.oidc[0].generic.use-nonce=true

### Max clock skew
cas.authn.pac4j.oidc[0].generic.max-clock-skew=5

### the client name used to identify the client in the cas application
cas.authn.pac4j.oidc[0].generic.client-name=MyCloudogu
cas.authn.pac4j.oidc[0].generic.client-name=Cloudogu-Platform

### perform automatic redirects to the configured provider when a user logs into the cas
cas.authn.pac4j.oidc[0].generic.auto-redirect=false

### redirect back to the ces after successful logout
cas.authn.pac4j.oidc[0].generic.target-url=
cas.authn.pac4j.oidc[0].generic.auto-redirect-type=NONE

### information that are supposed to be contained in the responses of the OIDC provider
cas.authn.pac4j.oidc[0].generic.scope=openid email profile groups
cas.authn.pac4j.oidc[0].generic.responseType=code
cas.authn.pac4j.oidc[0].generic.scope=openid email profile roles
cas.authn.pac4j.oidc[0].generic.response-type=code

### preferred algorithm to use for the open id connect jwt tokens
cas.authn.pac4j.oidc[0].generic.preferredJwsAlgorithm=RS256
cas.authn.pac4j.oidc[0].generic.preferred-jws-algorithm=RS256

### the attribute that should be used as the principal id
ces.services.oidcPrincipalsAttribute=username
cas.authn.pac4j.oidc[0].generic.principal-id-attribute=preferred_username

### redirect back to the ces after successful logout
ces.delegation.oidc.redirect-uri=

### attribute mapping
ces.services.attributeMapping=email:mail,family_name:surname,given_name:givenName,preferred_username:username,name:displayName
########################################################################################################################
ces.delegation.oidc.attributeMapping=preferred_username:cn,preferred_username:username,given_name:givenName,family_name:surname,name:displayName,email:mail

########################################################################################################################
# OIDC+OAuth2 - Client Mode
Expand Down
68 changes: 68 additions & 0 deletions app/src/main/java/de/triology/cas/ldap/CesInternalLdapUser.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package de.triology.cas.ldap;

import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import org.ldaptive.LdapEntry;

import java.lang.reflect.Array;
import java.util.HashSet;
import java.util.Set;

@Setter
@Getter
@EqualsAndHashCode
public class CesInternalLdapUser {
public static final String[] ObjectClasses = new String[]{"top", "person", "inetOrgPerson", "organizationalPerson", "cesperson"};

public static final String UidAttribute = "uid";
public static final String CnAttribute = "cn";
public static final String SnAttribute = "sn";
public static final String GivenNameAttribute = "givenname";
public static final String DisplayNameAttribute = "displayName";
public static final String MailAttribute = "mail";
public static final String ExternalAttribute = "external";
public static final String MemberOfAttribute = "memberOf";

private String uid;
private String givenName;
private String familyName;
private String displayName;
private String mail;
private boolean external;
private Set<String> groups;

public CesInternalLdapUser(String uid, String givenName, String familyName, String displayName, String mail, boolean external) {
this.uid = uid;
this.givenName = givenName;
this.familyName = familyName;
this.displayName = displayName;
this.mail = mail;
this.external = external;
this.groups = new HashSet<>();
}

public static CesInternalLdapUser UserFromEntry(LdapEntry entry) {
String uid = entry.getAttribute(CesInternalLdapUser.UidAttribute).getStringValue();
String givenName = entry.getAttribute(CesInternalLdapUser.GivenNameAttribute).getStringValue();
String familyName = entry.getAttribute(CesInternalLdapUser.SnAttribute).getStringValue();
String displayName = entry.getAttribute(CesInternalLdapUser.DisplayNameAttribute).getStringValue();
String mail = entry.getAttribute(CesInternalLdapUser.MailAttribute).getStringValue();

String externalString = entry.getAttribute(CesInternalLdapUser.ExternalAttribute).getStringValue();
boolean external = UserManager.LDAP_TRUE.equals(externalString);

CesInternalLdapUser user = new CesInternalLdapUser(uid, givenName, familyName, displayName, mail, external);

if (entry.getAttribute(CesInternalLdapUser.MemberOfAttribute) != null) {
for (String value : entry.getAttribute(CesInternalLdapUser.MemberOfAttribute).getStringValues()) {
String group = Util.extractGroupNameFromDn(value);
user.groups.add(group);
}
}

return user;
}


}
11 changes: 11 additions & 0 deletions app/src/main/java/de/triology/cas/ldap/CesLdapException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package de.triology.cas.ldap;

public class CesLdapException extends Exception {
public CesLdapException(String msg) {
super(msg);
}

public CesLdapException(String msg, Throwable e) {
super(msg, e);
}
}
27 changes: 27 additions & 0 deletions app/src/main/java/de/triology/cas/ldap/LdapOperationFactory.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package de.triology.cas.ldap;

import org.ldaptive.AddOperation;
import org.ldaptive.ConnectionFactory;
import org.ldaptive.ModifyOperation;
import org.ldaptive.SearchOperation;

public class LdapOperationFactory {

private final ConnectionFactory connectionFactory;

public LdapOperationFactory(ConnectionFactory connectionFactory) {
this.connectionFactory = connectionFactory;
}

public SearchOperation searchOperation() {
return new SearchOperation(connectionFactory);
}

public AddOperation addOperation() {
return new AddOperation(connectionFactory);
}

public ModifyOperation modifyOperation() {
return new ModifyOperation(connectionFactory);
}
}
144 changes: 144 additions & 0 deletions app/src/main/java/de/triology/cas/ldap/UserManager.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package de.triology.cas.ldap;

import lombok.extern.slf4j.Slf4j;
import org.ldaptive.*;

/**
* UserManager can load, create and update {@link CesInternalLdapUser}s.
*/
@Slf4j
public class UserManager {
public static final String LDAP_TRUE = "TRUE";
public static final String LDAP_FALSE = "FALSE";

public static final String ObjectClassAttributeName = "objectClass";

private final String baseDN;
private final LdapOperationFactory operationFactory;

/**
* Creates a new Usermanager that can load, create and update {@link CesInternalLdapUser}s.
*/
public UserManager(String baseDN, LdapOperationFactory operationFactory) {
this.baseDN = baseDN;
this.operationFactory = operationFactory;
}

/**
* Gets the {@link CesInternalLdapUser} for the given UID.
*
* @param uid the UID of the user to get
* @return null if the user for the given UID could not be found
* @throws CesLdapException for errors querying LDAP
*/
public CesInternalLdapUser getUserByUid(String uid) throws CesLdapException {
final SearchResponse response;
try {
final SearchRequest request = createGetUserRequest(uid);
response = operationFactory.searchOperation().execute(request);
} catch (final LdapException e) {
throw new CesLdapException("Failed executing LDAP query", e);
}

if (!response.isSuccess()) {
throw new CesLdapException(response.getDiagnosticMessage());
}

if (response.getEntries().size() > 1) {
throw new CesLdapException("did not expect more then one result");
}

LdapEntry entry = response.getEntry();
if (entry == null) {
return null;
}

return CesInternalLdapUser.UserFromEntry(entry);
}


/**
* Creates a new {@link CesInternalLdapUser}
*
* @param user the user to create
* @throws CesLdapException for errors while creating the user in LDAP
*/
public void createUser(CesInternalLdapUser user) throws CesLdapException {
try {
final AddOperation modify = operationFactory.addOperation();
final AddRequest request = AddRequest.builder()
.dn(createDnForUser(user))
.attributes(
new LdapAttribute(ObjectClassAttributeName, CesInternalLdapUser.ObjectClasses),
new LdapAttribute(CesInternalLdapUser.CnAttribute, user.getUid()),
new LdapAttribute(CesInternalLdapUser.SnAttribute, user.getFamilyName()),
new LdapAttribute(CesInternalLdapUser.GivenNameAttribute, user.getGivenName()),
new LdapAttribute(CesInternalLdapUser.DisplayNameAttribute, user.getDisplayName()),
new LdapAttribute(CesInternalLdapUser.MailAttribute, user.getMail()),
new LdapAttribute(CesInternalLdapUser.ExternalAttribute, user.isExternal() ? LDAP_TRUE : LDAP_FALSE)
)
.build();

final AddResponse response = modify.execute(request);
if (!response.isSuccess()) {
throw new CesLdapException(response.getDiagnosticMessage());
}

} catch (LdapException e) {
throw new CesLdapException("error while creating user", e);
}
}

/**
* Updates the given user in LDAP
*
* @param user the user to update
* @throws CesLdapException for errors while updating the user in LDAP
*/
public void updateUser(CesInternalLdapUser user) throws CesLdapException {
try {
final ModifyOperation modify = operationFactory.modifyOperation();
final ModifyRequest request = ModifyRequest.builder()
.dn(createDnForUser(user))
.modifications(
new AttributeModification(AttributeModification.Type.REPLACE, new LdapAttribute(CesInternalLdapUser.CnAttribute, user.getUid())),
new AttributeModification(AttributeModification.Type.REPLACE, new LdapAttribute(CesInternalLdapUser.SnAttribute, user.getFamilyName())),
new AttributeModification(AttributeModification.Type.REPLACE, new LdapAttribute(CesInternalLdapUser.GivenNameAttribute, user.getGivenName())),
new AttributeModification(AttributeModification.Type.REPLACE, new LdapAttribute(CesInternalLdapUser.DisplayNameAttribute, user.getDisplayName())),
new AttributeModification(AttributeModification.Type.REPLACE, new LdapAttribute(CesInternalLdapUser.MailAttribute, user.getMail()))
)
.build();
final ModifyResponse response = modify.execute(request);

if (!response.isSuccess()) {
throw new CesLdapException(response.getDiagnosticMessage());
}
} catch (LdapException e) {
throw new CesLdapException("error while updating user", e);
}
}

private String createDnForUser(CesInternalLdapUser user) {
return CesInternalLdapUser.UidAttribute + "=" + user.getUid() + "," + this.baseDN;
}

private SearchRequest createGetUserRequest(String uid) {
String filter = String.format("(&%s%s)", uidFilter(uid), externalUsersFilter());

SearchRequest request = new SearchRequest();
request.setBaseDn(this.baseDN);
request.setReturnAttributes(CesInternalLdapUser.UidAttribute, CesInternalLdapUser.CnAttribute, CesInternalLdapUser.SnAttribute, CesInternalLdapUser.GivenNameAttribute, CesInternalLdapUser.DisplayNameAttribute, CesInternalLdapUser.MailAttribute, CesInternalLdapUser.ExternalAttribute, CesInternalLdapUser.MailAttribute, CesInternalLdapUser.ExternalAttribute, CesInternalLdapUser.MemberOfAttribute);
request.setFilter(filter);
request.setSearchScope(SearchScope.SUBTREE);
request.setSizeLimit(1);
return request;
}

private static String externalUsersFilter() {
return String.format("(%s=%s)", CesInternalLdapUser.ExternalAttribute, LDAP_TRUE);
}

private static String uidFilter(String uid) {
return String.format("(%s=%s)", CesInternalLdapUser.UidAttribute, uid);
}
}
20 changes: 20 additions & 0 deletions app/src/main/java/de/triology/cas/ldap/Util.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package de.triology.cas.ldap;

public class Util {
public static String extractGroupNameFromDn(String dn) {
String result = dn;
int eqindex = dn.indexOf('=');
int coindex = dn.indexOf(',');
if (eqindex > 0 && (coindex < 0 || eqindex < coindex) && dn.length() > eqindex + 1) {
dn = dn.substring(eqindex + 1);
coindex = dn.indexOf(',');
if (coindex > 0) {
result = dn.substring(0, coindex);
} else {
result = dn;
}
}
return result;
}

}
Loading

0 comments on commit 2e4defd

Please sign in to comment.