X-Git-Url: https://git.argeo.org/?a=blobdiff_plain;f=org.argeo.cms%2Fsrc%2Forg%2Fargeo%2Fcms%2Fauth%2FUserAdminLoginModule.java;h=aa41e108301abd0ce47abe25665586a880b5d0d6;hb=e4a5502f49e2a2c35d16bbc96efdffead1362a76;hp=ea2a6dedf6ea2877c3dcf487999420118aa9aa31;hpb=cba35caa1a083661c8e113f5295b2dbfc90a16cd;p=lgpl%2Fargeo-commons.git diff --git a/org.argeo.cms/src/org/argeo/cms/auth/UserAdminLoginModule.java b/org.argeo.cms/src/org/argeo/cms/auth/UserAdminLoginModule.java index ea2a6dedf..aa41e1083 100644 --- a/org.argeo.cms/src/org/argeo/cms/auth/UserAdminLoginModule.java +++ b/org.argeo.cms/src/org/argeo/cms/auth/UserAdminLoginModule.java @@ -1,11 +1,17 @@ package org.argeo.cms.auth; +import static org.argeo.api.acr.ldap.LdapAttr.cn; + import java.io.IOException; -import java.util.Iterator; +import java.security.PrivilegedAction; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; +import javax.naming.ldap.LdapName; import javax.security.auth.Subject; import javax.security.auth.callback.Callback; import javax.security.auth.callback.CallbackHandler; @@ -13,157 +19,336 @@ import javax.security.auth.callback.LanguageCallback; import javax.security.auth.callback.NameCallback; import javax.security.auth.callback.PasswordCallback; import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.auth.kerberos.KerberosPrincipal; import javax.security.auth.login.CredentialNotFoundException; -import javax.security.auth.login.FailedLoginException; import javax.security.auth.login.LoginException; import javax.security.auth.spi.LoginModule; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpSession; - -import org.argeo.ArgeoException; -import org.argeo.cms.internal.kernel.Activator; -import org.argeo.eclipse.ui.specific.UiContext; -import org.osgi.framework.BundleContext; -import org.osgi.service.http.HttpContext; + +import org.argeo.api.acr.ldap.LdapAttr; +import org.argeo.api.cms.CmsConstants; +import org.argeo.api.cms.CmsLog; +import org.argeo.cms.directory.ldap.IpaUtils; +import org.argeo.cms.internal.runtime.CmsContextImpl; +import org.argeo.cms.osgi.useradmin.AuthenticatingUser; +import org.argeo.cms.osgi.useradmin.TokenUtils; import org.osgi.service.useradmin.Authorization; +import org.osgi.service.useradmin.Group; import org.osgi.service.useradmin.User; import org.osgi.service.useradmin.UserAdmin; -public class UserAdminLoginModule implements LoginModule, AuthConstants { +/** + * Use the {@link UserAdmin} in the OSGi registry as the basis for + * authentication. + */ +public class UserAdminLoginModule implements LoginModule { + private final static CmsLog log = CmsLog.getLog(UserAdminLoginModule.class); + private Subject subject; private CallbackHandler callbackHandler; - private boolean isAnonymous = false; + private Map sharedState = null; + + private List indexedUserProperties = Arrays.asList(new String[] { LdapAttr.mail.name(), + LdapAttr.uid.name(), LdapAttr.employeeNumber.name(), LdapAttr.authPassword.name() }); + + // private state +// private BundleContext bc; + private User authenticatedUser = null; + private Locale locale; + + private Authorization bindAuthorization = null; - private HttpServletRequest request = null; +// private boolean singleUser = Activator.isSingleUser(); + @SuppressWarnings("unchecked") @Override - public void initialize(Subject subject, CallbackHandler callbackHandler, - Map sharedState, Map options) { + public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, + Map options) { + this.subject = subject; try { - this.subject = subject; +// bc = FrameworkUtil.getBundle(UserAdminLoginModule.class).getBundleContext(); this.callbackHandler = callbackHandler; - if (options.containsKey("anonymous")) - isAnonymous = Boolean.parseBoolean(options.get("anonymous") - .toString()); + this.sharedState = (Map) sharedState; } catch (Exception e) { - throw new ArgeoException("Cannot initialize login module", e); + throw new IllegalStateException("Cannot initialize login module", e); } } @Override public boolean login() throws LoginException { - BundleContext bc = Activator.getBundleContext(); - UserAdmin userAdmin = bc.getService(bc - .getServiceReference(UserAdmin.class)); - Authorization authorization = null; - if (isAnonymous) { - authorization = userAdmin.getAuthorization(null); + UserAdmin userAdmin = CmsContextImpl.getCmsContext().getUserAdmin(); + final String username; + final char[] password; + Object certificateChain = null; + boolean preauth = false; + if (sharedState.containsKey(CmsAuthUtils.SHARED_STATE_NAME) + && sharedState.containsKey(CmsAuthUtils.SHARED_STATE_PWD)) { + // NB: required by Basic http auth + username = (String) sharedState.get(CmsAuthUtils.SHARED_STATE_NAME); + password = (char[]) sharedState.get(CmsAuthUtils.SHARED_STATE_PWD); + // // TODO locale? + } else if (sharedState.containsKey(CmsAuthUtils.SHARED_STATE_NAME) + && sharedState.containsKey(CmsAuthUtils.SHARED_STATE_SPNEGO_TOKEN)) { + // SPNEGO login has succeeded, that's enough for us at this stage + return true; + } else if (sharedState.containsKey(CmsAuthUtils.SHARED_STATE_NAME) + && sharedState.containsKey(CmsAuthUtils.SHARED_STATE_CERTIFICATE_CHAIN)) { + String certDn = (String) sharedState.get(CmsAuthUtils.SHARED_STATE_NAME); + username = certDn; + certificateChain = sharedState.get(CmsAuthUtils.SHARED_STATE_CERTIFICATE_CHAIN); + password = null; + } else if (sharedState.containsKey(CmsAuthUtils.SHARED_STATE_NAME) + && sharedState.containsKey(CmsAuthUtils.SHARED_STATE_REMOTE_ADDR) + && sharedState.containsKey(CmsAuthUtils.SHARED_STATE_REMOTE_PORT)) {// ident + username = (String) sharedState.get(CmsAuthUtils.SHARED_STATE_NAME); + password = null; + preauth = true; } else { - HttpRequestCallback httpCallback = new HttpRequestCallback(); + // ask for username and password NameCallback nameCallback = new NameCallback("User"); - PasswordCallback passwordCallback = new PasswordCallback( - "Password", false); + PasswordCallback passwordCallback = new PasswordCallback("Password", false); LanguageCallback langCallback = new LanguageCallback(); try { - callbackHandler.handle(new Callback[] { httpCallback, - nameCallback, passwordCallback, langCallback }); + callbackHandler.handle(new Callback[] { nameCallback, passwordCallback, langCallback }); } catch (IOException e) { - throw new LoginException("Cannot handle http callback: " - + e.getMessage()); - } catch (ThreadDeath e) { - throw new ThreadDeathLoginException( - "Callbackhandler thread died", e); + throw new LoginException("Cannot handle callback: " + e.getMessage()); } catch (UnsupportedCallbackException e) { return false; } - request = httpCallback.getRequest(); - if (request != null) { - authorization = (Authorization) request - .getAttribute(HttpContext.AUTHORIZATION); - if (authorization == null) - authorization = (Authorization) request.getSession() - .getAttribute(HttpContext.AUTHORIZATION); - } // i18n - Locale locale = langCallback.getLocale(); + locale = langCallback.getLocale(); if (locale == null) locale = Locale.getDefault(); - UiContext.setLocale(locale); - - if (authorization == null) { - // create credentials - final String username = nameCallback.getName(); - if (username == null || username.trim().equals("")) { - // authorization = userAdmin.getAuthorization(null); - throw new CredentialNotFoundException( - "No credentials provided"); - } else { - char[] password = {}; - if (passwordCallback.getPassword() != null) - password = passwordCallback.getPassword(); - else - throw new CredentialNotFoundException( - "No credentials provided"); - - User user = userAdmin.getUser(null, username); - if (user == null) - throw new FailedLoginException("Invalid credentials"); - if (!user.hasCredential(null, password)) - throw new FailedLoginException("Invalid credentials"); - // return false; - authorization = userAdmin.getAuthorization(user); + // FIXME add it to Subject + // Locale.setDefault(locale); + + username = nameCallback.getName(); + if (username == null || username.trim().equals("")) { + // authorization = userAdmin.getAuthorization(null); + throw new CredentialNotFoundException("No credentials provided"); + } + if (passwordCallback.getPassword() != null) + password = passwordCallback.getPassword(); + else + throw new CredentialNotFoundException("No credentials provided"); + sharedState.put(CmsAuthUtils.SHARED_STATE_NAME, username); + sharedState.put(CmsAuthUtils.SHARED_STATE_PWD, password); + } + User user = searchForUser(userAdmin, username); + + // Tokens + if (user == null) { + String token = username; + Group tokenGroup = searchForToken(userAdmin, token); + if (tokenGroup != null) { + Authorization tokenAuthorization = getAuthorizationFromToken(userAdmin, tokenGroup); + if (tokenAuthorization != null) { + bindAuthorization = tokenAuthorization; + authenticatedUser = (User) userAdmin.getRole(bindAuthorization.getName()); + return true; + } + } + } + + if (user == null) + return true;// expect Kerberos + + if (password != null) { + // TODO disabling bind for the time being, + // as it requires authorisations to be set at LDAP level + boolean tryBind = false; + // try bind first + if (tryBind) + try { + AuthenticatingUser authenticatingUser = new AuthenticatingUser(user.getName(), password); + bindAuthorization = userAdmin.getAuthorization(authenticatingUser); + // TODO check tokens as well + if (bindAuthorization != null) { + authenticatedUser = user; + return true; + } + } catch (Exception e) { + // silent + if (log.isTraceEnabled()) + log.trace("Bind failed", e); } + + // works only if a connection password is provided + if (!user.hasCredential(null, password)) { + return false; } - // } else { - // authorization = userAdmin.getAuthorization(null); - // } + } else if (certificateChain != null) { + // TODO check CRLs/OSCP validity? + // NB: authorization in commit() will work only if an LDAP connection password + // is provided +// } else if (singleUser) { +// // TODO verify IP address? + } else if (preauth) { + // ident + } else { + throw new CredentialNotFoundException("No credentials provided"); } - subject.getPrivateCredentials().add(authorization); + + authenticatedUser = user; return true; } @Override public boolean commit() throws LoginException { - Authorization authorization = subject - .getPrivateCredentials(Authorization.class).iterator().next(); - if (request != null && authorization.getName() != null) { - request.setAttribute(HttpContext.REMOTE_USER, - authorization.getName()); - request.setAttribute(HttpContext.AUTHORIZATION, authorization); - request.getSession().setAttribute(HttpContext.AUTHORIZATION, - authorization); - subject.getPrivateCredentials().add(request.getSession()); + if (locale != null) + subject.getPublicCredentials().add(locale); + +// if (singleUser) { +// OsUserUtils.loginAsSystemUser(subject); +// } + UserAdmin userAdmin = CmsContextImpl.getCmsContext().getUserAdmin(); + Authorization authorization; + if (callbackHandler == null) {// anonymous + authorization = userAdmin.getAuthorization(null); + } else if (bindAuthorization != null) {// bind + authorization = bindAuthorization; + } else {// Kerberos + User authenticatingUser; + Set kerberosPrincipals = subject.getPrincipals(KerberosPrincipal.class); + if (kerberosPrincipals.isEmpty()) { + if (authenticatedUser == null) { + if (log.isTraceEnabled()) + log.trace("Neither kerberos nor user admin login succeeded. Login failed."); + throw new CredentialNotFoundException("Bad credentials."); + } else { + authenticatingUser = authenticatedUser; + } + } else { + KerberosPrincipal kerberosPrincipal = kerberosPrincipals.iterator().next(); + LdapName dn = IpaUtils.kerberosToDn(kerberosPrincipal.getName()); + authenticatingUser = new AuthenticatingUser(dn); + if (authenticatedUser != null && !authenticatingUser.getName().equals(authenticatedUser.getName())) + throw new LoginException("Kerberos login " + authenticatingUser.getName() + + " is inconsistent with user admin login " + authenticatedUser.getName()); + } + if (log.isTraceEnabled()) + log.trace("Retrieve authorization for " + authenticatingUser + "... "); + authorization = Subject.doAs(subject, new PrivilegedAction() { + + @Override + public Authorization run() { + Authorization authorization = userAdmin.getAuthorization(authenticatingUser); + return authorization; + } + + }); + if (authorization == null) + throw new LoginException( + "User admin found no authorization for authenticated user " + authenticatingUser.getName()); } + + // Log and monitor new login + RemoteAuthRequest request = (RemoteAuthRequest) sharedState.get(CmsAuthUtils.SHARED_STATE_HTTP_REQUEST); + CmsAuthUtils.addAuthorization(subject, authorization); + + // Unlock keyring (underlying login to the JCR repository) +// char[] password = (char[]) sharedState.get(CmsAuthUtils.SHARED_STATE_PWD); +// if (password != null) { +// ServiceReference keyringSr = bc.getServiceReference(CryptoKeyring.class); +// if (keyringSr != null) { +// CryptoKeyring keyring = bc.getService(keyringSr); +// Subject.doAs(subject, new PrivilegedAction() { +// +// @Override +// public Void run() { +// try { +// keyring.unlock(password); +// } catch (Exception e) { +// e.printStackTrace(); +// log.warn("Could not unlock keyring with the password provided by " + authorization.getName() +// + ": " + e.getMessage()); +// } +// return null; +// } +// +// }); +// } +// } + + // Register CmsSession with initial subject + CmsAuthUtils.registerSessionAuthorization(request, subject, authorization, locale); + + if (log.isDebugEnabled()) + log.debug("Logged in to CMS: " + subject); return true; } @Override public boolean abort() throws LoginException { - cleanUp(); return true; } @Override public boolean logout() throws LoginException { - Set httpSession = subject - .getPrivateCredentials(HttpSession.class); - Iterator it = httpSession.iterator(); - while (it.hasNext()) { - HttpSession sess = it.next(); - sess.setAttribute(HttpContext.AUTHORIZATION, null); - // sess.setMaxInactiveInterval(1);// invalidate session - } - subject.getPrivateCredentials().removeAll(httpSession); - cleanUp(); + if (log.isTraceEnabled()) + log.trace("Logging out from CMS... " + subject); + CmsAuthUtils.cleanUp(subject); return true; } - private void cleanUp() { - subject.getPrivateCredentials().removeAll( - subject.getPrivateCredentials(Authorization.class)); - subject = null; + protected User searchForUser(UserAdmin userAdmin, String providedUsername) { + try { + // TODO check value null or empty + Set collectedUsers = new HashSet<>(); + // try dn + User user = null; + user = null; + // try all indexes + for (String attr : indexedUserProperties) { + user = userAdmin.getUser(attr, providedUsername); + if (user != null) + collectedUsers.add(user); + } + if (collectedUsers.size() == 1) { + user = collectedUsers.iterator().next(); + return user; + } else if (collectedUsers.size() > 1) { + log.warn(collectedUsers.size() + " users for provided username" + providedUsername); + } + // try DN as a last resort + try { + user = (User) userAdmin.getRole(providedUsername); + if (user != null) + return user; + } catch (Exception e) { + // silent + } + return null; + } catch (Exception e) { + if (log.isTraceEnabled()) + log.warn("Cannot search for user " + providedUsername, e); + return null; + } + } + protected Group searchForToken(UserAdmin userAdmin, String token) { + String dn = cn + "=" + token + "," + CmsConstants.TOKENS_BASEDN; + Group tokenGroup = (Group) userAdmin.getRole(dn); + return tokenGroup; + } + + protected Authorization getAuthorizationFromToken(UserAdmin userAdmin, Group tokenGroup) { + if (TokenUtils.isExpired(tokenGroup)) + return null; +// String expiryDateStr = (String) tokenGroup.getProperties().get(description.name()); +// if (expiryDateStr != null) { +// Instant expiryDate = NamingUtils.ldapDateToInstant(expiryDateStr); +// if (expiryDate.isBefore(Instant.now())) { +// if (log.isDebugEnabled()) +// log.debug("Token " + tokenGroup.getName() + " has expired."); +// return null; +// } +// } + String userDn = TokenUtils.userDn(tokenGroup); + User user = (User) userAdmin.getRole(userDn); + Authorization auth = userAdmin.getAuthorization(user); + return auth; + } }