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 807b01248d..4c27a2a133 100644
--- a/source/java/org/alfresco/rest/api/PublicApiDeclarativeRegistry.java
+++ b/source/java/org/alfresco/rest/api/PublicApiDeclarativeRegistry.java
@@ -36,15 +36,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;
@@ -109,19 +103,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");
@@ -162,6 +157,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 408b1954fa..00cd46663c 100644
--- a/source/java/org/alfresco/rest/framework/core/ResourceInspector.java
+++ b/source/java/org/alfresco/rest/framework/core/ResourceInspector.java
@@ -308,7 +308,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 28a8fc4ea7..2df88876ee 100644
--- a/source/test-java/org/alfresco/rest/api/tests/AbstractBaseApiTest.java
+++ b/source/test-java/org/alfresco/rest/api/tests/AbstractBaseApiTest.java
@@ -161,7 +161,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());
@@ -179,6 +179,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);
@@ -288,9 +304,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 3f444f99f2..593a89a782 100644
--- a/source/test-java/org/alfresco/rest/api/tests/ApiTest.java
+++ b/source/test-java/org/alfresco/rest/api/tests/ApiTest.java
@@ -44,6 +44,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";
+ }
+}