001package co.codewizards.cloudstore.rest.server.service; 002 003import static co.codewizards.cloudstore.core.util.AssertUtil.*; 004 005import co.codewizards.cloudstore.core.io.ByteArrayInputStream; 006import java.io.CharArrayReader; 007import java.io.CharArrayWriter; 008import java.io.IOException; 009import java.io.InputStreamReader; 010import java.io.Reader; 011import java.io.UnsupportedEncodingException; 012import java.net.MalformedURLException; 013import java.net.URL; 014import java.util.Arrays; 015import java.util.UUID; 016 017import javax.servlet.http.HttpServletRequest; 018import javax.ws.rs.PathParam; 019import javax.ws.rs.WebApplicationException; 020import javax.ws.rs.core.Context; 021import javax.ws.rs.core.MediaType; 022import javax.ws.rs.core.Response; 023import javax.ws.rs.core.Response.Status; 024 025import org.glassfish.jersey.internal.util.Base64; 026import org.slf4j.Logger; 027import org.slf4j.LoggerFactory; 028 029import co.codewizards.cloudstore.core.auth.AuthConstants; 030import co.codewizards.cloudstore.core.dto.Error; 031import co.codewizards.cloudstore.core.oio.File; 032import co.codewizards.cloudstore.core.repo.local.LocalRepoManager; 033import co.codewizards.cloudstore.core.repo.local.LocalRepoManagerFactory; 034import co.codewizards.cloudstore.core.repo.local.LocalRepoRegistryImpl; 035import co.codewizards.cloudstore.core.repo.transport.RepoTransport; 036import co.codewizards.cloudstore.core.repo.transport.RepoTransportFactory; 037import co.codewizards.cloudstore.core.repo.transport.RepoTransportFactoryRegistry; 038import co.codewizards.cloudstore.core.util.IOUtil; 039import co.codewizards.cloudstore.core.util.UrlUtil; 040import co.codewizards.cloudstore.rest.server.auth.Auth; 041import co.codewizards.cloudstore.rest.server.auth.NotAuthorizedException; 042import co.codewizards.cloudstore.rest.server.auth.TransientRepoPasswordManager; 043import co.codewizards.cloudstore.rest.server.ldap.LdapClientProvider; 044 045public abstract class AbstractServiceWithRepoToRepoAuth { 046 047 private static final Logger logger = LoggerFactory.getLogger(AbstractServiceWithRepoToRepoAuth.class); 048 049 protected @Context HttpServletRequest request; 050 051 protected @PathParam("repositoryName") String repositoryName; 052 053 private Auth auth; 054 055 /** 056 * Get the authentication information. This method does <b>not</b> verify, if the given authentication information 057 * is correct! It merely checks, if the client sent a 'Basic' authentication header. If it did not, 058 * this method throws a {@link WebApplicationException} with {@link Status#UNAUTHORIZED} or {@link Status#FORBIDDEN}. 059 * If it did, it extracts the information and puts it into an {@link Auth} instance. 060 * @return the {@link Auth} instance extracted from the client's headers. Never <code>null</code>. 061 * @throws WebApplicationException with {@link Status#UNAUTHORIZED}, if the client did not send an 'Authorization' header; 062 * with {@link Status#FORBIDDEN}, if there is an 'Authorization' header, but no 'Basic' authentication header (other authentication modes, like e.g. 'Digest' 063 * are not supported). 064 */ 065 protected Auth getAuth() 066 throws WebApplicationException 067 { 068 if (auth == null) { 069 final String authorizationHeader = request.getHeader("Authorization"); 070 if (authorizationHeader == null || authorizationHeader.isEmpty()) { 071 logger.debug("getAuth: There is no 'Authorization' header. Replying with a Status.UNAUTHORIZED response asking for 'Basic' authentication."); 072 throw newUnauthorizedException(); 073 } 074 075 logger.debug("getAuth: 'Authorization' header: {}", authorizationHeader); 076 077 if (!authorizationHeader.startsWith("Basic")) 078 throw new WebApplicationException(Response.status(Status.FORBIDDEN) 079 .type(MediaType.APPLICATION_XML) 080 .entity(new Error("Only 'Basic' authentication is supported!")).build()); 081 082 final String basicAuthEncoded = authorizationHeader.substring("Basic".length()).trim(); 083 final byte[] basicAuthDecodedBA = getBasicAuthEncodedBA(basicAuthEncoded); 084 final StringBuilder userNameSB = new StringBuilder(); 085 char[] password = null; 086 087 final ByteArrayInputStream in = new ByteArrayInputStream(basicAuthDecodedBA); 088 CharArrayWriter caw = new CharArrayWriter(basicAuthDecodedBA.length + 1); 089 CharArrayReader car = null; 090 try { 091 final Reader r = new InputStreamReader(in, IOUtil.CHARSET_NAME_UTF_8); 092 int charsReadTotal = 0; 093 int charsRead; 094 do { 095 final char[] c = new char[10]; 096 charsRead = r.read(c); 097 caw.write(c); 098 099 if (charsRead > 0) 100 charsReadTotal += charsRead; 101 } while (charsRead >= 0); 102 103 charsRead = 0; 104 105 car = new CharArrayReader(caw.toCharArray()); 106 int charsReadTotalCheck = 0; 107 108 while (charsRead >= 0 && charsRead < charsReadTotal) { 109 final char[] cbuf = new char[1]; 110 charsRead = car.read(cbuf); 111 if (charsRead > 0) 112 charsReadTotalCheck += charsRead; 113 114 if (cbuf[0] == ':') 115 break; 116 117 userNameSB.append(cbuf[0]); 118 } 119 120 if (charsRead >= 0 && charsRead < charsReadTotal) { 121 password = new char[charsReadTotal - charsReadTotalCheck]; 122 final int passwordSize = car.read(password); 123 if (passwordSize + charsReadTotalCheck != charsReadTotal) 124 throw new IllegalStateException("passwordSize and charsRead must match charsReadTotal!" 125 + " passwordSize=" + passwordSize 126 + ", charsRead=" + charsRead 127 + ", charsReadTotal=" + charsReadTotal);//TODO for testing 128 } 129 } catch (final Exception e) { 130 throw new WebApplicationException(Response.status(Status.INTERNAL_SERVER_ERROR).type(MediaType.APPLICATION_XML).entity(new Error(e)).build()); 131 } finally { 132 // For extra safety: Overwrite all sensitive memory with 0. 133 Arrays.fill(basicAuthDecodedBA, (byte)0); 134 135 final char[] zeroArray = new char[] {0}; 136 // overwrite caw & car: 137 if (caw != null) { 138 final int oldCawSize = caw.size(); 139 caw.reset(); 140 try { 141 if (car != null) { 142 car.reset(); 143 } 144 for (int i = 0; i < oldCawSize; ++i) 145 caw.write(zeroArray); 146 car = new CharArrayReader(caw.toCharArray()); 147 car.close(); 148 caw.reset(); 149 caw = null; 150 } catch (final IOException e) { 151 throw new RuntimeException(e); 152 } 153 } 154 } 155 156 final Auth auth = new Auth(); 157 auth.setUserName(userNameSB.toString()); 158 auth.setPassword(password); 159 this.auth = auth; 160 } 161 return auth; 162 } 163 164 private byte[] getBasicAuthEncodedBA(final String basicAuthEncoded) { 165 byte[] basicAuthDecodedBA; 166 try { 167 basicAuthDecodedBA = Base64.decode(basicAuthEncoded.getBytes(IOUtil.CHARSET_NAME_UTF_8)); 168 } catch (final UnsupportedEncodingException e1) { 169 throw new RuntimeException(e1); 170 } 171 return basicAuthDecodedBA; 172 } 173 174 /** 175 * Get the {@link Auth} information via {@link #getAuth()} and verify, if they are valid. 176 * @return the {@link Auth} information via {@link #getAuth()}; never <code>null</code>. 177 * @throws WebApplicationException with {@link Status#UNAUTHORIZED}, if the client did not send an 'Authorization' header 178 * or if user-name / password is wrong; 179 * with {@link Status#FORBIDDEN}, if there is an 'Authorization' header, but no 'Basic' authentication header (other authentication modes, like e.g. 'Digest' 180 * are not supported); with {@link Status#INTERNAL_SERVER_ERROR}, if there was an {@link IOException}. 181 */ 182 protected String authenticateAndReturnUserName() 183 throws WebApplicationException 184 { 185 final UUID serverRepositoryId = LocalRepoRegistryImpl.getInstance().getRepositoryId(repositoryName); 186 if (serverRepositoryId == null) { 187 throw new WebApplicationException(Response.status(Status.NOT_FOUND) 188 .type(MediaType.APPLICATION_XML) 189 .entity(new Error(String.format("HTTP 404: repositoryName='%s' is neither an alias nor an ID of a known repository!", repositoryName))).build()); 190 } 191 192 // We don't clear this auth anymore, because we might need to invoke this authenticateAndReturnUserName() in service-sub-classes 193 // again, before delegating to the super-service-method. 194 final Auth auth = getAuth(); 195 final UUID clientRepositoryId = getClientRepositoryIdFromUserName(auth.getUserName()); 196 if (clientRepositoryId != null) { 197 if (TransientRepoPasswordManager.getInstance().isPasswordValid(serverRepositoryId, clientRepositoryId, auth.getPassword())) 198 return auth.getUserName(); 199 else 200 throw newUnauthorizedException(); 201 } else{ 202 return LdapClientProvider.getInstance().getClient().authenticate(auth); 203 } 204 } 205 206 protected UUID getClientRepositoryIdFromUserName(final String userName) { 207 if (assertNotNull(userName, "userName").startsWith(AuthConstants.USER_NAME_REPOSITORY_ID_PREFIX)) { 208 final String repositoryIdString = userName.substring(AuthConstants.USER_NAME_REPOSITORY_ID_PREFIX.length()); 209 final UUID clientRepositoryId = UUID.fromString(repositoryIdString); 210 return clientRepositoryId; 211 } 212 return null; 213 } 214 215 protected UUID getClientRepositoryIdFromUserNameOrFail(final String userName) { 216 final UUID clientRepositoryId = getClientRepositoryIdFromUserName(userName); 217 if (clientRepositoryId == null) 218 throw new IllegalArgumentException(String.format("userName='%s' is not a repository!", userName)); 219 220 return clientRepositoryId; 221 } 222 223 private WebApplicationException newUnauthorizedException() { 224 // TODO maybe better throw a new javax.ws.rs.NotAuthorizedException? 225 return new NotAuthorizedException(); 226 } 227 228 protected RepoTransport authenticateAndCreateLocalRepoTransport() { 229 final String userName = authenticateAndReturnUserName(); 230 final UUID clientRepositoryId = getClientRepositoryIdFromUserNameOrFail(userName); 231 final URL localRootURL = getLocalRootURL(clientRepositoryId); 232 final RepoTransportFactory repoTransportFactory = RepoTransportFactoryRegistry.getInstance().getRepoTransportFactoryOrFail(localRootURL); 233 final RepoTransport repoTransport = repoTransportFactory.createRepoTransport(localRootURL, clientRepositoryId); 234 return repoTransport; 235 } 236 237 protected RepoTransport authenticateWithLdap(){ 238 authenticateAndReturnUserName(); 239 final File localRoot = LocalRepoRegistryImpl.getInstance().getLocalRootForRepositoryNameOrFail(repositoryName); 240 URL localRootURL; 241 try { 242 localRootURL = localRoot.toURI().toURL(); 243 localRootURL = appendEmptyPathPrefix(localRootURL); 244 } catch (MalformedURLException e) { 245 throw new RuntimeException(e); 246 } 247 final RepoTransportFactory repoTransportFactory = RepoTransportFactoryRegistry.getInstance().getRepoTransportFactoryOrFail(localRootURL); 248 return repoTransportFactory.createRepoTransport(localRootURL, null); 249 } 250 251 protected URL authenticateAndGetLocalRootURL() { 252 final String userName = authenticateAndReturnUserName(); 253 final UUID clientRepositoryId = getClientRepositoryIdFromUserNameOrFail(userName); 254 return getLocalRootURL(clientRepositoryId); 255 } 256 257 protected URL getLocalRootURL(final UUID clientRepositoryId) { 258 assertNotNull(repositoryName, "repositoryName"); 259 final File localRoot = LocalRepoRegistryImpl.getInstance().getLocalRootForRepositoryNameOrFail(repositoryName); 260 final LocalRepoManager localRepoManager = LocalRepoManagerFactory.Helper.getInstance().createLocalRepoManagerForExistingRepository(localRoot); 261 try { 262 final String localPathPrefix = localRepoManager.getLocalPathPrefixOrFail(clientRepositoryId); 263 URL localRootURL; 264 try { 265 localRootURL = localRoot.toURI().toURL(); 266 } catch (final MalformedURLException e) { 267 throw new RuntimeException(e); 268 } 269 270 localRootURL = UrlUtil.appendNonEncodedPath(localRootURL, localPathPrefix); 271 272 return localRootURL; 273 } finally { 274 localRepoManager.close(); 275 } 276 } 277 278 private URL appendEmptyPathPrefix(URL localRoot){ 279 return UrlUtil. appendNonEncodedPath(localRoot, ""); 280 } 281}