/* * Copyright (C) 2005-2012 BetaCONCEPT Limited * * This file is part of Astroboa. * * Astroboa is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Astroboa is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with Astroboa. If not, see <http://www.gnu.org/licenses/>. */ package org.betaconceptframework.astroboa.engine.service.security; import java.io.IOException; import java.security.acl.Group; import java.util.Hashtable; import java.util.List; import javax.naming.Context; import javax.naming.InitialContext; import javax.security.auth.Subject; import javax.security.auth.callback.Callback; import javax.security.auth.callback.CallbackHandler; import javax.security.auth.callback.NameCallback; import javax.security.auth.callback.PasswordCallback; import javax.security.auth.callback.UnsupportedCallbackException; import javax.security.auth.login.AccountNotFoundException; import javax.security.auth.login.FailedLoginException; import javax.security.auth.login.LoginException; import org.apache.commons.lang.StringUtils; import org.betaconceptframework.astroboa.api.model.exception.CmsException; import org.betaconceptframework.astroboa.api.security.AstroboaPrincipalName; import org.betaconceptframework.astroboa.api.security.CmsRole; import org.betaconceptframework.astroboa.api.security.DisplayNamePrincipal; import org.betaconceptframework.astroboa.api.security.IdentityPrincipal; import org.betaconceptframework.astroboa.api.security.PersonUserIdPrincipal; import org.betaconceptframework.astroboa.api.security.management.IdentityStore; import org.betaconceptframework.astroboa.api.security.management.Person; import org.betaconceptframework.astroboa.configuration.RepositoryRegistry; import org.betaconceptframework.astroboa.context.AstroboaClientContextHolder; import org.betaconceptframework.astroboa.engine.jcr.dao.RepositoryDao; import org.betaconceptframework.astroboa.security.AstroboaAuthenticationCallback; import org.betaconceptframework.astroboa.security.CmsGroup; import org.betaconceptframework.astroboa.security.CmsPrincipal; import org.betaconceptframework.astroboa.security.CmsRoleAffiliationFactory; import org.betaconceptframework.astroboa.security.management.CmsPerson; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This class is responsible for the authentication of a user who wants * to connect to an Astroboa repository. * * @author Gregory Chomatas (gchomatas@betaconcept.com) * @author Savvas Triantafyllou (striantafyllou@betaconcept.com) * */ public class AstroboaLogin { private final Logger logger = LoggerFactory.getLogger(getClass()); private IdentityStore identityStore; private boolean useExternalIdentity; private Person loggedInPerson = null; private Subject subject; private CallbackHandler callbackHandler; private IdentityPrincipal identity; private String repositoryId; private RepositoryDao repositoryDao; private String authenticationTokenForSYSTEMofInternalIdentityStore; public AstroboaLogin(CallbackHandler callbackHandler, IdentityStore internalIdentityStore, RepositoryDao repositoryDao) { this.subject = new Subject(); this.callbackHandler = callbackHandler; this.identity = null; this.identityStore = internalIdentityStore; this.loggedInPerson = null; this.useExternalIdentity = false; this.repositoryId = null; this.repositoryDao = repositoryDao; } /** * Override login to provide extra checks in case user credentials are * correct * @return */ public Subject login() throws LoginException { boolean loginIsSuccessful = internalLogin(); if ( loginIsSuccessful == true ){ if (!loggedInPerson.isEnabled()) { throw new AccountNotFoundException(getUsername()); } //Add identity addIdentityPrincipalToSubject(); //Add PersonUserIdPrincipal to subject addPersonUserIdPrincipalToSubject(); //Add display name principal addDisplayNamePrincipalToSubject(); //Add roles to subject addRolesToSubject(); return subject; } else{ throw new LoginException(getUsername()); } } private String getUsername() { if (identity != null){ return identity.getName(); } return null; } private boolean internalLogin() throws LoginException { String[] credentialsInfo = getAuthenticationInformation(); String username = credentialsInfo[0]; String password = credentialsInfo[1]; String secretKey = credentialsInfo[2]; String identityStoreLocation = credentialsInfo[3]; repositoryId = credentialsInfo[4]; /* * Special case user is ANONYMOUS */ if (userIsAnonymous(username)) { identity = new IdentityPrincipal(IdentityPrincipal.ANONYMOUS); loadAnonymousPerson(); return true; } /* * User is not ANONYMOUS */ initializeIdentityStore(identityStoreLocation); //All went well //Authenticate using IdentityStore //If no password is provided then check only if user exists //This must be reviewed as we allow anyone to obtain a user's roles if (password == null) { //User must exist if (! getIdentityStore().userExists(username)) { throw new FailedLoginException(username); } //Also to allow login a secret key must be provided and it must match for this user if (! RepositoryRegistry.INSTANCE.isSecretKeyValidForUser(repositoryId, username, secretKey)) { throw new FailedLoginException(username); } } else if (!getIdentityStore().authenticate(username, password)) { throw new FailedLoginException(username); } loadPersonByUserName(username); //Identity is username as returned by Identity Store //Since username may be case insensitive we do not keep value entered by //user but rather we keep value returned from IdentityStore //which is responsible to provide username entered during user //registration //identity = new IdentityPrincipal(username); if (loggedInPerson == null || StringUtils.isBlank(loggedInPerson.getUsername())) { throw new FailedLoginException("No username returned from IdentityStore during login of user "+username); } identity = new IdentityPrincipal(loggedInPerson.getUsername()); return true; } private void initializeIdentityStore(String identityStoreLocation) throws FailedLoginException { if (StringUtils.isBlank(identityStoreLocation)){ throw new FailedLoginException("No identity store location is provided"); } if (useExternalIdentity){ initializeExternalIdentityStore(identityStoreLocation); } else{ setupContextForInternalIdentityStore(identityStoreLocation); } if (identityStore == null){ throw new FailedLoginException("Could not initialize identity store in location (either a JNDI name or a repository id)"+ identityStoreLocation); } } private void loadAnonymousPerson() { loggedInPerson = new CmsPerson(); loggedInPerson.setDisplayName(IdentityPrincipal.ANONYMOUS); loggedInPerson.setEnabled(true); loggedInPerson.setFamilyName(IdentityPrincipal.ANONYMOUS); loggedInPerson.setFatherName(IdentityPrincipal.ANONYMOUS); loggedInPerson.setFirstName(IdentityPrincipal.ANONYMOUS); loggedInPerson.setUserid(IdentityPrincipal.ANONYMOUS); loggedInPerson.setUsername(IdentityPrincipal.ANONYMOUS); } private boolean userIsAnonymous(String username) { return StringUtils.equals(IdentityPrincipal.ANONYMOUS, username); } private void initializeExternalIdentityStore(String identityStoreLocation) throws FailedLoginException { try { InitialContext context = new InitialContext(); //First check to see if initial context has been initiated at all Hashtable<?, ?> env = context.getEnvironment(); String initialContextFactoryName = env != null ? (String)env.get(Context.INITIAL_CONTEXT_FACTORY) : null; if (StringUtils.isNotBlank(initialContextFactoryName)){ Object serviceReference = context.lookup(identityStoreLocation); if (! (serviceReference instanceof IdentityStore)){ if (! identityStoreLocation.endsWith("/local")){ //JNDIName is provided by the user and the object it references is not an instance of IdentityStore. //It is probably an instance of NamingContext which is on top of a local or remote service //Since JNDIName does not end with "/local" , try to locate the local service under the returned NamingContext identityStore = (IdentityStore) context.lookup(identityStoreLocation+"/local"); } else{ throw new Exception("JNDI Name "+identityStoreLocation+ " refers to an object whose type is not IdentityStore. Unable to locate. External Identity Store "); } } else{ identityStore = (IdentityStore) serviceReference; } //TODO: It may also be the case another login to the identity store must be done } else{ throw new Exception("Initial Context Factory Name is blank therefore no initial context is configured, thus any lookup will result in exception." + "External Identity Store "+identityStoreLocation); } } catch (Exception e) { logger.error("",e); throw new FailedLoginException("During connection to external Identity Store "+ identityStoreLocation); } } private void setupContextForInternalIdentityStore(String identityStoreRepositoryId) { //Since we are using the internal identity store, we must setup the security context //for the user who will be used to connect to the repository which represents the //identity store. This user is the SYSTEM user by default and thus we perform //an internal login without the need of the SYSTEM's password Subject subject = new Subject(); //System identity subject.getPrincipals().add(new IdentityPrincipal(IdentityPrincipal.SYSTEM)); //Grant SYSTEM all roles Group rolesPrincipal = new CmsGroup(AstroboaPrincipalName.Roles.toString()); for (CmsRole cmsRole : CmsRole.values()){ rolesPrincipal.addMember(new CmsPrincipal(CmsRoleAffiliationFactory.INSTANCE.getCmsRoleAffiliationForRepository(cmsRole, identityStoreRepositoryId))); } subject.getPrincipals().add(rolesPrincipal); //Login using the Subject, the provided roles and SYSTEM's permanent key and get the authentication token authenticationTokenForSYSTEMofInternalIdentityStore = repositoryDao.login(identityStoreRepositoryId, subject, RepositoryRegistry.INSTANCE.getPermanentKeyForUser(identityStoreRepositoryId, IdentityPrincipal.SYSTEM)); } private void loadPersonByUserName(String username) throws LoginException { if (loggedInPerson == null){ try{ loggedInPerson = getIdentityStore().retrieveUser(username); } catch(Exception e){ logger.error("Problem when loading person for username "+username, e); throw new LoginException("Problem when loading person for username "+username); } if (loggedInPerson == null){ throw new AccountNotFoundException(username); } } } private void addDisplayNamePrincipalToSubject() { if (StringUtils.isNotBlank(loggedInPerson.getDisplayName())){ subject.getPrincipals().add(new DisplayNamePrincipal(loggedInPerson.getDisplayName())); } } private void addPersonUserIdPrincipalToSubject() { String personUserId = loggedInPerson.getUserid(); if (StringUtils.isNotBlank(personUserId)){ subject.getPrincipals().add(new PersonUserIdPrincipal(personUserId)); } } private void addIdentityPrincipalToSubject() { subject.getPrincipals().add(identity); } private void addRolesToSubject() throws LoginException { //Must return at list one group named "Roles" in order to be final Group groupContainingAllRoles = new CmsGroup(AstroboaPrincipalName.Roles.toString()); if (userIsAnonymous(getUsername())){ //User ANONYMOUS is a virtual user which must have specific role //regardless of whether the identity store is external or internal groupContainingAllRoles.addMember(new CmsPrincipal(CmsRoleAffiliationFactory.INSTANCE.getCmsRoleAffiliationForRepository(CmsRole.ROLE_CMS_EXTERNAL_VIEWER, repositoryId))); } else{ List<String> impliedRoles = getIdentityStore().getImpliedRoles(getUsername()); //Load all roles in a tree if (impliedRoles != null){ for (String impliedRole : impliedRoles){ groupContainingAllRoles.addMember(new CmsPrincipal(impliedRole)); } } } subject.getPrincipals().add(groupContainingAllRoles); } /** * * TAKEN FROM Jboss class * * org.jboss.security.auth.spi.UsernamePasswordLoginModule * * and adjust it to Astroboa requirements * * @return * @throws LoginException */ private String[] getAuthenticationInformation() throws LoginException { String[] info = {null, null, null, null, null}; // prompt for a username and password if( callbackHandler == null ) { throw new LoginException("Error: no CallbackHandler available " + "to collect authentication information"); } NameCallback nc = new NameCallback("User name: ", "guest"); PasswordCallback pc = new PasswordCallback("Password: ", false); AstroboaAuthenticationCallback authenticationCallback = new AstroboaAuthenticationCallback("Astroboa authentication info"); Callback[] callbacks = {nc, pc, authenticationCallback}; String username = null; String password = null; String identityStoreLocation = null; String userSecretKey = null; String repositoryId = null; try{ callbackHandler.handle(callbacks); username = nc.getName(); char[] tmpPassword = pc.getPassword(); if( tmpPassword != null ) { char[] credential = new char[tmpPassword.length]; System.arraycopy(tmpPassword, 0, credential, 0, tmpPassword.length); pc.clearPassword(); password = new String(credential); } identityStoreLocation = authenticationCallback.getIdentityStoreLocation(); useExternalIdentity = authenticationCallback.isExternalIdentityStore(); userSecretKey = authenticationCallback.getSecretKey(); repositoryId = authenticationCallback.getRepositoryId(); } catch(IOException e){ LoginException le = new LoginException("Failed to get username/password"); le.initCause(e); throw le; } catch(UnsupportedCallbackException e){ LoginException le = new LoginException("CallbackHandler does not support: " + e.getCallback()); le.initCause(e); throw le; } info[0] = username; info[1] = password; info[2] = userSecretKey; info[3] = identityStoreLocation; info[4] = repositoryId; return info; } private IdentityStore getIdentityStore(){ if (!useExternalIdentity){ if (StringUtils.isBlank(authenticationTokenForSYSTEMofInternalIdentityStore)){ throw new CmsException("Internal Identity Store must be used but there is no authentication token for the SYSTEM user"); } AstroboaClientContextHolder.activateClientContextForAuthenticationToken(authenticationTokenForSYSTEMofInternalIdentityStore); } return identityStore; } }