Skip to content

Commit

Permalink
JAMES-1783 POSTing on download endpoint should return a token
Browse files Browse the repository at this point in the history
  • Loading branch information
Raphael Ouazana committed Jun 29, 2016
1 parent 9963086 commit a5bd1c7
Show file tree
Hide file tree
Showing 18 changed files with 644 additions and 101 deletions.
Expand Up @@ -22,12 +22,14 @@
import java.util.concurrent.TimeUnit;

import org.apache.james.jmap.api.AccessTokenManager;
import org.apache.james.jmap.api.ContinuationTokenManager;
import org.apache.james.jmap.api.SimpleTokenFactory;
import org.apache.james.jmap.api.SimpleTokenManager;
import org.apache.james.jmap.api.access.AccessTokenRepository;
import org.apache.james.jmap.crypto.AccessTokenManagerImpl;
import org.apache.james.jmap.crypto.JamesSignatureHandler;
import org.apache.james.jmap.crypto.SignatureHandler;
import org.apache.james.jmap.crypto.SignedContinuationTokenManager;
import org.apache.james.jmap.crypto.SignedTokenFactory;
import org.apache.james.jmap.crypto.SignedTokenManager;
import org.apache.james.jmap.model.MessageFactory;
import org.apache.james.jmap.model.MessagePreviewGenerator;
import org.apache.james.jmap.send.MailFactory;
Expand All @@ -51,7 +53,7 @@ public class JMAPCommonModule extends AbstractModule {
protected void configure() {
bind(JamesSignatureHandler.class).in(Scopes.SINGLETON);
bind(DefaultZonedDateTimeProvider.class).in(Scopes.SINGLETON);
bind(SignedContinuationTokenManager.class).in(Scopes.SINGLETON);
bind(SignedTokenManager.class).in(Scopes.SINGLETON);
bind(AccessTokenManagerImpl.class).in(Scopes.SINGLETON);
bind(MailSpool.class).in(Scopes.SINGLETON);
bind(MailFactory.class).in(Scopes.SINGLETON);
Expand All @@ -61,7 +63,8 @@ protected void configure() {

bind(SignatureHandler.class).to(JamesSignatureHandler.class);
bind(ZonedDateTimeProvider.class).to(DefaultZonedDateTimeProvider.class);
bind(ContinuationTokenManager.class).to(SignedContinuationTokenManager.class);
bind(SimpleTokenManager.class).to(SignedTokenManager.class);
bind(SimpleTokenFactory.class).to(SignedTokenFactory.class);
bind(AutomaticallySentMailDetector.class).to(AutomaticallySentMailDetectorImpl.class);

bindConstant().annotatedWith(Names.named(AccessTokenRepository.TOKEN_EXPIRATION_IN_MS)).to(DEFAULT_TOKEN_EXPIRATION_IN_MS);
Expand Down
Expand Up @@ -36,7 +36,9 @@

import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import com.jayway.restassured.http.ContentType;
import com.jayway.restassured.response.Response;
import com.jayway.restassured.specification.RequestSpecification;

import cucumber.api.java.en.Given;
import cucumber.api.java.en.Then;
Expand Down Expand Up @@ -74,21 +76,23 @@ public void appendMessageWithAttachmentToMailbox(String user, String mailbox, St
@When("^\"([^\"]*)\" checks for the availability of the attachment endpoint$")
public void optionDownload(String username) throws Throwable {
AccessToken accessToken = userStepdefs.tokenByUser.get(username);
RequestSpecification with = with();
if (accessToken != null) {
with().header("Authorization", accessToken.serialize());
with.header("Authorization", accessToken.serialize());
}

response = with().options("/download/myBlob");
response = with.options("/download/myBlob");
}

@When("^\"([^\"]*)\" downloads \"([^\"]*)\"$")
public void downloads(String username, String attachmentId) throws Throwable {
String blobId = blobIdByAttachmentId.get(attachmentId);
AccessToken accessToken = userStepdefs.tokenByUser.get(username);
RequestSpecification with = with();
if (accessToken != null) {
with().header("Authorization", accessToken.serialize());
with.header("Authorization", accessToken.serialize());
}
response = with().get("/download/" + blobId);
response = with.get("/download/" + blobId);
}


Expand All @@ -109,6 +113,18 @@ public void getDownloadWithWrongBlobId(String username) throws Throwable {
.get("/download/badbadbadbadbadbadbadbadbadbadbadbadbadb");
}

@When("^\"([^\"]*)\" asks for a token for attachment \"([^\"]*)\"$")
public void postDownload(String username, String attachmentId) throws Throwable {
String blobId = blobIdByAttachmentId.get(attachmentId);
AccessToken accessToken = userStepdefs.tokenByUser.get(username);
RequestSpecification with = with();
if (accessToken != null) {
with = with.header("Authorization", accessToken.serialize());
}
response = with
.post("/download/" + blobId);
}

@Then("^the user should be authorized$")
public void httpStatusDifferentFromUnauthorized() throws Exception {
response.then()
Expand Down Expand Up @@ -139,4 +155,12 @@ public void httpNotFoundStatus() throws Throwable {
response.then()
.statusCode(404);
}

@Then("^the user should receive an attachment access token$")
public void accessTokenResponse() throws Throwable {
response.then()
.statusCode(200)
.contentType(ContentType.TEXT)
.content(notNullValue());
}
}
@@ -0,0 +1,17 @@
Feature: Alternative authentication mechanism for getting attachment via a POST request returning a specific authentication token
As a James user
I want to retrieve my attachments without an alternative authentication mechanim

Background:
Given a domain named "domain.tld"
And a connected user "username@domain.tld"
And "username@domain.tld" has a mailbox "inbox"

Scenario: Asking for an attachment access token with an unknown blobId
When "username@domain.tld" asks for a token for attachment "123"
Then the user should receive a not found response

Scenario: Asking for an attachment access token with a previously stored blobId
Given "username@domain.tld" mailbox "inbox" contains a message "1" with an attachment "2"
When "username@domain.tld" asks for a token for attachment "2"
Then the user should receive an attachment access token
Expand Up @@ -25,7 +25,7 @@
import cucumber.api.junit.Cucumber;

@RunWith(Cucumber.class)
@CucumberOptions(features={"classpath:cucumber/DownloadEndpoint.feature", "classpath:cucumber/DownloadGet.feature"},
@CucumberOptions(features={"classpath:cucumber/DownloadEndpoint.feature", "classpath:cucumber/DownloadGet.feature", "classpath:cucumber/DownloadPost.feature"},
glue={"org.apache.james.jmap.methods.integration", "org.apache.james.jmap.memory.cucumber"},
tags = {"~@Ignore"},
strict = true)
Expand Down
Expand Up @@ -27,7 +27,8 @@
import javax.servlet.http.HttpServletResponse;

import org.apache.james.jmap.api.AccessTokenManager;
import org.apache.james.jmap.api.ContinuationTokenManager;
import org.apache.james.jmap.api.SimpleTokenFactory;
import org.apache.james.jmap.api.SimpleTokenManager;
import org.apache.james.jmap.api.access.AccessToken;
import org.apache.james.jmap.exceptions.BadRequestException;
import org.apache.james.jmap.exceptions.InternalErrorException;
Expand All @@ -54,13 +55,15 @@ public class AuthenticationServlet extends HttpServlet {

private final ObjectMapper mapper;
private final UsersRepository usersRepository;
private final ContinuationTokenManager continuationTokenManager;
private final SimpleTokenManager simpleTokenManager;
private final AccessTokenManager accessTokenManager;
private final SimpleTokenFactory simpleTokenFactory;

@Inject
@VisibleForTesting AuthenticationServlet(UsersRepository usersRepository, ContinuationTokenManager continuationTokenManager, AccessTokenManager accessTokenManager) {
@VisibleForTesting AuthenticationServlet(UsersRepository usersRepository, SimpleTokenManager simpleTokenManager, SimpleTokenFactory simpleTokenFactory, AccessTokenManager accessTokenManager) {
this.usersRepository = usersRepository;
this.continuationTokenManager = continuationTokenManager;
this.simpleTokenManager = simpleTokenManager;
this.simpleTokenFactory = simpleTokenFactory;
this.accessTokenManager = accessTokenManager;
this.mapper = new MultipleObjectMapperBuilder()
.registerClass(ContinuationTokenRequest.UNIQUE_JSON_PATH, ContinuationTokenRequest.class)
Expand Down Expand Up @@ -129,7 +132,7 @@ private void handleContinuationTokenRequest(ContinuationTokenRequest request, Ht
try {
ContinuationTokenResponse continuationTokenResponse = ContinuationTokenResponse
.builder()
.continuationToken(continuationTokenManager.generateToken(request.getUsername()))
.continuationToken(simpleTokenFactory.generateContinuationToken(request.getUsername()))
.methods(ContinuationTokenResponse.AuthenticationMethod.PASSWORD)
.build();
mapper.writeValue(resp.getOutputStream(), continuationTokenResponse);
Expand All @@ -139,7 +142,7 @@ private void handleContinuationTokenRequest(ContinuationTokenRequest request, Ht
}

private void handleAccessTokenRequest(AccessTokenRequest request, HttpServletResponse resp) throws IOException {
switch (continuationTokenManager.getValidity(request.getToken())) {
switch (simpleTokenManager.getValidity(request.getToken())) {
case EXPIRED:
returnRestartAuthentication(resp);
break;
Expand Down
Expand Up @@ -32,6 +32,7 @@
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.io.IOUtils;
import org.apache.james.jmap.api.SimpleTokenFactory;
import org.apache.james.mailbox.MailboxSession;
import org.apache.james.mailbox.exception.AttachmentNotFoundException;
import org.apache.james.mailbox.exception.MailboxException;
Expand All @@ -49,12 +50,50 @@ public class DownloadServlet extends HttpServlet {

private static final String ROOT_URL = "/";
private static final Logger LOGGER = LoggerFactory.getLogger(DownloadServlet.class);
private static final String TEXT_PLAIN_CONTENT_TYPE = "text/plain";

private final MailboxSessionMapperFactory mailboxSessionMapperFactory;
private final SimpleTokenFactory simpleTokenFactory;

@Inject
@VisibleForTesting DownloadServlet(MailboxSessionMapperFactory mailboxSessionMapperFactory) {
@VisibleForTesting DownloadServlet(MailboxSessionMapperFactory mailboxSessionMapperFactory, SimpleTokenFactory simpleTokenFactory) {
this.mailboxSessionMapperFactory = mailboxSessionMapperFactory;
this.simpleTokenFactory = simpleTokenFactory;
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException {
String pathInfo = req.getPathInfo();
if (Strings.isNullOrEmpty(pathInfo) || pathInfo.equals(ROOT_URL)) {
resp.setStatus(SC_BAD_REQUEST);
} else {
respondAttachmentAccessToken(getMailboxSession(req), blobIdFrom(pathInfo), resp);
}
}

private void respondAttachmentAccessToken(MailboxSession mailboxSession, String blobId, HttpServletResponse resp) {
try {
if (! attachmentExists(mailboxSession, blobId)) {
resp.setStatus(SC_NOT_FOUND);
return;
}
resp.setContentType(TEXT_PLAIN_CONTENT_TYPE);
resp.getOutputStream().print(simpleTokenFactory.generateAttachmentAccessToken(mailboxSession.getUser().getUserName(), blobId).serialize());
resp.setStatus(SC_OK);
} catch (MailboxException | IOException e) {
LOGGER.error("Error while asking attachment access token", e);
resp.setStatus(SC_INTERNAL_SERVER_ERROR);
}
}

private boolean attachmentExists(MailboxSession mailboxSession, String blobId) throws MailboxException {
AttachmentMapper attachmentMapper = mailboxSessionMapperFactory.createAttachmentMapper(mailboxSession);
try {
attachmentMapper.getAttachment(AttachmentId.from(blobId));
return true;
} catch (AttachmentNotFoundException e) {
return false;
}
}

@Override
Expand Down
Expand Up @@ -57,6 +57,9 @@ private JMAPServer(JMAPConfiguration jmapConfiguration,
.only()
.serveAsOneLevelTemplate(JMAPUrls.DOWNLOAD)
.with(downloadServlet)
.filterAsOneLevelTemplate(JMAPUrls.DOWNLOAD)
.with(new AllowAllCrossOriginRequests(bypass(authenticationFilter).on("GET").and("OPTIONS").only()))
.only()
.build());
}

Expand Down
Expand Up @@ -19,19 +19,11 @@

package org.apache.james.jmap.api;

import org.apache.james.jmap.model.AttachmentAccessToken;
import org.apache.james.jmap.model.ContinuationToken;

public interface ContinuationTokenManager {
enum ContinuationTokenStatus {
OK,
INVALID,
EXPIRED
}

ContinuationToken generateToken(String username);

ContinuationTokenStatus getValidity(ContinuationToken token);

boolean isValid(ContinuationToken token);
public interface SimpleTokenFactory {
ContinuationToken generateContinuationToken(String username);

AttachmentAccessToken generateAttachmentAccessToken(String username, String blobId);
}
@@ -0,0 +1,34 @@
/****************************************************************
* Licensed to the Apache Software Foundation (ASF) under one *
* or more contributor license agreements. See the NOTICE file *
* distributed with this work for additional information *
* regarding copyright ownership. The ASF licenses this file *
* to you under the Apache License, Version 2.0 (the *
* "License"); you may not use this file except in compliance *
* with the License. You may obtain a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, *
* software distributed under the License is distributed on an *
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
* KIND, either express or implied. See the License for the *
* specific language governing permissions and limitations *
* under the License. *
****************************************************************/

package org.apache.james.jmap.api;

import org.apache.james.jmap.model.SignedExpiringToken;

public interface SimpleTokenManager {
enum TokenStatus {
OK,
INVALID,
EXPIRED
}

TokenStatus getValidity(SignedExpiringToken token);

boolean isValid(SignedExpiringToken token);
}
@@ -0,0 +1,73 @@
/****************************************************************
* Licensed to the Apache Software Foundation (ASF) under one *
* or more contributor license agreements. See the NOTICE file *
* distributed with this work for additional information *
* regarding copyright ownership. The ASF licenses this file *
* to you under the Apache License, Version 2.0 (the *
* "License"); you may not use this file except in compliance *
* with the License. You may obtain a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, *
* software distributed under the License is distributed on an *
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
* KIND, either express or implied. See the License for the *
* specific language governing permissions and limitations *
* under the License. *
****************************************************************/

package org.apache.james.jmap.crypto;

import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;

import javax.inject.Inject;

import org.apache.james.jmap.api.SimpleTokenFactory;
import org.apache.james.jmap.model.AttachmentAccessToken;
import org.apache.james.jmap.model.ContinuationToken;
import org.apache.james.util.date.ZonedDateTimeProvider;

import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;

public class SignedTokenFactory implements SimpleTokenFactory {

private final SignatureHandler signatureHandler;
private final ZonedDateTimeProvider zonedDateTimeProvider;

@Inject
public SignedTokenFactory(SignatureHandler signatureHandler, ZonedDateTimeProvider zonedDateTimeProvider) {
this.signatureHandler = signatureHandler;
this.zonedDateTimeProvider = zonedDateTimeProvider;
}

@Override
public ContinuationToken generateContinuationToken(String username) {
Preconditions.checkNotNull(username);
ZonedDateTime expirationTime = zonedDateTimeProvider.get().plusMinutes(15);
return new ContinuationToken(username,
expirationTime,
signatureHandler.sign(
Joiner.on(ContinuationToken.SEPARATOR)
.join(username,
DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(expirationTime))));
}

@Override
public AttachmentAccessToken generateAttachmentAccessToken(String username, String blobId) {
Preconditions.checkArgument(! Strings.isNullOrEmpty(blobId));
ZonedDateTime expirationTime = zonedDateTimeProvider.get().plusMinutes(5);
return AttachmentAccessToken.builder()
.username(username)
.blobId(blobId)
.expirationDate(expirationTime)
.signature(signatureHandler.sign(Joiner.on(AttachmentAccessToken.SEPARATOR)
.join(username,
blobId,
DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(expirationTime))))
.build();
}
}

0 comments on commit a5bd1c7

Please sign in to comment.