Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@

import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.logging.Level;
Expand All @@ -65,6 +66,13 @@
* @author Kohsuke Kawaguchi
*/
public class ActiveDirectoryAuthenticationProvider extends AbstractActiveDirectoryAuthenticationProvider {

/**
* See https://docs.microsoft.com/en-us/windows/desktop/adsi/example-code-for-reading-a-constructed-attribute
* And https://issues.jenkins-ci.org/browse/JENKINS-10086
*/
private static final int E_ADS_PROPERTY_NOT_FOUND = 0x8000_500D;

private final String defaultNamingContext;
/**
* ADO connection for searching Active Directory.
Expand Down Expand Up @@ -190,7 +198,9 @@ public UserDetails call() {
return new ActiveDirectoryUserDetail(
username, password,
!isAccountDisabled(usr),
true, true, true,
!isAccountExpired(usr),
!isPasswordExpired(usr),
!isAccountLocked(usr),
groups.toArray(new GrantedAuthority[0]),
getFullName(usr), getEmailAddress(usr), getTelephoneNumber(usr)
).updateUserInfo();
Expand Down Expand Up @@ -224,8 +234,9 @@ private String getTelephoneNumber(IADsUser usr) {
Object t = usr.telephoneNumber();
return t==null ? null : t.toString();
} catch (ComException e) {
if (e.getHRESULT()==0x8000500D) // see http://support.microsoft.com/kb/243440
if (e.getHRESULT() == E_ADS_PROPERTY_NOT_FOUND) {
return null;
}
throw e;
}
}
Expand All @@ -234,8 +245,9 @@ private String getEmailAddress(IADsUser usr) {
try {
return usr.emailAddress();
} catch (ComException e) {
if (e.getHRESULT()==0x8000500D) // see http://support.microsoft.com/kb/243440
if (e.getHRESULT() == E_ADS_PROPERTY_NOT_FOUND){
return null;
}
throw e;
}
}
Expand All @@ -244,8 +256,9 @@ private String getFullName(IADsUser usr) {
try {
return usr.fullName();
} catch (ComException e) {
if (e.getHRESULT()==0x8000500D) // see http://support.microsoft.com/kb/243440
if (e.getHRESULT() == E_ADS_PROPERTY_NOT_FOUND) {
return null;
}
throw e;
}
}
Expand All @@ -254,13 +267,50 @@ private boolean isAccountDisabled(IADsUser usr) {
try {
return usr.accountDisabled();
} catch (ComException e) {
if (e.getHRESULT()==0x8000500D)
/*
See http://support.microsoft.com/kb/243440 and JENKINS-10086
We suspect this to be caused by old directory items that do not have this value,
so assume this account is enabled.
*/
if (e.getHRESULT() == E_ADS_PROPERTY_NOT_FOUND) {
return false;
}
throw e;
}
}

private boolean isAccountExpired(IADsUser usr) {
try {
Date expirationDate = usr.accountExpirationDate();
if (expirationDate != null) {
return new Date().after(expirationDate);
}
return false;
} catch (ComException e) {
if (e.getHRESULT() == E_ADS_PROPERTY_NOT_FOUND) {
return false;
}
throw e;
}
}

private boolean isPasswordExpired(IADsUser usr) {
try {
Date expirationDate = usr.passwordExpirationDate();
if (expirationDate != null) {
return new Date().after(expirationDate);
}
return false;
} catch (ComException e) {
if (e.getHRESULT() == E_ADS_PROPERTY_NOT_FOUND) {
return false;
}
throw e;
}
}

private boolean isAccountLocked(IADsUser usr) {
try {
return usr.isAccountLocked();
} catch (ComException e) {
if (e.getHRESULT() == E_ADS_PROPERTY_NOT_FOUND) {
return false;
}
throw e;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,9 @@
import javax.naming.directory.SearchResult;
import javax.naming.ldap.LdapName;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
Expand Down Expand Up @@ -420,7 +422,14 @@ public UserDetails call() throws AuthenticationException, NamingException {
Set<GrantedAuthority> groups = resolveGroups(domainDN, dnFormatted, context);
groups.add(SecurityRealm.AUTHENTICATED_AUTHORITY);

cacheMiss[0] = new ActiveDirectoryUserDetail(username, password, true, true, true, true, groups.toArray(new GrantedAuthority[0]),
boolean isEnabled = UserAttributesHelper.checkIfUserIsEnabled(user);
boolean isAccountNonExpired = UserAttributesHelper.checkIfAccountNonExpired(user);
boolean areCredentialsNotExpired = UserAttributesHelper.checkIfCredentialsAreNonExpired(user);
boolean isAccountNonLocked = UserAttributesHelper.checkIfAccountNonLocked(user);

cacheMiss[0] = new ActiveDirectoryUserDetail(username, password,
isEnabled, isAccountNonExpired, areCredentialsNotExpired, isAccountNonLocked,
groups.toArray(new GrantedAuthority[0]),
getStringAttribute(user, "displayName"),
getStringAttribute(user, "mail"),
getStringAttribute(user, "telephoneNumber")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
/*
* The MIT License
*
* Copyright (c) 2019, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package hudson.plugins.active_directory;

import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;

import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.concurrent.TimeUnit;

/**
* Ease all the computations required to determine the user account optional attributes for creating
* the UserDetails that will be used by the SecurityRealm
*/
@Restricted(NoExternalUse.class)
public class UserAttributesHelper {
Comment thread
Wadeck marked this conversation as resolved.
// https://support.microsoft.com/en-us/help/305144/how-to-use-the-useraccountcontrol-flags-to-manipulate-user-account-pro
private static final String ATTR_USER_ACCOUNT_CONTROL = "userAccountControl";
private static final String ATTR_ACCOUNT_EXPIRES = "accountExpires";
// for Windows Server 2003-based domain
private static final String ATTR_USER_ACCOUNT_CONTROL_COMPUTED = "msDS-User-Account-Control-Computed";
// for ADAM (Active Directory Application Mode), replace the ADS_UF_DISABLED
private static final String ATTR_USER_ACCOUNT_DISABLED = "msDS-UserAccountDisabled";
// for ADAM, replace the ADS_UF_PASSWORD_EXPIRED
private static final String ATTR_USER_PASSWORD_EXPIRED = "msDS-UserPasswordExpired";

// https://docs.microsoft.com/en-us/windows/desktop/adschema/a-accountexpires
// constant names follow the code in Iads.h
private static final long ACCOUNT_NO_EXPIRATION = 0x7FFF_FFFF_FFFF_FFFFL;
private static final int ADS_UF_DISABLED = 0x0002;
private static final int ADS_UF_LOCK_OUT = 0x0010;
private static final int ADS_UF_PASSWORD_EXPIRED = 0x80_0000;

public static boolean checkIfUserIsEnabled(@Nonnull Attributes user) {
try {
String userAccountControl = getStringAttribute(user, ATTR_USER_ACCOUNT_CONTROL);
if (userAccountControl != null) {
int uacAsInt = Integer.parseInt(userAccountControl);
if ((uacAsInt & ADS_UF_DISABLED) == ADS_UF_DISABLED) {
return false;
}
}

String adamUserAccountDisabled = getStringAttribute(user, ATTR_USER_ACCOUNT_DISABLED);
if (adamUserAccountDisabled != null) {
if (adamUserAccountDisabled.equals("true")) {
return false;
} else {
return true;
}
}

return true;
} catch (NamingException e) {
return true;
}
}

public static boolean checkIfAccountNonExpired(@Nonnull Attributes user) {
try {
String accountExpirationDate = getStringAttribute(user, ATTR_ACCOUNT_EXPIRES);
if (accountExpirationDate != null) {
long expirationAsLong = Long.parseLong(accountExpirationDate);
if (expirationAsLong == 0L || expirationAsLong == ACCOUNT_NO_EXPIRATION) {
return true;
}

long nowIn100NsFromJan1601 = getWin32EpochHundredNanos();
boolean expired = expirationAsLong < nowIn100NsFromJan1601;
return !expired;
}

return true;
} catch (NamingException e) {
return true;
}
}

// documentation: https://docs.microsoft.com/en-us/windows/desktop/adschema/a-accountexpires
// code inspired by https://community.oracle.com/thread/1157460
private static long getWin32EpochHundredNanos() {
GregorianCalendar win32Epoch = new GregorianCalendar(1601, Calendar.JANUARY, 1);
Date win32EpochDate = win32Epoch.getTime();
// note that 1/1/1601 will be returned as a negative value by Java
GregorianCalendar today = new GregorianCalendar();
Date todayDate = today.getTime();
long timeSinceWin32EpochInMs = todayDate.getTime() - win32EpochDate.getTime();
// milliseconds to microseconds => x1000
long timeSinceWin32EpochInNs = TimeUnit.NANOSECONDS.convert(timeSinceWin32EpochInMs, TimeUnit.MILLISECONDS);
// but we need in 100 ns, as 1000 ns = 1 micro, add a x10 factor
return timeSinceWin32EpochInNs * 100;
}

public static boolean checkIfCredentialsAreNonExpired(@Nonnull Attributes user) {
try {
String userAccountControl = getStringAttribute(user, ATTR_USER_ACCOUNT_CONTROL);
if (userAccountControl != null) {
int uacAsInt = Integer.parseInt(userAccountControl);
if ((uacAsInt & ADS_UF_PASSWORD_EXPIRED) == ADS_UF_PASSWORD_EXPIRED) {
return false;
}
}

String userAccountControlComputed = getStringAttribute(user, ATTR_USER_ACCOUNT_CONTROL_COMPUTED);
if (userAccountControlComputed != null) {
int uacAsInt = Integer.parseInt(userAccountControlComputed);
if ((uacAsInt & ADS_UF_PASSWORD_EXPIRED) == ADS_UF_PASSWORD_EXPIRED) {
return false;
}
}

String adamUserPasswordExpired = getStringAttribute(user, ATTR_USER_PASSWORD_EXPIRED);
if (adamUserPasswordExpired != null) {
if (adamUserPasswordExpired.equals("true")) {
return false;
} else {
return true;
}
}

return true;
} catch (NamingException e) {
return true;
}
}

public static boolean checkIfAccountNonLocked(@Nonnull Attributes user) {
try {
String userAccountControl = getStringAttribute(user, ATTR_USER_ACCOUNT_CONTROL);
if (userAccountControl != null) {
int uacAsInt = Integer.parseInt(userAccountControl);
if ((uacAsInt & ADS_UF_LOCK_OUT) == ADS_UF_LOCK_OUT) {
return false;
}
}

String userAccountControlComputed = getStringAttribute(user, ATTR_USER_ACCOUNT_CONTROL_COMPUTED);
if (userAccountControlComputed != null) {
int uacAsInt = Integer.parseInt(userAccountControlComputed);
if ((uacAsInt & ADS_UF_LOCK_OUT) == ADS_UF_LOCK_OUT) {
return false;
}
}

return true;
} catch (NamingException e) {
return true;
}
}

private static @CheckForNull String getStringAttribute(@Nonnull Attributes user, @Nonnull String name) throws NamingException {
Attribute a = user.get(name);
if (a == null) {
return null;
}
Object v = a.get();
if (v == null) {
return null;
}
return v.toString();
}
}