diff --git a/cadcAccessControl-Identity/build.xml b/cadcAccessControl-Identity/build.xml new file mode 100644 index 0000000000000000000000000000000000000000..b4e08ad9db4f89f5e3781c9213c0faa4f42020f5 --- /dev/null +++ b/cadcAccessControl-Identity/build.xml @@ -0,0 +1,119 @@ +<!-- +************************************************************************ +******************* CANADIAN ASTRONOMY DATA CENTRE ******************* +************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** +* +* (c) 2009. (c) 2009. +* Government of Canada Gouvernement du Canada +* National Research Council Conseil national de recherches +* Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 +* All rights reserved Tous droits réservés +* +* NRC disclaims any warranties, Le CNRC dénie toute garantie +* expressed, implied, or énoncée, implicite ou légale, +* statutory, of any kind with de quelque nature que ce +* respect to the software, soit, concernant le logiciel, +* including without limitation y compris sans restriction +* any warranty of merchantability toute garantie de valeur +* or fitness for a particular marchande ou de pertinence +* purpose. NRC shall not be pour un usage particulier. +* liable in any event for any Le CNRC ne pourra en aucun cas +* damages, whether direct or être tenu responsable de tout +* indirect, special or general, dommage, direct ou indirect, +* consequential or incidental, particulier ou général, +* arising from the use of the accessoire ou fortuit, résultant +* software. Neither the name de l'utilisation du logiciel. Ni +* of the National Research le nom du Conseil National de +* Council of Canada nor the Recherches du Canada ni les noms +* names of its contributors may de ses participants ne peuvent +* be used to endorse or promote être utilisés pour approuver ou +* products derived from this promouvoir les produits dérivés +* software without specific prior de ce logiciel sans autorisation +* written permission. préalable et particulière +* par écrit. +* +* This file is part of the Ce fichier fait partie du projet +* OpenCADC project. OpenCADC. +* +* OpenCADC is free software: OpenCADC est un logiciel libre ; +* you can redistribute it and/or vous pouvez le redistribuer ou le +* modify it under the terms of modifier suivant les termes de +* the GNU Affero General Public la “GNU Affero General Public +* License as published by the License” telle que publiée +* Free Software Foundation, par la Free Software Foundation +* either version 3 of the : soit la version 3 de cette +* License, or (at your option) licence, soit (à votre gré) +* any later version. toute version ultérieure. +* +* OpenCADC is distributed in the OpenCADC est distribué +* hope that it will be useful, dans l’espoir qu’il vous +* but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE +* without even the implied GARANTIE : sans même la garantie +* warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ +* or FITNESS FOR A PARTICULAR ni d’ADÉQUATION À UN OBJECTIF +* PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence +* General Public License for Générale Publique GNU Affero +* more details. pour plus de détails. +* +* You should have received Vous devriez avoir reçu une +* a copy of the GNU Affero copie de la Licence Générale +* General Public License along Publique GNU Affero avec +* with OpenCADC. If not, see OpenCADC ; si ce n’est +* <http://www.gnu.org/licenses/>. pas le cas, consultez : +* <http://www.gnu.org/licenses/>. +* +* $Revision: 4 $ +* +************************************************************************ +--> + +<!DOCTYPE project> +<project default="build" basedir="."> + <property environment="env"/> + <property file="local.build.properties" /> + + <!-- site-specific build properties or overrides of values in opencadc.properties --> + <property file="${env.CADC_PREFIX}/etc/local.properties" /> + + <!-- site-specific targets, e.g. install, cannot duplicate those in opencadc.targets.xml --> + <import file="${env.CADC_PREFIX}/etc/local.targets.xml" optional="true" /> + + <!-- default properties and targets --> + <property file="${env.CADC_PREFIX}/etc/opencadc.properties" /> + <import file="${env.CADC_PREFIX}/etc/opencadc.targets.xml"/> + + <!-- developer convenience: place for extra targets and properties --> + <import file="extras.xml" optional="true" /> + + <property name="project" value="cadcAccessControl-Identity" /> + + <property name="cadcUtil" value="${lib}/cadcUtil.jar" /> + <property name="cadcRegistry" value="${lib}/cadcRegistry.jar" /> + <property name="cadcLog" value="${lib}/cadcLog.jar" /> + <property name="cadcVOSI" value="${lib}/cadcVOSI.jar" /> + <property name="cadcAccessControl" value="${lib}/cadcAccessControl.jar" /> + + <property name="log4j" value="${ext.lib}/log4j.jar" /> + + <property name="jars" value="${log4j}:${cadcUtil}:${cadcRegistry}:${cadcLog}:${cadcVOSI}:${cadcAccessControl}" /> + + <target name="build" depends="compile"> + <jar jarfile="${build}/lib/${project}.jar" + basedir="${build}/class" + update="no"> + <include name="ca/nrc/cadc/**" /> + </jar> + </target> + + <!-- JAR files needed to run the test suite --> + <property name="xerces" value="${ext.lib}/xerces.jar" /> + <property name="asm" value="${ext.dev}/asm.jar" /> + <property name="cglib" value="${ext.dev}/cglib.jar" /> + <property name="easymock" value="${ext.dev}/easymock.jar" /> + <property name="junit" value="${ext.dev}/junit.jar" /> + <property name="objenesis" value="${ext.dev}/objenesis.jar" /> + <property name="jsonassert" value="${ext.dev}/jsonassert.jar" /> + + <property name="testingJars" value="${build}/class:${jsonassert}:${jars}:${xerces}:${asm}:${cglib}:${easymock}:${junit}:${objenesis}" /> + +</project> diff --git a/cadcAccessControl-Identity/src/ca/nrc/cadc/auth/ACIdentityManager.java b/cadcAccessControl-Identity/src/ca/nrc/cadc/auth/ACIdentityManager.java new file mode 100644 index 0000000000000000000000000000000000000000..a56987d7f159b3224e2ffd02d8aee6dba02de612 --- /dev/null +++ b/cadcAccessControl-Identity/src/ca/nrc/cadc/auth/ACIdentityManager.java @@ -0,0 +1,247 @@ +package ca.nrc.cadc.auth; + +import java.io.File; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.security.Principal; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.sql.Types; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +import javax.security.auth.Subject; +import javax.security.auth.x500.X500Principal; + +import org.apache.log4j.Logger; + +import ca.nrc.cadc.ac.User; +import ca.nrc.cadc.ac.client.UserClient; +import ca.nrc.cadc.profiler.Profiler; +import ca.nrc.cadc.reg.client.LocalAuthority; +import ca.nrc.cadc.reg.client.RegistryClient; +import ca.nrc.cadc.vosi.avail.CheckResource; +import ca.nrc.cadc.vosi.avail.CheckWebService; + +/** + * AC implementation of the IdentityManager interface. This + * implementation returns the NumericPrincipal. + * + * @author pdowler + */ +public class ACIdentityManager implements IdentityManager +{ + private static final Logger log = Logger.getLogger(ACIdentityManager.class); + + private static final File DEFAULT_PRIVILEGED_PEM_FILE = new File(System.getProperty("user.home") + "/.ssl/cadcproxy.pem"); + private static final String ALT_PEM_KEY = ACIdentityManager.class.getName() + ".pemfile"; + + private File privilegedPemFile; + + public ACIdentityManager() + { + privilegedPemFile = DEFAULT_PRIVILEGED_PEM_FILE; + String altPemFile = System.getProperty(ALT_PEM_KEY); + if (altPemFile != null) + { + privilegedPemFile = new File(altPemFile); + } + } + + /** + * Returns a storage type constant from java.sql.Types. + * + * @return Types.INTEGER + */ + public int getOwnerType() + { + return Types.INTEGER; + } + + /** + * Returns a value of type specified by getOwnerType() for storage. + * + * @param subject + * @return an Integer internal CADC ID + */ + public Object toOwner(Subject subject) + { + X500Principal x500Principal = null; + if (subject != null) + { + Set<Principal> principals = subject.getPrincipals(); + for (Principal principal : principals) + { + if (principal instanceof NumericPrincipal) + { + NumericPrincipal cp = (NumericPrincipal) principal; + UUID id = cp.getUUID(); + Long l = Long.valueOf(id.getLeastSignificantBits()); + return l.intValue(); + } + if (principal instanceof X500Principal) + { + x500Principal = (X500Principal) principal; + } + } + } + + if (x500Principal == null) + { + return null; + } + + // The user has connected with a valid client cert but does + // not have an account (no numeric principal). + // Create an auto-approved account with their x500Principal. + NumericPrincipal numericPrincipal = createX500User(x500Principal); + subject.getPrincipals().add(numericPrincipal); + return Long.valueOf(numericPrincipal.getUUID().getLeastSignificantBits()); + } + + private NumericPrincipal createX500User(final X500Principal x500Principal) + { + PrivilegedExceptionAction<NumericPrincipal> action = new PrivilegedExceptionAction<NumericPrincipal>() + { + @Override + public NumericPrincipal run() throws Exception + { + LocalAuthority localAuth = new LocalAuthority(); + URI serviceURI = localAuth.getServiceURI("ums"); + + UserClient userClient = new UserClient(serviceURI); + User newUser = userClient.createUser(x500Principal); + + Set<NumericPrincipal> set = newUser.getIdentities(NumericPrincipal.class); + if (set.isEmpty()) + { + throw new IllegalStateException("missing internal id"); + } + return set.iterator().next(); + } + }; + + Subject servopsSubject = SSLUtil.createSubject(privilegedPemFile); + try + { + return Subject.doAs(servopsSubject, action); + } + catch (Exception e) + { + throw new IllegalStateException("failed to create internal id for user " + x500Principal.getName(), e); + } + } + + /** + * Get a consistent string representation of the user. + * + * @param subject + * @return an X509 distinguished name + */ + public String toOwnerString(Subject subject) + { + if (subject != null) + { + Set<Principal> principals = subject.getPrincipals(); + for (Principal principal : principals) + { + if (principal instanceof X500Principal) + { + return principal.getName(); + } + } + } + return null; + } + + /** + * Reconstruct the subject from the stored object. This method also + * re-populates the subject with all know alternate principals. + * + * @param o the stored object + * @return the complete subject + */ + public Subject toSubject(Object o) + { + try + { + if (o == null || !(o instanceof Integer)) + { + return null; + } + Integer i = (Integer) o; + if (i <= 0) + { + // identities <= 0 are internal + return new Subject(); + } + + UUID uuid = new UUID(0L, (long) i); + NumericPrincipal p = new NumericPrincipal(uuid); + Set<Principal> pset = new HashSet<Principal>(); + pset.add(p); + Subject ret = new Subject(false, pset, new HashSet(), new HashSet()); + + Profiler prof = new Profiler(ACIdentityManager.class); + augmentSubject(ret); + prof.checkpoint("CadcIdentityManager.augmentSubject"); + + return ret; + } + finally + { + + } + } + + public void augmentSubject(final Subject subject) + { + try + { + PrivilegedExceptionAction<Object> action = new PrivilegedExceptionAction<Object>() + { + public Object run() throws Exception + { + LocalAuthority localAuth = new LocalAuthority(); + URI serviceURI = localAuth.getServiceURI("ums"); + + UserClient userClient = new UserClient(serviceURI); + userClient.augmentSubject(subject); + return null; + } + }; + + log.debug("privileged user cert: " + privilegedPemFile.getAbsolutePath()); + Subject servopsSubject = SSLUtil.createSubject(privilegedPemFile); + Subject.doAs(servopsSubject, action); + } + catch (PrivilegedActionException e) + { + String msg = "Error augmenting subject " + subject; + throw new RuntimeException(msg, e); + } + } + + /** + * The returned CheckResource is the same as the one from AuthenticatorImpl. + * + * @return + */ + public static CheckResource getAvailabilityCheck() + { + try + { + RegistryClient regClient = new RegistryClient(); + LocalAuthority localAuth = new LocalAuthority(); + URI serviceURI = localAuth.getServiceURI("gms"); + URL availURL = regClient.getServiceURL(serviceURI, "http", "/availability"); + return new CheckWebService(availURL.toExternalForm()); + } + catch (MalformedURLException e) + { + throw new RuntimeException(e); + } + } +} diff --git a/cadcAccessControl-Identity/src/ca/nrc/cadc/auth/AuthenticatorImpl.java b/cadcAccessControl-Identity/src/ca/nrc/cadc/auth/AuthenticatorImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..af6b5a155831905bd62b05cdb129cf5c67543515 --- /dev/null +++ b/cadcAccessControl-Identity/src/ca/nrc/cadc/auth/AuthenticatorImpl.java @@ -0,0 +1,85 @@ +package ca.nrc.cadc.auth; + +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; + +import javax.security.auth.Subject; +import javax.security.auth.x500.X500Principal; + +import org.apache.log4j.Logger; + +import ca.nrc.cadc.profiler.Profiler; +import ca.nrc.cadc.reg.client.LocalAuthority; +import ca.nrc.cadc.reg.client.RegistryClient; +import ca.nrc.cadc.vosi.avail.CheckResource; +import ca.nrc.cadc.vosi.avail.CheckWebService; + +/** + * Implementation of default Authenticator for AuthenticationUtil in cadcUtil. + * This class augments the subject with additional identities using the access + * control library. + * + * @author pdowler + */ +public class AuthenticatorImpl implements Authenticator +{ + + private static final Logger log = Logger.getLogger(AuthenticatorImpl.class); + + public AuthenticatorImpl() + { + } + + /** + * @param subject + * @return the possibly modified subject + */ + public Subject getSubject(Subject subject) + { + AuthMethod am = AuthenticationUtil.getAuthMethod(subject); + if (am == null || AuthMethod.ANON.equals(am)) + { + return subject; + } + + if (subject != null && subject.getPrincipals().size() > 0) + { + Profiler prof = new Profiler(AuthenticatorImpl.class); + ACIdentityManager identityManager = new ACIdentityManager(); + identityManager.augmentSubject(subject); + prof.checkpoint("AuthenticatorImpl.augmentSubject()"); + + if (subject.getPrincipals(HttpPrincipal.class).isEmpty()) // no matching cadc account + { + // check to see if they connected with an client certificate at least + // they should be able to use services with only a client certificate + if (subject.getPrincipals(X500Principal.class).isEmpty()) + { + // if the caller had an invalid or forged CADC_SSO cookie, we could get + // in here and then not match any known identity: drop to anon + log.debug("HttpPrincipal not found - dropping to anon: " + subject); + subject = AuthenticationUtil.getAnonSubject(); + } + } + } + + return subject; + } + + public static CheckResource getAvailabilityCheck() + { + try + { + RegistryClient regClient = new RegistryClient(); + LocalAuthority localAuth = new LocalAuthority(); + URI serviceURI = localAuth.getServiceURI("gms"); + URL availURL = regClient.getServiceURL(serviceURI, "http", "/availability"); + return new CheckWebService(availURL.toExternalForm()); + } + catch (MalformedURLException e) + { + throw new RuntimeException(e); + } + } +} diff --git a/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/LoginServlet.java b/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/LoginServlet.java index 3a3713804eda584142421c1e20ddda209b7088c4..dd8722e84c616bcde1d934da9e50a1a1174a70e8 100755 --- a/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/LoginServlet.java +++ b/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/LoginServlet.java @@ -69,10 +69,14 @@ package ca.nrc.cadc.ac.server.web; import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; import java.security.AccessControlException; import java.security.Principal; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; +import java.util.Calendar; +import java.util.GregorianCalendar; import javax.security.auth.Subject; import javax.servlet.ServletConfig; @@ -92,8 +96,10 @@ import ca.nrc.cadc.ac.server.PluginFactory; import ca.nrc.cadc.ac.server.UserPersistence; import ca.nrc.cadc.ac.server.ldap.LdapGroupPersistence; import ca.nrc.cadc.auth.AuthenticatorImpl; +import ca.nrc.cadc.auth.DelegationToken; import ca.nrc.cadc.auth.HttpPrincipal; import ca.nrc.cadc.auth.SSOCookieManager; +import ca.nrc.cadc.date.DateUtil; import ca.nrc.cadc.log.ServletLogInfo; import ca.nrc.cadc.net.TransientException; import ca.nrc.cadc.util.StringUtil; @@ -154,6 +160,7 @@ public class LoginServlet extends HttpServlet log.info(logInfo.start()); String userID = request.getParameter("username"); String password = request.getParameter("password"); + String scope = request.getParameter("scope"); if (userID == null || userID.length() == 0) throw new IllegalArgumentException("Missing username"); @@ -176,9 +183,31 @@ public class LoginServlet extends HttpServlet (!StringUtil.hasText(proxyUser) && userPersistence.doLogin(userID, password))) { - String token = - new SSOCookieManager().generate( - new HttpPrincipal(userID, proxyUser)); + String token = null; + HttpPrincipal p = new HttpPrincipal(userID, proxyUser); + if (scope != null) + { + // This cookie will be scope to a certain URI, + // such as a VOSpace node + URI uri = null; + try + { + uri = new URI(scope); + } + catch (URISyntaxException e) + { + throw new IllegalArgumentException("Invalid scope: " + scope); + } + + final Calendar expiryDate = new GregorianCalendar(DateUtil.UTC); + expiryDate.add(Calendar.HOUR, SSOCookieManager.SSO_COOKIE_LIFETIME_HOURS); + DelegationToken dt = new DelegationToken(p, uri, expiryDate.getTime()); + token = DelegationToken.format(dt); + } + else + { + token = new SSOCookieManager().generate(p); + } response.setContentType(CONTENT_TYPE); response.setContentLength(token.length()); response.getWriter().write(token); diff --git a/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/ldap/LdapUserDAOTest.java b/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/ldap/LdapUserDAOTest.java index bf3d3c484057c4ef897ce1e03c79d1e5887106cb..3dcb390f7932721071798cfd9a1b4b0b1f2f88b4 100644 --- a/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/ldap/LdapUserDAOTest.java +++ b/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/ldap/LdapUserDAOTest.java @@ -131,52 +131,6 @@ public class LdapUserDAOTest extends AbstractLdapDAOTest catch (IllegalArgumentException expected) {} } - /** - * Test of addUser method, of class LdapUserDAO. - */ - @Test - public void testAddUserRequest() throws Exception - { - // add user using HttpPrincipal - String username = createUsername(); - final HttpPrincipal userID = new HttpPrincipal(username); - - final User testUser = new User(); - testUser.getIdentities().add(userID); - - testUser.personalDetails = new PersonalDetails("foo", "bar"); - testUser.personalDetails.email = username + "@canada.ca"; - - final UserRequest userRequest = new UserRequest(testUser, "password".toCharArray()); - - DNPrincipal dnPrincipal = new DNPrincipal("uid=" + username + "," + config.getUsersDN()); - Subject subject = new Subject(); - subject.getPrincipals().add(dnPrincipal); - - // do everything as owner - Subject.doAs(subject, new PrivilegedExceptionAction<Object>() - { - public Object run() - throws Exception - { - try - { - final LdapUserDAO userDAO = getUserDAO(); - userDAO.addUserRequest(userRequest); - - final User actual = userDAO.getUserRequest(userID); - check(testUser, actual); - - return null; - } - catch (Exception e) - { - throw new Exception("Problems", e); - } - } - }); - } - @Test public void testAddUser() throws Exception { @@ -220,23 +174,23 @@ public class LdapUserDAOTest extends AbstractLdapDAOTest * Test of addUserRequest method, of class LdapUserDAO. */ @Test - public void testAddPendingUser() throws Exception + public void testAddUserRequest() throws Exception { // add user using HttpPrincipal final String username = createUsername(); final HttpPrincipal userID = new HttpPrincipal(username); - final User httpExpected = new User(); - httpExpected.getIdentities().add(userID); + final User expectedUser = new User(); + expectedUser.getIdentities().add(userID); - PersonalDetails pd = new PersonalDetails("foo", "bar"); - pd.email = username + "@canada.ca"; - httpExpected.personalDetails = pd; + expectedUser.personalDetails = new PersonalDetails("foo", "bar"); + expectedUser.personalDetails.email = username + "@canada.ca"; - UserRequest userRequest = new UserRequest(httpExpected, "123456".toCharArray()); + UserRequest userRequest = new UserRequest(expectedUser, "123456".toCharArray()); - final LdapUserDAO httpUserDAO = getUserDAO(); - httpUserDAO.addUserRequest(userRequest); + // Adding a new user is done anonymously + final LdapUserDAO userDAO = getUserDAO(); + userDAO.addUserRequest(userRequest); DNPrincipal dnPrincipal = new DNPrincipal("uid=" + username + "," + config.getUserRequestsDN()); Subject subject = new Subject(); @@ -250,8 +204,8 @@ public class LdapUserDAOTest extends AbstractLdapDAOTest { try { - final User actual = httpUserDAO.getUserRequest(userID); - check(httpExpected, actual); + final User actualUser = userDAO.getUserRequest(userID); + check(expectedUser, actualUser); return null; } @@ -261,9 +215,48 @@ public class LdapUserDAOTest extends AbstractLdapDAOTest } } }); - } - // TODO testAddUser for an existing user + // try and add another user with the same username + final User dupUsername = new User(); + dupUsername.getIdentities().add(userID); + + dupUsername.personalDetails = new PersonalDetails("foo", "bar"); + dupUsername.personalDetails.email = username + "@foo.com"; + + UserRequest dupUsernameRequest = new UserRequest(dupUsername, "123456".toCharArray()); + + try + { + userDAO.addUserRequest(dupUsernameRequest); + fail("adding a duplicate user should throw a UserAlreadyExistsException"); + } + catch (UserAlreadyExistsException expected) + { + log.debug("expected exception: " + expected.getMessage()); + } + + // try and add another user with the same email address + final String username2 = createUsername(); + final HttpPrincipal userID2 = new HttpPrincipal(username); + + final User dupEmail = new User(); + dupEmail.getIdentities().add(userID2); + + dupEmail.personalDetails = new PersonalDetails("foo", "bar"); + dupEmail.personalDetails.email = username + "@canada.ca"; + + UserRequest dupEmailRequest = new UserRequest(dupEmail, "123456".toCharArray()); + + try + { + userDAO.addUserRequest(dupEmailRequest); + fail("adding a user with an existing email address should throw a UserAlreadyExistsException"); + } + catch (UserAlreadyExistsException expected) + { + log.debug("expected exception: " + expected.getMessage()); + } + } /** * Test of getUser method, of class LdapUserDAO.