001package co.codewizards.cloudstore.rest.server.service; 002 003import static java.util.Objects.*; 004 005import java.io.CharArrayReader; 006import java.io.CharArrayWriter; 007import java.io.IOException; 008import java.io.InputStreamReader; 009import java.io.Reader; 010import java.io.UnsupportedEncodingException; 011import java.net.MalformedURLException; 012import java.net.URL; 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.io.ByteArrayInputStream; 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 char[] ca = null; 089 CharArrayWriter caw = new CharArrayWriter(basicAuthDecodedBA.length + 1); 090 CharArrayReader car = null; 091 try { 092 final Reader r = new InputStreamReader(in, IOUtil.CHARSET_NAME_UTF_8); 093 int charsReadTotal = 0; 094 int charsRead; 095 do { 096 final char[] c = new char[10]; 097 charsRead = r.read(c); 098 caw.write(c); 099 100 if (charsRead > 0) 101 charsReadTotal += charsRead; 102 } while (charsRead >= 0); 103 104 charsRead = 0; 105 106 car = new CharArrayReader(ca = caw.toCharArray()); 107 int charsReadTotalCheck = 0; 108 109 while (charsRead >= 0 && charsRead < charsReadTotal) { 110 final char[] cbuf = new char[1]; 111 charsRead = car.read(cbuf); 112 if (charsRead > 0) 113 charsReadTotalCheck += charsRead; 114 115 if (cbuf[0] == ':') 116 break; 117 118 userNameSB.append(cbuf[0]); 119 } 120 121 if (charsRead >= 0 && charsRead < charsReadTotal) { 122 password = new char[charsReadTotal - charsReadTotalCheck]; 123 final int passwordSize = car.read(password); 124 if (passwordSize + charsReadTotalCheck != charsReadTotal) 125 throw new IllegalStateException("passwordSize and charsRead must match charsReadTotal!" 126 + " passwordSize=" + passwordSize 127 + ", charsRead=" + charsRead 128 + ", charsReadTotal=" + charsReadTotal);//TODO for testing 129 } 130 } catch (final Exception e) { 131 throw new WebApplicationException(Response.status(Status.INTERNAL_SERVER_ERROR).type(MediaType.APPLICATION_XML).entity(new Error(e)).build()); 132 } finally { 133 // For extra safety: Overwrite all sensitive memory with 0. 134 // Unfortunately, we cannot overwrite auth.password. But at least we minimize things as much as possible. 135 Arrays.fill(basicAuthDecodedBA, (byte)0); 136 137 if (ca != null) 138 Arrays.fill(ca, (char)0); 139 140 if (caw != null) { 141 final char[] zeroArray = new char[caw.size()]; 142 caw.reset(); 143 try { 144 caw.write(zeroArray); 145 caw = null; 146 } catch (final IOException e) { 147 throw new RuntimeException(e); 148 } 149 } 150 } 151 152 final Auth auth = new Auth(); 153 auth.setUserName(userNameSB.toString()); 154 auth.setPassword(password); 155 this.auth = auth; 156 } 157 return auth; 158 } 159 160 private byte[] getBasicAuthEncodedBA(final String basicAuthEncoded) { 161 byte[] basicAuthDecodedBA; 162 try { 163 basicAuthDecodedBA = Base64.decode(basicAuthEncoded.getBytes(IOUtil.CHARSET_NAME_UTF_8)); 164 } catch (final UnsupportedEncodingException e1) { 165 throw new RuntimeException(e1); 166 } 167 return basicAuthDecodedBA; 168 } 169 170 /** 171 * Get the {@link Auth} information via {@link #getAuth()} and verify, if they are valid. 172 * @return the {@link Auth} information via {@link #getAuth()}; never <code>null</code>. 173 * @throws WebApplicationException with {@link Status#UNAUTHORIZED}, if the client did not send an 'Authorization' header 174 * or if user-name / password is wrong; 175 * with {@link Status#FORBIDDEN}, if there is an 'Authorization' header, but no 'Basic' authentication header (other authentication modes, like e.g. 'Digest' 176 * are not supported); with {@link Status#INTERNAL_SERVER_ERROR}, if there was an {@link IOException}. 177 */ 178 protected String authenticateAndReturnUserName() 179 throws WebApplicationException 180 { 181 final UUID serverRepositoryId = LocalRepoRegistryImpl.getInstance().getRepositoryId(repositoryName); 182 if (serverRepositoryId == null) { 183 throw new WebApplicationException(Response.status(Status.NOT_FOUND) 184 .type(MediaType.APPLICATION_XML) 185 .entity(new Error(String.format("HTTP 404: repositoryName='%s' is neither an alias nor an ID of a known repository!", repositoryName))).build()); 186 } 187 188 // We don't clear this auth anymore, because we might need to invoke this authenticateAndReturnUserName() in service-sub-classes 189 // again, before delegating to the super-service-method. 190 final Auth auth = getAuth(); 191 final UUID clientRepositoryId = getClientRepositoryIdFromUserName(auth.getUserName()); 192 if (clientRepositoryId != null) { 193 if (TransientRepoPasswordManager.getInstance().isPasswordValid(serverRepositoryId, clientRepositoryId, auth.getPassword())) 194 return auth.getUserName(); 195 else 196 throw newUnauthorizedException(); 197 } else{ 198 return LdapClientProvider.getInstance().getClient().authenticate(auth); 199 } 200 } 201 202 protected UUID getClientRepositoryIdFromUserName(final String userName) { 203 if (requireNonNull(userName, "userName").startsWith(AuthConstants.USER_NAME_REPOSITORY_ID_PREFIX)) { 204 final String repositoryIdString = userName.substring(AuthConstants.USER_NAME_REPOSITORY_ID_PREFIX.length()); 205 final UUID clientRepositoryId = UUID.fromString(repositoryIdString); 206 return clientRepositoryId; 207 } 208 return null; 209 } 210 211 protected UUID getClientRepositoryIdFromUserNameOrFail(final String userName) { 212 final UUID clientRepositoryId = getClientRepositoryIdFromUserName(userName); 213 if (clientRepositoryId == null) 214 throw new IllegalArgumentException(String.format("userName='%s' is not a repository!", userName)); 215 216 return clientRepositoryId; 217 } 218 219 private WebApplicationException newUnauthorizedException() { 220 // TODO maybe better throw a new javax.ws.rs.NotAuthorizedException? 221 return new NotAuthorizedException(); 222 } 223 224 protected RepoTransport authenticateAndCreateLocalRepoTransport() { 225 final String userName = authenticateAndReturnUserName(); 226 final UUID clientRepositoryId = getClientRepositoryIdFromUserNameOrFail(userName); 227 final URL localRootURL = getLocalRootURL(clientRepositoryId); 228 final RepoTransportFactory repoTransportFactory = RepoTransportFactoryRegistry.getInstance().getRepoTransportFactoryOrFail(localRootURL); 229 final RepoTransport repoTransport = repoTransportFactory.createRepoTransport(localRootURL, clientRepositoryId); 230 return repoTransport; 231 } 232 233 protected RepoTransport authenticateWithLdap(){ 234 authenticateAndReturnUserName(); 235 final File localRoot = LocalRepoRegistryImpl.getInstance().getLocalRootForRepositoryNameOrFail(repositoryName); 236 URL localRootURL; 237 try { 238 localRootURL = localRoot.toURI().toURL(); 239 localRootURL = appendEmptyPathPrefix(localRootURL); 240 } catch (MalformedURLException e) { 241 throw new RuntimeException(e); 242 } 243 final RepoTransportFactory repoTransportFactory = RepoTransportFactoryRegistry.getInstance().getRepoTransportFactoryOrFail(localRootURL); 244 return repoTransportFactory.createRepoTransport(localRootURL, null); 245 } 246 247 protected URL authenticateAndGetLocalRootURL() { 248 final String userName = authenticateAndReturnUserName(); 249 final UUID clientRepositoryId = getClientRepositoryIdFromUserNameOrFail(userName); 250 return getLocalRootURL(clientRepositoryId); 251 } 252 253 protected URL getLocalRootURL(final UUID clientRepositoryId) { 254 requireNonNull(repositoryName, "repositoryName"); 255 final File localRoot = LocalRepoRegistryImpl.getInstance().getLocalRootForRepositoryNameOrFail(repositoryName); 256 final LocalRepoManager localRepoManager = LocalRepoManagerFactory.Helper.getInstance().createLocalRepoManagerForExistingRepository(localRoot); 257 try { 258 final String localPathPrefix = localRepoManager.getLocalPathPrefixOrFail(clientRepositoryId); 259 URL localRootURL; 260 try { 261 localRootURL = localRoot.toURI().toURL(); 262 } catch (final MalformedURLException e) { 263 throw new RuntimeException(e); 264 } 265 266 localRootURL = UrlUtil.appendNonEncodedPath(localRootURL, localPathPrefix); 267 268 return localRootURL; 269 } finally { 270 localRepoManager.close(); 271 } 272 } 273 274 private URL appendEmptyPathPrefix(URL localRoot){ 275 return UrlUtil. appendNonEncodedPath(localRoot, ""); 276 } 277}