001package co.codewizards.cloudstore.rest.server.service; 002 003import static co.codewizards.cloudstore.core.util.Util.*; 004 005import java.io.ByteArrayInputStream; 006import java.io.File; 007import java.io.IOException; 008import java.io.InputStreamReader; 009import java.io.Reader; 010import java.net.MalformedURLException; 011import java.net.URL; 012import java.nio.CharBuffer; 013import java.util.Arrays; 014import java.util.UUID; 015 016import javax.servlet.http.HttpServletRequest; 017import javax.ws.rs.PathParam; 018import javax.ws.rs.WebApplicationException; 019import javax.ws.rs.core.Context; 020import javax.ws.rs.core.MediaType; 021import javax.ws.rs.core.Response; 022import javax.ws.rs.core.Response.Status; 023 024import org.glassfish.jersey.internal.util.Base64; 025import org.slf4j.Logger; 026import org.slf4j.LoggerFactory; 027 028import co.codewizards.cloudstore.core.auth.AuthConstants; 029import co.codewizards.cloudstore.core.dto.Error; 030import co.codewizards.cloudstore.core.repo.local.LocalRepoManager; 031import co.codewizards.cloudstore.core.repo.local.LocalRepoManagerFactory; 032import co.codewizards.cloudstore.core.repo.local.LocalRepoRegistry; 033import co.codewizards.cloudstore.core.repo.transport.RepoTransport; 034import co.codewizards.cloudstore.core.repo.transport.RepoTransportFactory; 035import co.codewizards.cloudstore.core.repo.transport.RepoTransportFactoryRegistry; 036import co.codewizards.cloudstore.core.util.IOUtil; 037import co.codewizards.cloudstore.rest.server.auth.Auth; 038import co.codewizards.cloudstore.rest.server.auth.TransientRepoPasswordManager; 039 040public abstract class AbstractServiceWithRepoToRepoAuth { 041 042 private static final Logger logger = LoggerFactory.getLogger(AbstractServiceWithRepoToRepoAuth.class); 043 044 protected @Context HttpServletRequest request; 045 046 protected @PathParam("repositoryName") String repositoryName; 047 048 private Auth auth; 049 050 /** 051 * Get the authentication information. This method does <b>not</b> verify, if the given authentication information 052 * is correct! It merely checks, if the client sent a 'Basic' authentication header. If it did not, 053 * this method throws a {@link WebApplicationException} with {@link Status#UNAUTHORIZED} or {@link Status#FORBIDDEN}. 054 * If it did, it extracts the information and puts it into an {@link Auth} instance. 055 * @return the {@link Auth} instance extracted from the client's headers. Never <code>null</code>. 056 * @throws WebApplicationException with {@link Status#UNAUTHORIZED}, if the client did not send an 'Authorization' header; 057 * with {@link Status#FORBIDDEN}, if there is an 'Authorization' header, but no 'Basic' authentication header (other authentication modes, like e.g. 'Digest' 058 * are not supported). 059 */ 060 protected Auth getAuth() 061 throws WebApplicationException 062 { 063 if (auth == null) { 064 String authorizationHeader = request.getHeader("Authorization"); 065 if (authorizationHeader == null || authorizationHeader.isEmpty()) { 066 logger.debug("getAuth: There is no 'Authorization' header. Replying with a Status.UNAUTHORIZED response asking for 'Basic' authentication."); 067 068 throw newUnauthorizedException(); 069 } 070 071 logger.debug("getAuth: 'Authorization' header: {}", authorizationHeader); 072 073 if (!authorizationHeader.startsWith("Basic")) 074 throw new WebApplicationException(Response.status(Status.FORBIDDEN) 075 .type(MediaType.APPLICATION_XML) 076 .entity(new Error("Only 'Basic' authentication is supported!")).build()); 077 078 String basicAuthEncoded = authorizationHeader.substring("Basic".length()).trim(); 079 byte[] basicAuthDecodedBA = Base64.decode(basicAuthEncoded.getBytes(IOUtil.CHARSET_UTF_8)); 080 StringBuilder userNameSB = new StringBuilder(); 081 char[] password = null; 082 083 ByteArrayInputStream in = new ByteArrayInputStream(basicAuthDecodedBA); 084 CharBuffer cb = CharBuffer.allocate(basicAuthDecodedBA.length + 1); 085 try { 086 Reader r = new InputStreamReader(in, IOUtil.CHARSET_UTF_8); 087 int charsReadTotal = 0; 088 int charsRead; 089 do { 090 charsRead = r.read(cb); 091 092 if (charsRead > 0) 093 charsReadTotal += charsRead; 094 } while (charsRead >= 0); 095 cb.position(0); 096 097 while (cb.position() < charsReadTotal) { 098 char c = cb.get(); 099 if (c == ':') 100 break; 101 102 userNameSB.append(c); 103 } 104 105 if (cb.position() < charsReadTotal) { 106 password = new char[charsReadTotal - cb.position()]; 107 int idx = 0; 108 while (cb.position() < charsReadTotal) 109 password[idx++] = cb.get(); 110 } 111 } catch (Exception e) { 112 throw new WebApplicationException(Response.status(Status.INTERNAL_SERVER_ERROR).type(MediaType.APPLICATION_XML).entity(new Error(e)).build()); 113 } finally { 114 // For extra safety: Overwrite all sensitive memory with 0. 115 Arrays.fill(basicAuthDecodedBA, (byte)0); 116 117 cb.position(0); 118 for (int i = 0; i < cb.capacity(); ++i) 119 cb.put((char)0); 120 } 121 122 Auth auth = new Auth(); 123 auth.setUserName(userNameSB.toString()); 124 auth.setPassword(password); 125 this.auth = auth; 126 } 127 return auth; 128 } 129 130 /** 131 * Get the {@link Auth} information via {@link #getAuth()} and verify, if they are valid. 132 * @return the {@link Auth} information via {@link #getAuth()}; never <code>null</code>. 133 * @throws WebApplicationException with {@link Status#UNAUTHORIZED}, if the client did not send an 'Authorization' header 134 * or if user-name / password is wrong; 135 * with {@link Status#FORBIDDEN}, if there is an 'Authorization' header, but no 'Basic' authentication header (other authentication modes, like e.g. 'Digest' 136 * are not supported); with {@link Status#INTERNAL_SERVER_ERROR}, if there was an {@link IOException}. 137 */ 138 protected String authenticateAndReturnUserName() 139 throws WebApplicationException 140 { 141 UUID serverRepositoryId = LocalRepoRegistry.getInstance().getRepositoryId(repositoryName); 142 if (serverRepositoryId == null) { 143 throw new WebApplicationException(Response.status(Status.NOT_FOUND) 144 .type(MediaType.APPLICATION_XML) 145 .entity(new Error(String.format("HTTP 404: repositoryName='%s' is neither an alias nor an ID of a known repository!", repositoryName))).build()); 146 } 147 148 Auth auth = getAuth(); 149 try { 150 UUID clientRepositoryId = getClientRepositoryIdFromUserName(auth.getUserName()); 151 if (clientRepositoryId != null) { 152 if (TransientRepoPasswordManager.getInstance().isPasswordValid(serverRepositoryId, clientRepositoryId, auth.getPassword())) 153 return auth.getUserName(); 154 else 155 throw newUnauthorizedException(); 156 } 157 } finally { 158 // We clear auth, even though it is kept in this instance, because we need the password only for 159 // authentication. We authenticate only once and don't need it later, again. Every service invocation 160 // has its own new REST service object instance. Hence, this is clearing should be really no problem. 161 auth.clear(); 162 } 163 throw newUnauthorizedException(); 164 } 165 166 protected UUID getClientRepositoryIdFromUserName(String userName) { 167 if (assertNotNull("userName", userName).startsWith(AuthConstants.USER_NAME_REPOSITORY_ID_PREFIX)) { 168 String repositoryIdString = userName.substring(AuthConstants.USER_NAME_REPOSITORY_ID_PREFIX.length()); 169 UUID clientRepositoryId = UUID.fromString(repositoryIdString); 170 return clientRepositoryId; 171 } 172 return null; 173 } 174 175 protected UUID getClientRepositoryIdFromUserNameOrFail(String userName) { 176 UUID clientRepositoryId = getClientRepositoryIdFromUserName(userName); 177 if (clientRepositoryId == null) 178 throw new IllegalArgumentException(String.format("userName='%s' is not a repository!", userName)); 179 180 return clientRepositoryId; 181 } 182 183 private WebApplicationException newUnauthorizedException() { 184 return new WebApplicationException(Response.status(Status.UNAUTHORIZED).header("WWW-Authenticate", "Basic realm=\"CloudStoreServer\"").build()); 185 } 186 187 protected RepoTransport authenticateAndCreateLocalRepoTransport() { 188 String userName = authenticateAndReturnUserName(); 189 UUID clientRepositoryId = getClientRepositoryIdFromUserNameOrFail(userName); 190 URL localRootURL = getLocalRootURL(clientRepositoryId); 191 RepoTransportFactory repoTransportFactory = RepoTransportFactoryRegistry.getInstance().getRepoTransportFactoryOrFail(localRootURL); 192 RepoTransport repoTransport = repoTransportFactory.createRepoTransport(localRootURL, clientRepositoryId); 193 return repoTransport; 194 } 195 196 protected URL authenticateAndGetLocalRootURL() { 197 String userName = authenticateAndReturnUserName(); 198 UUID clientRepositoryId = getClientRepositoryIdFromUserNameOrFail(userName); 199 return getLocalRootURL(clientRepositoryId); 200 } 201 202 protected URL getLocalRootURL(UUID clientRepositoryId) { 203 assertNotNull("repositoryName", repositoryName); 204 final File localRoot = LocalRepoRegistry.getInstance().getLocalRootForRepositoryNameOrFail(repositoryName); 205 final LocalRepoManager localRepoManager = LocalRepoManagerFactory.Helper.getInstance().createLocalRepoManagerForExistingRepository(localRoot); 206 try { 207 final String localPathPrefix = localRepoManager.getLocalPathPrefixOrFail(clientRepositoryId); 208 URL localRootURL; 209 try { 210 localRootURL = localRoot.toURI().toURL(); 211 } catch (MalformedURLException e) { 212 throw new RuntimeException(e); 213 } 214 if (!localPathPrefix.isEmpty()) { 215 String localRootURLString = localRootURL.toExternalForm(); 216 if (localRootURLString.endsWith("/")) 217 localRootURLString = localRootURLString.substring(0, localRootURLString.length() - 1); 218 219 // localPathPrefix is guaranteed to start with a '/'. 220 localRootURLString += localPathPrefix; 221 try { 222 localRootURL = new URL(localRootURLString); 223 } catch (MalformedURLException e) { 224 throw new RuntimeException(e); 225 } 226 } 227 return localRootURL; 228 } finally { 229 localRepoManager.close(); 230 } 231 } 232}