From d29575ff1b8e275d4ca83d6903578169c20640aa Mon Sep 17 00:00:00 2001 From: Jamal Kaabi-Mofrad Date: Thu, 2 Jun 2016 21:26:29 +0000 Subject: [PATCH] Merged API-STRIKES-BACK (5.2.0) to HEAD (5.2) 125604 jkaabimofrad: RA-933: Initial commit for ticket base authentication. git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@127556 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- config/alfresco/public-rest-context.xml | 25 ++ .../alfresco/rest/api/Authentications.java | 38 +++ .../api/PublicApiDeclarativeRegistry.java | 21 +- .../AuthenticationTicketsEntityResource.java | 85 ++++++ .../api/authentications/package-info.java | 5 + .../rest/api/impl/AuthenticationsImpl.java | 153 ++++++++++ .../alfresco/rest/api/model/LoginTicket.java | 71 +++++ .../rest/api/model/LoginTicketResponse.java | 55 ++++ .../framework/core/ResourceInspector.java | 2 +- .../rest/api/tests/AbstractBaseApiTest.java | 41 ++- .../org/alfresco/rest/api/tests/ApiTest.java | 1 + .../rest/api/tests/AuthenticationsTest.java | 277 ++++++++++++++++++ 12 files changed, 760 insertions(+), 14 deletions(-) create mode 100644 source/java/org/alfresco/rest/api/Authentications.java create mode 100644 source/java/org/alfresco/rest/api/authentications/AuthenticationTicketsEntityResource.java create mode 100644 source/java/org/alfresco/rest/api/authentications/package-info.java create mode 100644 source/java/org/alfresco/rest/api/impl/AuthenticationsImpl.java create mode 100644 source/java/org/alfresco/rest/api/model/LoginTicket.java create mode 100644 source/java/org/alfresco/rest/api/model/LoginTicketResponse.java create mode 100644 source/test-java/org/alfresco/rest/api/tests/AuthenticationsTest.java diff --git a/config/alfresco/public-rest-context.xml b/config/alfresco/public-rest-context.xml index f40fa2c2f6..0fb1592ef0 100644 --- a/config/alfresco/public-rest-context.xml +++ b/config/alfresco/public-rest-context.xml @@ -1147,4 +1147,29 @@ + + + + + + + + + + + org.alfresco.rest.api.Authentications + + + + + + + + + + + + + + diff --git a/source/java/org/alfresco/rest/api/Authentications.java b/source/java/org/alfresco/rest/api/Authentications.java new file mode 100644 index 0000000000..5edccb814d --- /dev/null +++ b/source/java/org/alfresco/rest/api/Authentications.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2005-2016 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ + +package org.alfresco.rest.api; + +import org.alfresco.rest.api.model.LoginTicket; +import org.alfresco.rest.api.model.LoginTicketResponse; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.rest.framework.webscripts.WithResponse; + +/** + * @author Jamal Kaabi-Mofrad + */ +public interface Authentications +{ + + LoginTicketResponse createTicket(LoginTicket loginRequest, Parameters parameters); + + LoginTicketResponse validateTicket(String ticket, Parameters parameters, WithResponse withResponse); + + void deleteTicket(String ticket, Parameters parameters, WithResponse withResponse); +} diff --git a/source/java/org/alfresco/rest/api/PublicApiDeclarativeRegistry.java b/source/java/org/alfresco/rest/api/PublicApiDeclarativeRegistry.java index 2ca73e2296..a73bd1d22d 100644 --- a/source/java/org/alfresco/rest/api/PublicApiDeclarativeRegistry.java +++ b/source/java/org/alfresco/rest/api/PublicApiDeclarativeRegistry.java @@ -29,15 +29,9 @@ import java.util.Set; import org.alfresco.rest.framework.Api; import org.alfresco.rest.framework.core.ResourceLocator; import org.alfresco.rest.framework.core.ResourceWithMetadata; -import org.alfresco.rest.framework.core.exceptions.DeletedResourceException; -import org.alfresco.rest.framework.core.exceptions.UnsupportedResourceOperationException; import org.alfresco.rest.framework.resource.actions.interfaces.BinaryResourceAction; import org.alfresco.rest.framework.resource.actions.interfaces.EntityResourceAction; -import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction; import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceBinaryAction; -import org.alfresco.rest.framework.resource.actions.interfaces.ResourceAction; -import org.alfresco.rest.framework.resource.content.BinaryResource; -import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; import org.apache.commons.lang.StringUtils; import org.springframework.extensions.webscripts.ArgumentTypeDescription; import org.springframework.extensions.webscripts.Container; @@ -102,19 +96,20 @@ public class PublicApiDeclarativeRegistry extends DeclarativeRegistry Match match = null; HttpMethod httpMethod = HttpMethod.valueOf(method); - if (httpMethod.equals(HttpMethod.GET)) + boolean isPost = httpMethod.equals(HttpMethod.POST); + if (httpMethod.equals(HttpMethod.GET) || isPost) { - if (uri.equals(PublicApiTenantWebScriptServletRequest.NETWORKS_PATH)) + if (!isPost && uri.equals(PublicApiTenantWebScriptServletRequest.NETWORKS_PATH)) { - Map templateVars = new HashMap(); + Map templateVars = new HashMap<>(); templateVars.put("apiScope", "public"); templateVars.put("apiVersion", "1"); templateVars.put("apiName", "networks"); match = new Match("", templateVars, "", getNetworksWebScript); } - else if (uri.equals(PublicApiTenantWebScriptServletRequest.NETWORK_PATH)) + else if (!isPost && uri.equals(PublicApiTenantWebScriptServletRequest.NETWORK_PATH)) { - Map templateVars = new HashMap(); + Map templateVars = new HashMap<>(); templateVars.put("apiScope", "public"); templateVars.put("apiVersion", "1"); templateVars.put("apiName", "network"); @@ -155,6 +150,10 @@ public class PublicApiDeclarativeRegistry extends DeclarativeRegistry { resAction = EntityResourceAction.Read.class; } + else if (EntityResourceAction.Create.class.isAssignableFrom(rwm.getResource().getClass())) + { + resAction = EntityResourceAction.Create.class; + } } break; case PROPERTY: diff --git a/source/java/org/alfresco/rest/api/authentications/AuthenticationTicketsEntityResource.java b/source/java/org/alfresco/rest/api/authentications/AuthenticationTicketsEntityResource.java new file mode 100644 index 0000000000..8d7fbecbb6 --- /dev/null +++ b/source/java/org/alfresco/rest/api/authentications/AuthenticationTicketsEntityResource.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2005-2016 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ + +package org.alfresco.rest.api.authentications; + +import org.alfresco.rest.api.Authentications; +import org.alfresco.rest.api.model.LoginTicket; +import org.alfresco.rest.framework.WebApiDescription; +import org.alfresco.rest.framework.WebApiNoAuth; +import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; +import org.alfresco.rest.framework.resource.EntityResource; +import org.alfresco.rest.framework.resource.actions.interfaces.EntityResourceAction; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.rest.framework.webscripts.WithResponse; +import org.alfresco.util.PropertyCheck; +import org.springframework.beans.factory.InitializingBean; + +import java.util.Collections; +import java.util.List; + +/** + * @author Jamal Kaabi-Mofrad + */ +@EntityResource(name = "tickets", title = "Authentication tickets") +public class AuthenticationTicketsEntityResource implements EntityResourceAction.Create, + EntityResourceAction.ReadByIdWithResponse, + EntityResourceAction.DeleteWithResponse, + InitializingBean +{ + private Authentications authentications; + + public void setAuthentications(Authentications authentications) + { + this.authentications = authentications; + } + + @Override + public void afterPropertiesSet() throws Exception + { + PropertyCheck.mandatory(this, "authentications", authentications); + } + + @WebApiDescription(title = "Login", description = "Login.") + @WebApiNoAuth + @Override + public List create(List entity, Parameters parameters) + { + if (entity == null || entity.size() != 1) + { + throw new InvalidArgumentException("Please specify one login request only."); + } + LoginTicket result = authentications.createTicket(entity.get(0), parameters); + return Collections.singletonList(result); + } + + @WebApiDescription(title = "Validate login ticket", description = "Validates the specified ticket is still valid.") + @Override + public LoginTicket readById(String ticket, Parameters parameters, WithResponse withResponse) + { + return authentications.validateTicket(ticket, parameters, withResponse); + } + + @WebApiDescription(title = "Logout", description = "Logout.") + @Override + public void delete(String ticket, Parameters parameters, WithResponse withResponse) + { + authentications.deleteTicket(ticket, parameters, withResponse); + } +} diff --git a/source/java/org/alfresco/rest/api/authentications/package-info.java b/source/java/org/alfresco/rest/api/authentications/package-info.java new file mode 100644 index 0000000000..9852da8a6c --- /dev/null +++ b/source/java/org/alfresco/rest/api/authentications/package-info.java @@ -0,0 +1,5 @@ +@WebApi(name = "alfresco", scope = Api.SCOPE.PUBLIC, version = 1) +package org.alfresco.rest.api.authentications; + +import org.alfresco.rest.framework.Api; +import org.alfresco.rest.framework.WebApi; \ No newline at end of file diff --git a/source/java/org/alfresco/rest/api/impl/AuthenticationsImpl.java b/source/java/org/alfresco/rest/api/impl/AuthenticationsImpl.java new file mode 100644 index 0000000000..100417e139 --- /dev/null +++ b/source/java/org/alfresco/rest/api/impl/AuthenticationsImpl.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2005-2016 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ + +package org.alfresco.rest.api.impl; + +import org.alfresco.repo.security.authentication.AuthenticationException; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.TicketComponent; +import org.alfresco.rest.api.Authentications; +import org.alfresco.rest.api.model.LoginTicket; +import org.alfresco.rest.api.model.LoginTicketResponse; +import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; +import org.alfresco.rest.framework.core.exceptions.PermissionDeniedException; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.rest.framework.webscripts.WithResponse; +import org.alfresco.service.cmr.security.AuthenticationService; +import org.alfresco.util.PropertyCheck; +import org.apache.commons.lang.StringUtils; +import org.springframework.extensions.webscripts.Status; + +/** + * @author Jamal Kaabi-Mofrad + */ +public class AuthenticationsImpl implements Authentications +{ + private AuthenticationService authenticationService; + private TicketComponent ticketComponent; + + public void setAuthenticationService(AuthenticationService authenticationService) + { + this.authenticationService = authenticationService; + } + + public void setTicketComponent(TicketComponent ticketComponent) + { + this.ticketComponent = ticketComponent; + } + + public void init() + { + PropertyCheck.mandatory(this, "authenticationService", authenticationService); + PropertyCheck.mandatory(this, "ticketComponent", ticketComponent); + } + + @Override + public LoginTicketResponse createTicket(LoginTicket loginRequest, Parameters parameters) + { + validateLoginRequest(loginRequest); + try + { + // get ticket + authenticationService.authenticate(loginRequest.getUsername(), loginRequest.getPassword().toCharArray()); + + LoginTicketResponse response = new LoginTicketResponse(); + response.setUsername(loginRequest.getUsername()); + response.setTicket(authenticationService.getCurrentTicket()); + + return response; + } + catch (AuthenticationException e) + { + throw new PermissionDeniedException("Login failed"); + } + finally + { + AuthenticationUtil.clearCurrentSecurityContext(); + } + } + + @Override + public LoginTicketResponse validateTicket(String ticket, Parameters parameters, WithResponse withResponse) + { + if (StringUtils.isEmpty(ticket)) + { + throw new InvalidArgumentException("ticket can't be null or empty."); + } + + try + { + String ticketUser = ticketComponent.validateTicket(ticket); + + String currentUser = AuthenticationUtil.getFullyAuthenticatedUser(); + // do not go any further if tickets are different + // or the user is not fully authenticated + if (currentUser == null || !currentUser.equals(ticketUser)) + { + withResponse.setStatus(Status.STATUS_NOT_FOUND); + } + } + catch (AuthenticationException e) + { + withResponse.setStatus(Status.STATUS_NOT_FOUND); + } + LoginTicketResponse response = new LoginTicketResponse(); + response.setTicket(ticket); + return response; + } + + @Override + public void deleteTicket(String ticket, Parameters parameters, WithResponse withResponse) + { + if (StringUtils.isEmpty(ticket)) + { + throw new InvalidArgumentException("ticket can't be null or empty."); + } + + try + { + String ticketUser = ticketComponent.validateTicket(ticket); + + String currentUser = AuthenticationUtil.getFullyAuthenticatedUser(); + // do not go any further if tickets are different + // or the user is not fully authenticated + if (currentUser == null || !currentUser.equals(ticketUser)) + { + withResponse.setStatus(Status.STATUS_NOT_FOUND); + } + else + { + // delete the ticket + authenticationService.invalidateTicket(ticket); + } + } + catch (AuthenticationException e) + { + withResponse.setStatus(Status.STATUS_NOT_FOUND); + } + } + + protected void validateLoginRequest(LoginTicket loginTicket) + { + if (loginTicket == null || loginTicket.getUsername() == null || loginTicket.getPassword() == null) + { + throw new InvalidArgumentException("Invalid login details."); + } + } +} diff --git a/source/java/org/alfresco/rest/api/model/LoginTicket.java b/source/java/org/alfresco/rest/api/model/LoginTicket.java new file mode 100644 index 0000000000..d3327277bd --- /dev/null +++ b/source/java/org/alfresco/rest/api/model/LoginTicket.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2005-2016 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ + +package org.alfresco.rest.api.model; + +/** + * @author Jamal Kaabi-Mofrad + */ +public class LoginTicket +{ + protected String username; + protected String password; + protected String ticket; + + public String getUsername() + { + return username; + } + + public void setUsername(String username) + { + this.username = username; + } + + public String getPassword() + { + return password; + } + + public void setPassword(String password) + { + this.password = password; + } + + public String getTicket() + { + return ticket; + } + + public void setTicket(String ticket) + { + this.ticket = ticket; + } + + @Override + public String toString() + { + final StringBuilder sb = new StringBuilder(150); + sb.append("LoginTicket [username=").append(username) + .append(", password=").append(password) + .append(", ticket=").append(ticket) + .append(']'); + return sb.toString(); + } +} diff --git a/source/java/org/alfresco/rest/api/model/LoginTicketResponse.java b/source/java/org/alfresco/rest/api/model/LoginTicketResponse.java new file mode 100644 index 0000000000..24a1f53235 --- /dev/null +++ b/source/java/org/alfresco/rest/api/model/LoginTicketResponse.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2005-2016 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ + +package org.alfresco.rest.api.model; + +/** + * @author Jamal Kaabi-Mofrad + */ +public class LoginTicketResponse extends LoginTicket +{ + + public LoginTicketResponse() + { + this.password = null; + } + + @Override + public String getPassword() + { + return null; + } + + @Override + public void setPassword(String password) + { + // intentionally empty + } + + @Override + public String toString() + { + final StringBuilder sb = new StringBuilder(150); + sb.append("LoginTicketResponse [username=").append(username) + .append(", password=").append(password) + .append(", ticket=").append(ticket) + .append(']'); + return sb.toString(); + } +} diff --git a/source/java/org/alfresco/rest/framework/core/ResourceInspector.java b/source/java/org/alfresco/rest/framework/core/ResourceInspector.java index 7b54a1e017..6e5e5ca852 100644 --- a/source/java/org/alfresco/rest/framework/core/ResourceInspector.java +++ b/source/java/org/alfresco/rest/framework/core/ResourceInspector.java @@ -300,7 +300,7 @@ public class ResourceInspector if (isNoAuth(aMethod)) { - if (! httpMethod.equals(HttpMethod.GET)) + if (! (httpMethod.equals(HttpMethod.GET) || httpMethod.equals(HttpMethod.POST))) { throw new IllegalArgumentException("@WebApiNoAuth should only be on GET methods: "+operation.getTitle()); } diff --git a/source/test-java/org/alfresco/rest/api/tests/AbstractBaseApiTest.java b/source/test-java/org/alfresco/rest/api/tests/AbstractBaseApiTest.java index dee9908885..9eedaeb34a 100644 --- a/source/test-java/org/alfresco/rest/api/tests/AbstractBaseApiTest.java +++ b/source/test-java/org/alfresco/rest/api/tests/AbstractBaseApiTest.java @@ -154,7 +154,7 @@ public abstract class AbstractBaseApiTest extends EnterpriseTestApi protected HttpResponse getAll(String url, String runAsUser, PublicApiClient.Paging paging, Map otherParams, int expectedStatus) throws Exception { publicApiClient.setRequestContext(new RequestContext(runAsUser)); - Map params = (paging == null) ? null : createParams(paging, otherParams); + Map params = createParams(paging, otherParams); HttpResponse response = publicApiClient.get(getScope(), url, null, null, null, params); checkStatus(expectedStatus, response.getStatusCode()); @@ -172,6 +172,22 @@ public abstract class AbstractBaseApiTest extends EnterpriseTestApi return response; } + protected HttpResponse getAll(String url, String runAsUser, PublicApiClient.Paging paging, Map otherParams, Map headers, int expectedStatus) throws Exception + { + Map params = createParams(paging, otherParams); + RequestBuilder requestBuilder = httpClient.new GetRequestBuilder() + .setRequestContext(new RequestContext(runAsUser)) + .setScope(getScope()) + .setEntityCollectionName(url) + .setParams(params) + .setHeaders(headers); + + HttpResponse response = publicApiClient.execute(requestBuilder); + checkStatus(expectedStatus, response.getStatusCode()); + + return response; + } + protected HttpResponse getSingle(String url, String runAsUser, String entityId, int expectedStatus) throws Exception { return getSingle(url, runAsUser, entityId, null, expectedStatus); @@ -281,9 +297,30 @@ public abstract class AbstractBaseApiTest extends EnterpriseTestApi return response; } + protected HttpResponse delete(String url, String runAsUser, String entityId, Map params, Map headers, int expectedStatus) throws Exception + { + RequestBuilder requestBuilder = httpClient.new DeleteRequestBuilder() + .setRequestContext(new RequestContext(runAsUser)) + .setScope(getScope()) + .setEntityCollectionName(url) + .setEntityId(entityId) + .setParams(params) + .setHeaders(headers); + + HttpResponse response = publicApiClient.execute(requestBuilder); + checkStatus(expectedStatus, response.getStatusCode()); + + return response; + } + protected String createUser(String username) { - PersonInfo personInfo = new PersonInfo(username, username, username, "password", null, null, null, null, null, null, null); + return createUser(username, "password"); + } + + protected String createUser(String username, String password) + { + PersonInfo personInfo = new PersonInfo(username, username, username, password, null, null, null, null, null, null, null); RepoService.TestPerson person = repoService.createUser(personInfo, username, null); return person.getId(); } diff --git a/source/test-java/org/alfresco/rest/api/tests/ApiTest.java b/source/test-java/org/alfresco/rest/api/tests/ApiTest.java index 40bfc86113..14643e9ddb 100644 --- a/source/test-java/org/alfresco/rest/api/tests/ApiTest.java +++ b/source/test-java/org/alfresco/rest/api/tests/ApiTest.java @@ -19,6 +19,7 @@ import org.junit.runners.Suite; SharedLinkApiTest.class, ActivitiesPostingTest.class, DeletedNodesTest.class, + AuthenticationsTest.class, TestSites.class, TestNodeComments.class, TestCMIS.class, diff --git a/source/test-java/org/alfresco/rest/api/tests/AuthenticationsTest.java b/source/test-java/org/alfresco/rest/api/tests/AuthenticationsTest.java new file mode 100644 index 0000000000..9a1d78480a --- /dev/null +++ b/source/test-java/org/alfresco/rest/api/tests/AuthenticationsTest.java @@ -0,0 +1,277 @@ +/* + * Copyright (C) 2005-2016 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ + +package org.alfresco.rest.api.tests; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.rest.api.Nodes; +import org.alfresco.rest.api.model.LoginTicket; +import org.alfresco.rest.api.model.LoginTicketResponse; +import org.alfresco.rest.api.sites.SiteEntityResource; +import org.alfresco.rest.api.tests.client.HttpResponse; +import org.alfresco.rest.api.tests.client.PublicApiClient.Paging; +import org.alfresco.rest.api.tests.client.data.Document; +import org.alfresco.rest.api.tests.client.data.Folder; +import org.alfresco.rest.api.tests.util.RestApiUtil; +import org.alfresco.service.cmr.security.MutableAuthenticationService; +import org.alfresco.service.cmr.security.PersonService; +import org.apache.commons.codec.binary.Base64; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * @author Jamal Kaabi-Mofrad + */ +public class AuthenticationsTest extends AbstractBaseApiTest +{ + private String user1; + private String user2; + private List users = new ArrayList<>(); + protected MutableAuthenticationService authenticationService; + protected PersonService personService; + + @Before + public void setup() throws Exception + { + authenticationService = applicationContext.getBean("authenticationService", MutableAuthenticationService.class); + personService = applicationContext.getBean("personService", PersonService.class); + + user1 = createUser("user1" + System.currentTimeMillis(), "user1Password"); + user2 = createUser("user2" + System.currentTimeMillis(), "user2Password"); + + users.add(user1); + users.add(user2); + AuthenticationUtil.clearCurrentSecurityContext(); + } + + @After + public void tearDown() throws Exception + { + AuthenticationUtil.setAdminUserAsFullyAuthenticatedUser(); + for (final String user : users) + { + transactionHelper.doInTransaction(new RetryingTransactionCallback() + { + @Override + public Void execute() throws Throwable + { + if (personService.personExists(user)) + { + authenticationService.deleteAuthentication(user); + personService.deletePerson(user); + } + return null; + } + }); + } + users.clear(); + AuthenticationUtil.clearCurrentSecurityContext(); + } + + /** + * Tests login (create ticket), logout (delete ticket), and validate (get ticket). + * + *

POST:

+ * {@literal :/alfresco/api//public/alfresco/versions/1/tickets} + * + *

GET:

+ * {@literal :/alfresco/api//public/alfresco/versions/1/tickets/} + * + *

DELETE:

+ * {@literal :/alfresco/api//public/alfresco/versions/1/tickets/} + */ + @Test + public void testCreateValidateDeleteTicket() throws Exception + { + Paging paging = getPaging(0, 100); + // Unauthorized call + getAll(SiteEntityResource.class, null, paging, null, 401); + + /* + * user1 login + */ + + // User1 login request + LoginTicket loginRequest = new LoginTicket(); + // Invalid login details + post("tickets", null, RestApiUtil.toJsonAsString(loginRequest), 400); + + loginRequest.setUsername(null); + loginRequest.setPassword("user1Password"); + // Invalid login details + post("tickets", null, RestApiUtil.toJsonAsString(loginRequest), 400); + + loginRequest.setUsername(user1); + loginRequest.setPassword(null); + // Invalid login details + post("tickets", null, RestApiUtil.toJsonAsString(loginRequest), 400); + + loginRequest.setUsername(user1); + loginRequest.setPassword("user1Password"); + // Authenticate and create a ticket + HttpResponse response = post("tickets", null, RestApiUtil.toJsonAsString(loginRequest), 201); + LoginTicketResponse loginResponse = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), LoginTicketResponse.class); + assertNotNull(loginResponse.getTicket()); + assertNotNull(loginResponse.getUsername()); + + // Get list of sites by appending the alf_ticket to the URL + // e.g. .../alfresco/versions/1/sites/?alf_ticket=TICKET_57866258ea56c28491bb3e75d8355ebf6fbaaa23 + Map ticket = Collections.singletonMap("alf_ticket", loginResponse.getTicket()); + getAll(SiteEntityResource.class, null, paging, ticket, 200); + + // Validate ticket + response = getSingle("tickets", null, loginResponse.getTicket(), ticket, 200); + LoginTicketResponse validatedTicket = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), LoginTicketResponse.class); + assertEquals(loginResponse.getTicket(), validatedTicket.getTicket()); + + // Validate ticket - non-existent ticket + getSingle("tickets", null, "TICKET_" + System.currentTimeMillis(), ticket, 404); + + // Delete the ticket - Logout + delete("tickets", null, loginResponse.getTicket(), ticket, 204); + + // Validate ticket - 401 as ticket has been invalidated so the API call is unauthorized + getSingle("tickets", null, loginResponse.getTicket(), ticket, 401); + // Check the ticket has been invalidated - the difference with the above is that the API call is authorized + getSingle("tickets", user1, loginResponse.getTicket(), ticket, 404); + + // Ticket has already been invalidated + delete("tickets", user1, loginResponse.getTicket(), ticket, 404); + + // Get list of site by appending the invalidated ticket + getAll(SiteEntityResource.class, null, paging, ticket, 401); + + + /* + * user2 login + */ + + // User2 create a folder within his home folder (-my-) + Folder folderResp = createFolder(user2, Nodes.PATH_MY, "F2", null); + assertNotNull(folderResp.getId()); + + getAll(getNodeChildrenUrl(Nodes.PATH_MY), null, paging, 401); + + // User2 login request + loginRequest = new LoginTicket(); + loginRequest.setUsername(user2); + loginRequest.setPassword("wrongPassword"); + // Authentication failed - wrong password + post("tickets", null, RestApiUtil.toJsonAsString(loginRequest), 403); + + loginRequest.setUsername(user1); + loginRequest.setPassword("user2Password"); + // Authentication failed - username/password mismatch + post("tickets", null, RestApiUtil.toJsonAsString(loginRequest), 403); + + // Set the correct details + loginRequest.setUsername(user2); + loginRequest.setPassword("user2Password"); + // Authenticate and create a ticket + response = post("tickets", null, RestApiUtil.toJsonAsString(loginRequest), 201); + loginResponse = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), LoginTicketResponse.class); + assertNotNull(loginResponse.getTicket()); + assertNotNull(loginResponse.getUsername()); + + String encodedTicket = Base64.encodeBase64String(loginResponse.getTicket().getBytes()); + // Set the authorization (encoded ticket only) header rather than appending the ticket to the URL + Map header = Collections.singletonMap("Authorization", "Basic " + encodedTicket); + // Get children of user2 home folder + response = getAll(getNodeChildrenUrl(Nodes.PATH_MY), null, paging, null, header, 200); + List nodes = RestApiUtil.parseRestApiEntries(response.getJsonResponse(), Document.class); + assertEquals(1, nodes.size()); + + // Validate ticket - user2 + response = getSingle("tickets", null, loginResponse.getTicket(), null, header, 200); + validatedTicket = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), LoginTicketResponse.class); + assertEquals(loginResponse.getTicket(), validatedTicket.getTicket()); + + // Try list children for user2 again. + // Encode Alfresco predefined username for ticket authentication, ROLE_TICKET, and the ticket + String encodedUsernameAndTicket = Base64.encodeBase64String(("ROLE_TICKET:" + loginResponse.getTicket()).getBytes()); + // Set the authorization (encoded username:ticket) header rather than appending the ticket to the URL + header = Collections.singletonMap("Authorization", "Basic " + encodedUsernameAndTicket); + // Get children of user2 home folder + response = getAll(getNodeChildrenUrl(Nodes.PATH_MY), null, paging, null, header, 200); + nodes = RestApiUtil.parseRestApiEntries(response.getJsonResponse(), Document.class); + assertEquals(1, nodes.size()); + + // Try list children for user2 again - appending ticket + ticket = Collections.singletonMap("alf_ticket", loginResponse.getTicket()); + response = getAll(getNodeChildrenUrl(Nodes.PATH_MY), null, paging, ticket, 200); + nodes = RestApiUtil.parseRestApiEntries(response.getJsonResponse(), Document.class); + assertEquals(1, nodes.size()); + + // Delete the ticket - Logout + header = Collections.singletonMap("Authorization", "Basic " + encodedUsernameAndTicket); + delete("tickets", null, loginResponse.getTicket(), null, header, 204); + + // Get children of user2 home folder - invalidated ticket + getAll(getNodeChildrenUrl(Nodes.PATH_MY), null, paging, null, header, 401); + + /* + * user1 and user2 login + */ + loginRequest = new LoginTicket(); + loginRequest.setUsername(user1); + loginRequest.setPassword("user1Password"); + // Authenticate and create a ticket + response = post("tickets", null, RestApiUtil.toJsonAsString(loginRequest), 201); + LoginTicketResponse user1_loginResponse = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), LoginTicketResponse.class); + Map user1_ticket = Collections.singletonMap("alf_ticket", user1_loginResponse.getTicket()); + + loginRequest = new LoginTicket(); + loginRequest.setUsername(user2); + loginRequest.setPassword("user2Password"); + // Authenticate and create a ticket + response = post("tickets", null, RestApiUtil.toJsonAsString(loginRequest), 201); + LoginTicketResponse user2_loginResponse = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), LoginTicketResponse.class); + Map user2_ticket = Collections.singletonMap("alf_ticket", user2_loginResponse.getTicket()); + + // Validate ticket - user1 tries to validate user2's ticket + getSingle("tickets", null, user2_loginResponse.getTicket(), user1_ticket, 404); + + // Check that user2 ticket is still valid + getSingle("tickets", null, user2_loginResponse.getTicket(), user2_ticket, 200); + + // User1 tries to delete user2's ticket + delete("tickets", null, user2_loginResponse.getTicket(), user1_ticket, 404); + + // User1 logs out + delete("tickets", null, user1_loginResponse.getTicket(), user1_ticket, 204); + + // User2 logs out + delete("tickets", null, user2_loginResponse.getTicket(), user2_ticket, 204); + } + + @Override + public String getScope() + { + return "public"; + } +}