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}