package co.codewizards.cloudstore.rest.server.service; import static co.codewizards.cloudstore.core.util.AssertUtil.*; import co.codewizards.cloudstore.core.io.ByteArrayInputStream; import java.io.CharArrayReader; import java.io.CharArrayWriter; import java.io.IOException; import java.io.InputStreamReader; import java.io.Reader; import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URL; import java.util.Arrays; import java.util.UUID; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.PathParam; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; import org.glassfish.jersey.internal.util.Base64; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import co.codewizards.cloudstore.core.auth.AuthConstants; import co.codewizards.cloudstore.core.dto.Error; import co.codewizards.cloudstore.core.oio.File; import co.codewizards.cloudstore.core.repo.local.LocalRepoManager; import co.codewizards.cloudstore.core.repo.local.LocalRepoManagerFactory; import co.codewizards.cloudstore.core.repo.local.LocalRepoRegistryImpl; import co.codewizards.cloudstore.core.repo.transport.RepoTransport; import co.codewizards.cloudstore.core.repo.transport.RepoTransportFactory; import co.codewizards.cloudstore.core.repo.transport.RepoTransportFactoryRegistry; import co.codewizards.cloudstore.core.util.IOUtil; import co.codewizards.cloudstore.core.util.UrlUtil; import co.codewizards.cloudstore.rest.server.auth.Auth; import co.codewizards.cloudstore.rest.server.auth.NotAuthorizedException; import co.codewizards.cloudstore.rest.server.auth.TransientRepoPasswordManager; import co.codewizards.cloudstore.rest.server.ldap.LdapClientProvider; public abstract class AbstractServiceWithRepoToRepoAuth { private static final Logger logger = LoggerFactory.getLogger(AbstractServiceWithRepoToRepoAuth.class); protected @Context HttpServletRequest request; protected @PathParam("repositoryName") String repositoryName; private Auth auth; /** * Get the authentication information. This method does <b>not</b> verify, if the given authentication information * is correct! It merely checks, if the client sent a 'Basic' authentication header. If it did not, * this method throws a {@link WebApplicationException} with {@link Status#UNAUTHORIZED} or {@link Status#FORBIDDEN}. * If it did, it extracts the information and puts it into an {@link Auth} instance. * @return the {@link Auth} instance extracted from the client's headers. Never <code>null</code>. * @throws WebApplicationException with {@link Status#UNAUTHORIZED}, if the client did not send an 'Authorization' header; * with {@link Status#FORBIDDEN}, if there is an 'Authorization' header, but no 'Basic' authentication header (other authentication modes, like e.g. 'Digest' * are not supported). */ protected Auth getAuth() throws WebApplicationException { if (auth == null) { final String authorizationHeader = request.getHeader("Authorization"); if (authorizationHeader == null || authorizationHeader.isEmpty()) { logger.debug("getAuth: There is no 'Authorization' header. Replying with a Status.UNAUTHORIZED response asking for 'Basic' authentication."); throw newUnauthorizedException(); } logger.debug("getAuth: 'Authorization' header: {}", authorizationHeader); if (!authorizationHeader.startsWith("Basic")) throw new WebApplicationException(Response.status(Status.FORBIDDEN) .type(MediaType.APPLICATION_XML) .entity(new Error("Only 'Basic' authentication is supported!")).build()); final String basicAuthEncoded = authorizationHeader.substring("Basic".length()).trim(); final byte[] basicAuthDecodedBA = getBasicAuthEncodedBA(basicAuthEncoded); final StringBuilder userNameSB = new StringBuilder(); char[] password = null; final ByteArrayInputStream in = new ByteArrayInputStream(basicAuthDecodedBA); CharArrayWriter caw = new CharArrayWriter(basicAuthDecodedBA.length + 1); CharArrayReader car = null; try { final Reader r = new InputStreamReader(in, IOUtil.CHARSET_NAME_UTF_8); int charsReadTotal = 0; int charsRead; do { final char[] c = new char[10]; charsRead = r.read(c); caw.write(c); if (charsRead > 0) charsReadTotal += charsRead; } while (charsRead >= 0); charsRead = 0; car = new CharArrayReader(caw.toCharArray()); int charsReadTotalCheck = 0; while (charsRead >= 0 && charsRead < charsReadTotal) { final char[] cbuf = new char[1]; charsRead = car.read(cbuf); if (charsRead > 0) charsReadTotalCheck += charsRead; if (cbuf[0] == ':') break; userNameSB.append(cbuf[0]); } if (charsRead >= 0 && charsRead < charsReadTotal) { password = new char[charsReadTotal - charsReadTotalCheck]; final int passwordSize = car.read(password); if (passwordSize + charsReadTotalCheck != charsReadTotal) throw new IllegalStateException("passwordSize and charsRead must match charsReadTotal!" + " passwordSize=" + passwordSize + ", charsRead=" + charsRead + ", charsReadTotal=" + charsReadTotal);//TODO for testing } } catch (final Exception e) { throw new WebApplicationException(Response.status(Status.INTERNAL_SERVER_ERROR).type(MediaType.APPLICATION_XML).entity(new Error(e)).build()); } finally { // For extra safety: Overwrite all sensitive memory with 0. Arrays.fill(basicAuthDecodedBA, (byte)0); final char[] zeroArray = new char[] {0}; // overwrite caw & car: if (caw != null) { final int oldCawSize = caw.size(); caw.reset(); try { if (car != null) { car.reset(); } for (int i = 0; i < oldCawSize; ++i) caw.write(zeroArray); car = new CharArrayReader(caw.toCharArray()); car.close(); caw.reset(); caw = null; } catch (final IOException e) { throw new RuntimeException(e); } } } final Auth auth = new Auth(); auth.setUserName(userNameSB.toString()); auth.setPassword(password); this.auth = auth; } return auth; } private byte[] getBasicAuthEncodedBA(final String basicAuthEncoded) { byte[] basicAuthDecodedBA; try { basicAuthDecodedBA = Base64.decode(basicAuthEncoded.getBytes(IOUtil.CHARSET_NAME_UTF_8)); } catch (final UnsupportedEncodingException e1) { throw new RuntimeException(e1); } return basicAuthDecodedBA; } /** * Get the {@link Auth} information via {@link #getAuth()} and verify, if they are valid. * @return the {@link Auth} information via {@link #getAuth()}; never <code>null</code>. * @throws WebApplicationException with {@link Status#UNAUTHORIZED}, if the client did not send an 'Authorization' header * or if user-name / password is wrong; * with {@link Status#FORBIDDEN}, if there is an 'Authorization' header, but no 'Basic' authentication header (other authentication modes, like e.g. 'Digest' * are not supported); with {@link Status#INTERNAL_SERVER_ERROR}, if there was an {@link IOException}. */ protected String authenticateAndReturnUserName() throws WebApplicationException { final UUID serverRepositoryId = LocalRepoRegistryImpl.getInstance().getRepositoryId(repositoryName); if (serverRepositoryId == null) { throw new WebApplicationException(Response.status(Status.NOT_FOUND) .type(MediaType.APPLICATION_XML) .entity(new Error(String.format("HTTP 404: repositoryName='%s' is neither an alias nor an ID of a known repository!", repositoryName))).build()); } // We don't clear this auth anymore, because we might need to invoke this authenticateAndReturnUserName() in service-sub-classes // again, before delegating to the super-service-method. final Auth auth = getAuth(); final UUID clientRepositoryId = getClientRepositoryIdFromUserName(auth.getUserName()); if (clientRepositoryId != null) { if (TransientRepoPasswordManager.getInstance().isPasswordValid(serverRepositoryId, clientRepositoryId, auth.getPassword())) return auth.getUserName(); else throw newUnauthorizedException(); } else{ return LdapClientProvider.getInstance().getClient().authenticate(auth); } } protected UUID getClientRepositoryIdFromUserName(final String userName) { if (assertNotNull(userName, "userName").startsWith(AuthConstants.USER_NAME_REPOSITORY_ID_PREFIX)) { final String repositoryIdString = userName.substring(AuthConstants.USER_NAME_REPOSITORY_ID_PREFIX.length()); final UUID clientRepositoryId = UUID.fromString(repositoryIdString); return clientRepositoryId; } return null; } protected UUID getClientRepositoryIdFromUserNameOrFail(final String userName) { final UUID clientRepositoryId = getClientRepositoryIdFromUserName(userName); if (clientRepositoryId == null) throw new IllegalArgumentException(String.format("userName='%s' is not a repository!", userName)); return clientRepositoryId; } private WebApplicationException newUnauthorizedException() { // TODO maybe better throw a new javax.ws.rs.NotAuthorizedException? return new NotAuthorizedException(); } protected RepoTransport authenticateAndCreateLocalRepoTransport() { final String userName = authenticateAndReturnUserName(); final UUID clientRepositoryId = getClientRepositoryIdFromUserNameOrFail(userName); final URL localRootURL = getLocalRootURL(clientRepositoryId); final RepoTransportFactory repoTransportFactory = RepoTransportFactoryRegistry.getInstance().getRepoTransportFactoryOrFail(localRootURL); final RepoTransport repoTransport = repoTransportFactory.createRepoTransport(localRootURL, clientRepositoryId); return repoTransport; } protected RepoTransport authenticateWithLdap(){ authenticateAndReturnUserName(); final File localRoot = LocalRepoRegistryImpl.getInstance().getLocalRootForRepositoryNameOrFail(repositoryName); URL localRootURL; try { localRootURL = localRoot.toURI().toURL(); localRootURL = appendEmptyPathPrefix(localRootURL); } catch (MalformedURLException e) { throw new RuntimeException(e); } final RepoTransportFactory repoTransportFactory = RepoTransportFactoryRegistry.getInstance().getRepoTransportFactoryOrFail(localRootURL); return repoTransportFactory.createRepoTransport(localRootURL, null); } protected URL authenticateAndGetLocalRootURL() { final String userName = authenticateAndReturnUserName(); final UUID clientRepositoryId = getClientRepositoryIdFromUserNameOrFail(userName); return getLocalRootURL(clientRepositoryId); } protected URL getLocalRootURL(final UUID clientRepositoryId) { assertNotNull(repositoryName, "repositoryName"); final File localRoot = LocalRepoRegistryImpl.getInstance().getLocalRootForRepositoryNameOrFail(repositoryName); final LocalRepoManager localRepoManager = LocalRepoManagerFactory.Helper.getInstance().createLocalRepoManagerForExistingRepository(localRoot); try { final String localPathPrefix = localRepoManager.getLocalPathPrefixOrFail(clientRepositoryId); URL localRootURL; try { localRootURL = localRoot.toURI().toURL(); } catch (final MalformedURLException e) { throw new RuntimeException(e); } localRootURL = UrlUtil.appendNonEncodedPath(localRootURL, localPathPrefix); return localRootURL; } finally { localRepoManager.close(); } } private URL appendEmptyPathPrefix(URL localRoot){ return UrlUtil. appendNonEncodedPath(localRoot, ""); } }