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}