[SYNCOPE-1035] Using JWT as authentication mean, obtained via initial call
Project: http://git-wip-us.apache.org/repos/asf/syncope/repo Commit: http://git-wip-us.apache.org/repos/asf/syncope/commit/521f51a9 Tree: http://git-wip-us.apache.org/repos/asf/syncope/tree/521f51a9 Diff: http://git-wip-us.apache.org/repos/asf/syncope/diff/521f51a9 Branch: refs/heads/2_0_X Commit: 521f51a9dd2face373ed7437837a8de82a609675 Parents: 077edf8 Author: Francesco Chicchiriccò <ilgro...@apache.org> Authored: Tue Feb 28 17:58:39 2017 +0100 Committer: Francesco Chicchiriccò <ilgro...@apache.org> Committed: Thu Mar 2 11:52:44 2017 +0100 ---------------------------------------------------------------------- .../syncope/client/cli/SyncopeServices.java | 2 +- .../client/console/SyncopeConsoleSession.java | 45 ++-- .../client/console/rest/BaseRestClient.java | 11 +- .../console/rest/ConnectorRestClient.java | 4 +- .../client/console/rest/ResourceRestClient.java | 4 +- .../client/enduser/SyncopeEnduserSession.java | 48 ++-- .../lib/AnonymousAuthenticationHandler.java | 30 +++ .../client/lib/AuthenticationHandler.java | 27 +++ .../client/lib/BasicAuthenticationHandler.java | 43 ++++ .../client/lib/JWTAuthenticationHandler.java | 36 +++ .../client/lib/NoAuthenticationHandler.java | 26 ++ .../client/lib/RestClientFactoryBean.java | 91 ------- .../syncope/client/lib/SyncopeClient.java | 192 ++++++++++----- .../client/lib/SyncopeClientFactoryBean.java | 45 +++- .../syncope/client/lib/ConcurrencyTest.java | 13 +- .../syncope/common/lib/SyncopeConstants.java | 2 + .../syncope/common/lib/to/AccessTokenTO.java | 88 +++++++ .../common/lib/types/StandardEntitlement.java | 4 + .../syncope/common/rest/api/RESTHeaders.java | 2 + .../common/rest/api/beans/AccessTokenQuery.java | 33 +++ .../rest/api/service/AccessTokenService.java | 82 +++++++ core/logic/pom.xml | 10 + .../syncope/core/logic/AccessTokenLogic.java | 187 +++++++++++++++ .../apache/syncope/core/logic/ReportLogic.java | 3 +- .../persistence/api/dao/AccessTokenDAO.java | 42 ++++ .../persistence/api/entity/AccessToken.java | 36 +++ .../persistence/jpa/dao/JPAAccessTokenDAO.java | 143 +++++++++++ .../core/persistence/jpa/dao/JPAUserDAO.java | 10 + .../persistence/jpa/entity/JPAAccessToken.java | 83 +++++++ .../jpa/entity/JPAEntityFactory.java | 3 + .../main/resources/domains/MasterContent.xml | 13 + .../persistence/jpa/inner/MultitenancyTest.java | 2 +- .../persistence/jpa/inner/PlainSchemaTest.java | 2 +- .../core/persistence/jpa/inner/TaskTest.java | 2 +- .../persistence/jpa/outer/AccessTokenTest.java | 64 +++++ .../test/resources/domains/MasterContent.xml | 12 + .../src/test/resources/domains/TwoContent.xml | 18 ++ .../api/data/AccessTokenDataBinder.java | 33 +++ .../api/serialization/POJOHelper.java | 13 + .../java/data/AccessTokenDataBinderImpl.java | 70 ++++++ .../java/data/UserDataBinderImpl.java | 11 + .../java/job/ExpiredAccessTokenCleanup.java | 42 ++++ .../java/job/IdentityRecertification.java | 21 +- .../cxf/service/AccessTokenServiceImpl.java | 73 ++++++ core/spring/pom.xml | 5 + .../core/spring/security/AuthDataAccessor.java | 106 +++++++-- .../core/spring/security/JWTAuthentication.java | 88 +++++++ .../security/JWTAuthenticationFilter.java | 108 +++++++++ .../security/JWTAuthenticationProvider.java | 91 +++++++ .../security/SyncopeAuthenticationDetails.java | 34 +-- .../security/SyncopeAuthenticationProvider.java | 235 ------------------- .../SyncopeDigestAuthenticationEntryPoint.java | 43 ++++ .../security/SyncopeGrantedAuthority.java | 8 +- .../UsernamePasswordAuthenticationProvider.java | 210 +++++++++++++++++ .../src/main/resources/security.properties | 4 + .../src/main/resources/securityContext.xml | 90 ++++--- .../org/apache/syncope/fit/AbstractITCase.java | 6 +- .../syncope/fit/core/AuthenticationITCase.java | 70 ++++-- .../apache/syncope/fit/core/GroupITCase.java | 5 +- .../syncope/fit/core/MultitenancyITCase.java | 2 +- .../org/apache/syncope/fit/core/RESTITCase.java | 12 +- .../apache/syncope/fit/core/ResourceITCase.java | 4 +- .../org/apache/syncope/fit/core/UserITCase.java | 9 +- .../syncope/fit/core/VirSchemaITCase.java | 5 +- pom.xml | 21 +- .../concepts/typemanagement.adoc | 2 +- 66 files changed, 2296 insertions(+), 583 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/syncope/blob/521f51a9/client/cli/src/main/java/org/apache/syncope/client/cli/SyncopeServices.java ---------------------------------------------------------------------- diff --git a/client/cli/src/main/java/org/apache/syncope/client/cli/SyncopeServices.java b/client/cli/src/main/java/org/apache/syncope/client/cli/SyncopeServices.java index d534930..beb5feb 100644 --- a/client/cli/src/main/java/org/apache/syncope/client/cli/SyncopeServices.java +++ b/client/cli/src/main/java/org/apache/syncope/client/cli/SyncopeServices.java @@ -53,7 +53,7 @@ public final class SyncopeServices { setUseCompression(BooleanUtils.toBoolean(useGZIPCompression)). create(properties.getProperty("syncope.admin.user"), syncopeAdminPassword); - LOG.debug("Creting service for {}", clazz.getName()); + LOG.debug("Creating service for {}", clazz.getName()); return syncopeClient.getService(clazz); } http://git-wip-us.apache.org/repos/asf/syncope/blob/521f51a9/client/console/src/main/java/org/apache/syncope/client/console/SyncopeConsoleSession.java ---------------------------------------------------------------------- diff --git a/client/console/src/main/java/org/apache/syncope/client/console/SyncopeConsoleSession.java b/client/console/src/main/java/org/apache/syncope/client/console/SyncopeConsoleSession.java index 658e52b..1ce3b71 100644 --- a/client/console/src/main/java/org/apache/syncope/client/console/SyncopeConsoleSession.java +++ b/client/console/src/main/java/org/apache/syncope/client/console/SyncopeConsoleSession.java @@ -37,8 +37,8 @@ import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.time.FastDateFormat; import org.apache.commons.lang3.tuple.Pair; import org.apache.cxf.jaxrs.client.WebClient; +import org.apache.syncope.client.lib.AnonymousAuthenticationHandler; import org.apache.syncope.client.lib.SyncopeClient; -import org.apache.syncope.client.lib.SyncopeClientFactoryBean; import org.apache.syncope.common.lib.SyncopeConstants; import org.apache.syncope.common.lib.EntityTOUtils; import org.apache.syncope.common.lib.to.DomainTO; @@ -87,10 +87,6 @@ public class SyncopeConsoleSession extends AuthenticatedWebSession { private SyncopeClient client; - private String username; - - private String password; - private UserTO selfTO; private Map<String, Set<String>> auth; @@ -106,9 +102,10 @@ public class SyncopeConsoleSession extends AuthenticatedWebSession { public SyncopeConsoleSession(final Request request) { super(request); - SyncopeClient anonymousClient = SyncopeConsoleApplication.get().getClientFactory().create( - SyncopeConsoleApplication.get().getAnonymousUser(), - SyncopeConsoleApplication.get().getAnonymousKey()); + SyncopeClient anonymousClient = SyncopeConsoleApplication.get().getClientFactory(). + create(new AnonymousAuthenticationHandler( + SyncopeConsoleApplication.get().getAnonymousUser(), + SyncopeConsoleApplication.get().getAnonymousKey())); platformInfo = anonymousClient.getService(SyncopeService.class).platform(); systemInfo = anonymousClient.getService(SyncopeService.class).system(); @@ -134,8 +131,16 @@ public class SyncopeConsoleSession extends AuthenticatedWebSession { @Override public void invalidate() { + client.logout(); + executorService.shutdown(); super.invalidate(); + } + + @Override + public void invalidateNow() { + client.logout(); executorService.shutdownNow(); + super.invalidateNow(); } public PlatformInfo getPlatformInfo() { @@ -158,6 +163,10 @@ public class SyncopeConsoleSession extends AuthenticatedWebSession { return StringUtils.isBlank(domain) ? SyncopeConstants.MASTER_DOMAIN : domain; } + public String getJWT() { + return client.getJWT(); + } + @Override public boolean authenticate(final String username, final String password) { boolean authenticated = false; @@ -170,8 +179,6 @@ public class SyncopeConsoleSession extends AuthenticatedWebSession { auth = self.getKey(); selfTO = self.getValue(); - this.username = username; - this.password = password; authenticated = true; } catch (Exception e) { LOG.error("Authentication failed", e); @@ -199,7 +206,7 @@ public class SyncopeConsoleSession extends AuthenticatedWebSession { } public void refreshAuth() { - authenticate(username, password); + client.refresh(); roles = null; } @@ -213,8 +220,7 @@ public class SyncopeConsoleSession extends AuthenticatedWebSession { services.put(serviceClass, service); } - WebClient.client(service). - type(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON); + WebClient.client(service).type(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON); return service; } @@ -231,18 +237,9 @@ public class SyncopeConsoleSession extends AuthenticatedWebSession { } public <T> T getService(final MediaType mediaType, final Class<T> serviceClass) { - T service; + T service = client.getService(serviceClass); - synchronized (SyncopeConsoleApplication.get().getClientFactory()) { - SyncopeClientFactoryBean.ContentType preType = SyncopeConsoleApplication.get().getClientFactory(). - getContentType(); - - SyncopeConsoleApplication.get().getClientFactory(). - setContentType(SyncopeClientFactoryBean.ContentType.fromString(mediaType.toString())); - service = SyncopeConsoleApplication.get().getClientFactory(). - create(username, password).getService(serviceClass); - SyncopeConsoleApplication.get().getClientFactory().setContentType(preType); - } + WebClient.client(service).type(mediaType).accept(mediaType); return service; } http://git-wip-us.apache.org/repos/asf/syncope/blob/521f51a9/client/console/src/main/java/org/apache/syncope/client/console/rest/BaseRestClient.java ---------------------------------------------------------------------- diff --git a/client/console/src/main/java/org/apache/syncope/client/console/rest/BaseRestClient.java b/client/console/src/main/java/org/apache/syncope/client/console/rest/BaseRestClient.java index 9eaa65d..d2c73f1 100644 --- a/client/console/src/main/java/org/apache/syncope/client/console/rest/BaseRestClient.java +++ b/client/console/src/main/java/org/apache/syncope/client/console/rest/BaseRestClient.java @@ -24,6 +24,7 @@ import org.apache.syncope.client.console.SyncopeConsoleApplication; import org.apache.syncope.client.console.SyncopeConsoleSession; import org.apache.syncope.client.lib.SyncopeClient; import org.apache.syncope.common.lib.search.OrderByClauseBuilder; +import org.apache.syncope.common.rest.api.RESTHeaders; import org.apache.syncope.common.rest.api.service.JAXRSService; import org.apache.syncope.common.rest.api.service.SyncopeService; import org.apache.wicket.extensions.markup.html.repeater.util.SortParam; @@ -69,10 +70,14 @@ public abstract class BaseRestClient implements RestClient { return builder.build(); } - protected static <E extends JAXRSService, T> T getObject(final E service, final URI location, - final Class<T> resultClass) { + protected static <E extends JAXRSService, T> T getObject( + final E service, final URI location, final Class<T> resultClass) { + WebClient webClient = WebClient.fromClient(WebClient.client(service)); webClient.accept(SyncopeConsoleApplication.get().getMediaType()).to(location.toASCIIString(), false); - return webClient.get(resultClass); + return webClient. + header(RESTHeaders.DOMAIN, SyncopeConsoleSession.get().getDomain()). + header(RESTHeaders.TOKEN, SyncopeConsoleSession.get().getJWT()). + get(resultClass); } } http://git-wip-us.apache.org/repos/asf/syncope/blob/521f51a9/client/console/src/main/java/org/apache/syncope/client/console/rest/ConnectorRestClient.java ---------------------------------------------------------------------- diff --git a/client/console/src/main/java/org/apache/syncope/client/console/rest/ConnectorRestClient.java b/client/console/src/main/java/org/apache/syncope/client/console/rest/ConnectorRestClient.java index e506a77..7ac3fcc 100644 --- a/client/console/src/main/java/org/apache/syncope/client/console/rest/ConnectorRestClient.java +++ b/client/console/src/main/java/org/apache/syncope/client/console/rest/ConnectorRestClient.java @@ -58,8 +58,8 @@ public class ConnectorRestClient extends BaseRestClient { connectorTO.getConf().clear(); connectorTO.getConf().addAll(filteredConf); - final ConnectorService service = getService(ConnectorService.class); - final Response response = service.create(connectorTO); + ConnectorService service = getService(ConnectorService.class); + Response response = service.create(connectorTO); return getObject(service, response.getLocation(), ConnInstanceTO.class); } http://git-wip-us.apache.org/repos/asf/syncope/blob/521f51a9/client/console/src/main/java/org/apache/syncope/client/console/rest/ResourceRestClient.java ---------------------------------------------------------------------- diff --git a/client/console/src/main/java/org/apache/syncope/client/console/rest/ResourceRestClient.java b/client/console/src/main/java/org/apache/syncope/client/console/rest/ResourceRestClient.java index 46e7d3e..7ba375d 100644 --- a/client/console/src/main/java/org/apache/syncope/client/console/rest/ResourceRestClient.java +++ b/client/console/src/main/java/org/apache/syncope/client/console/rest/ResourceRestClient.java @@ -104,8 +104,8 @@ public class ResourceRestClient extends BaseRestClient { } public ResourceTO create(final ResourceTO resourceTO) { - final ResourceService service = getService(ResourceService.class); - final Response response = service.create(resourceTO); + ResourceService service = getService(ResourceService.class); + Response response = service.create(resourceTO); return getObject(service, response.getLocation(), ResourceTO.class); } http://git-wip-us.apache.org/repos/asf/syncope/blob/521f51a9/client/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeEnduserSession.java ---------------------------------------------------------------------- diff --git a/client/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeEnduserSession.java b/client/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeEnduserSession.java index cd2fe24..7c1af3b 100644 --- a/client/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeEnduserSession.java +++ b/client/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeEnduserSession.java @@ -18,9 +18,7 @@ */ package org.apache.syncope.client.enduser; -import java.text.DateFormat; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.Set; import javax.ws.rs.core.EntityTag; @@ -29,6 +27,7 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.Predicate; import org.apache.commons.lang3.tuple.Pair; import org.apache.cxf.jaxrs.client.WebClient; +import org.apache.syncope.client.lib.AnonymousAuthenticationHandler; import org.apache.syncope.client.lib.SyncopeClient; import org.apache.syncope.common.lib.info.PlatformInfo; import org.apache.syncope.common.lib.to.PlainSchemaTO; @@ -58,10 +57,6 @@ public class SyncopeEnduserSession extends WebSession { private SyncopeClient client; - private String username; - - private String password; - private final PlatformInfo platformInfo; private final List<PlainSchemaTO> datePlainSchemas; @@ -81,9 +76,10 @@ public class SyncopeEnduserSession extends WebSession { // define cookie utility to manage application cookies cookieUtils = new CookieUtils(); - anonymousClient = SyncopeEnduserApplication.get().getClientFactory().create( - SyncopeEnduserApplication.get().getAnonymousUser(), - SyncopeEnduserApplication.get().getAnonymousKey()); + anonymousClient = SyncopeEnduserApplication.get().getClientFactory(). + create(new AnonymousAuthenticationHandler( + SyncopeEnduserApplication.get().getAnonymousUser(), + SyncopeEnduserApplication.get().getAnonymousKey())); platformInfo = anonymousClient.getService(SyncopeService.class).platform(); datePlainSchemas = anonymousClient.getService(SchemaService.class). @@ -108,10 +104,8 @@ public class SyncopeEnduserSession extends WebSession { Pair<Map<String, Set<String>>, UserTO> self = client.self(); selfTO = self.getValue(); - this.username = username; - this.password = password; - // bind explicitly this session to have a stateful behavior during http requests, unless session will expire - // for every request + // bind explicitly this session to have a stateful behavior during http requests, unless session will + // expire for every request this.bind(); authenticated = true; } catch (Exception e) { @@ -135,14 +129,6 @@ public class SyncopeEnduserSession extends WebSession { return serviceInstance; } - public String getUsername() { - return username; - } - - public String getPassword() { - return password; - } - public PlatformInfo getPlatformInfo() { return platformInfo; } @@ -156,13 +142,7 @@ public class SyncopeEnduserSession extends WebSession { } public boolean isAuthenticated() { - return getUsername() != null; - } - - public DateFormat getDateFormat() { - final Locale locale = getLocale() == null ? Locale.ENGLISH : getLocale(); - - return DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, locale); + return client.getJWT() != null; } public CookieUtils getCookieUtils() { @@ -177,4 +157,16 @@ public class SyncopeEnduserSession extends WebSession { this.xsrfTokenGenerated = xsrfTokenGenerated; } + @Override + public void invalidate() { + client.logout(); + super.invalidate(); + } + + @Override + public void invalidateNow() { + client.logout(); + super.invalidateNow(); + } + } http://git-wip-us.apache.org/repos/asf/syncope/blob/521f51a9/client/lib/src/main/java/org/apache/syncope/client/lib/AnonymousAuthenticationHandler.java ---------------------------------------------------------------------- diff --git a/client/lib/src/main/java/org/apache/syncope/client/lib/AnonymousAuthenticationHandler.java b/client/lib/src/main/java/org/apache/syncope/client/lib/AnonymousAuthenticationHandler.java new file mode 100644 index 0000000..b34be66 --- /dev/null +++ b/client/lib/src/main/java/org/apache/syncope/client/lib/AnonymousAuthenticationHandler.java @@ -0,0 +1,30 @@ +/* + * 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.syncope.client.lib; + +/** + * Implementation providing Basic Authentication capability for the special {@code anonymous} user. + */ +public class AnonymousAuthenticationHandler extends BasicAuthenticationHandler implements AuthenticationHandler { + + public AnonymousAuthenticationHandler(final String username, final String password) { + super(username, password); + } + +} http://git-wip-us.apache.org/repos/asf/syncope/blob/521f51a9/client/lib/src/main/java/org/apache/syncope/client/lib/AuthenticationHandler.java ---------------------------------------------------------------------- diff --git a/client/lib/src/main/java/org/apache/syncope/client/lib/AuthenticationHandler.java b/client/lib/src/main/java/org/apache/syncope/client/lib/AuthenticationHandler.java new file mode 100644 index 0000000..d30de3d --- /dev/null +++ b/client/lib/src/main/java/org/apache/syncope/client/lib/AuthenticationHandler.java @@ -0,0 +1,27 @@ +/* + * 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.syncope.client.lib; + +/** + * Marker interface for usage with + * {@link SyncopeClientFactoryBean#create(org.apache.syncope.client.lib.AuthenticationHandler)}. + */ +public interface AuthenticationHandler { + +} http://git-wip-us.apache.org/repos/asf/syncope/blob/521f51a9/client/lib/src/main/java/org/apache/syncope/client/lib/BasicAuthenticationHandler.java ---------------------------------------------------------------------- diff --git a/client/lib/src/main/java/org/apache/syncope/client/lib/BasicAuthenticationHandler.java b/client/lib/src/main/java/org/apache/syncope/client/lib/BasicAuthenticationHandler.java new file mode 100644 index 0000000..ff1452e --- /dev/null +++ b/client/lib/src/main/java/org/apache/syncope/client/lib/BasicAuthenticationHandler.java @@ -0,0 +1,43 @@ +/* + * 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.syncope.client.lib; + +/** + * Implementation providing Basic Authentication capability. + */ +public class BasicAuthenticationHandler implements AuthenticationHandler { + + private final String username; + + private final String password; + + public BasicAuthenticationHandler(final String username, final String password) { + this.username = username; + this.password = password; + } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } + +} http://git-wip-us.apache.org/repos/asf/syncope/blob/521f51a9/client/lib/src/main/java/org/apache/syncope/client/lib/JWTAuthenticationHandler.java ---------------------------------------------------------------------- diff --git a/client/lib/src/main/java/org/apache/syncope/client/lib/JWTAuthenticationHandler.java b/client/lib/src/main/java/org/apache/syncope/client/lib/JWTAuthenticationHandler.java new file mode 100644 index 0000000..b13ec2e --- /dev/null +++ b/client/lib/src/main/java/org/apache/syncope/client/lib/JWTAuthenticationHandler.java @@ -0,0 +1,36 @@ +/* + * 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.syncope.client.lib; + +/** + * Implementation providing JSON Web Token authentication capability. + */ +public class JWTAuthenticationHandler implements AuthenticationHandler { + + private final String jwt; + + public JWTAuthenticationHandler(final String jwtToken) { + this.jwt = jwtToken; + } + + public String getJwt() { + return jwt; + } + +} http://git-wip-us.apache.org/repos/asf/syncope/blob/521f51a9/client/lib/src/main/java/org/apache/syncope/client/lib/NoAuthenticationHandler.java ---------------------------------------------------------------------- diff --git a/client/lib/src/main/java/org/apache/syncope/client/lib/NoAuthenticationHandler.java b/client/lib/src/main/java/org/apache/syncope/client/lib/NoAuthenticationHandler.java new file mode 100644 index 0000000..6ad2b1f --- /dev/null +++ b/client/lib/src/main/java/org/apache/syncope/client/lib/NoAuthenticationHandler.java @@ -0,0 +1,26 @@ +/* + * 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.syncope.client.lib; + +/** + * Empty implementation not providing any real authentication capability. + */ +public class NoAuthenticationHandler implements AuthenticationHandler { + +} http://git-wip-us.apache.org/repos/asf/syncope/blob/521f51a9/client/lib/src/main/java/org/apache/syncope/client/lib/RestClientFactoryBean.java ---------------------------------------------------------------------- diff --git a/client/lib/src/main/java/org/apache/syncope/client/lib/RestClientFactoryBean.java b/client/lib/src/main/java/org/apache/syncope/client/lib/RestClientFactoryBean.java deleted file mode 100644 index c126deb..0000000 --- a/client/lib/src/main/java/org/apache/syncope/client/lib/RestClientFactoryBean.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * 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.syncope.client.lib; - -import javax.ws.rs.core.MediaType; -import org.apache.commons.lang3.StringUtils; -import org.apache.cxf.jaxrs.client.Client; -import org.apache.cxf.jaxrs.client.ClientConfiguration; -import org.apache.cxf.jaxrs.client.JAXRSClientFactoryBean; -import org.apache.cxf.jaxrs.client.WebClient; -import org.apache.cxf.transport.common.gzip.GZIPInInterceptor; -import org.apache.cxf.transport.common.gzip.GZIPOutInterceptor; -import org.apache.cxf.transport.http.URLConnectionHTTPConduit; - -/** - * Provides shortcuts for creating JAX-RS service instances via CXF's {@link JAXRSClientFactoryBean}. - */ -public class RestClientFactoryBean extends JAXRSClientFactoryBean { - - public static final String HEADER_SPLIT_PROPERTY = "org.apache.cxf.http.header.split"; - - /** - * Creates an anonymous instance of the given service class, for the given content type. - * - * @param <T> any service class - * @param serviceClass service class reference - * @param mediaType XML or JSON are supported - * @return anonymous service instance of the given reference class - */ - public <T> T createServiceInstance(final Class<T> serviceClass, final MediaType mediaType) { - return createServiceInstance(serviceClass, mediaType, null, null, false); - } - - /** - * Creates an authenticated instance of the given service class, for the given content type. - * - * @param <T> any service class - * @param serviceClass service class reference - * @param mediaType XML or JSON are supported - * @param username username for REST authentication - * @param password password for REST authentication - * @param useCompression whether transparent gzip <tt>Content-Encoding</tt> handling is to be enabled - * @return anonymous service instance of the given reference class - */ - public <T> T createServiceInstance( - final Class<T> serviceClass, - final MediaType mediaType, - final String username, - final String password, - final boolean useCompression) { - - if (StringUtils.isNotBlank(username)) { - setUsername(username); - } - if (StringUtils.isNotBlank(password)) { - setPassword(password); - } - - setServiceClass(serviceClass); - T serviceInstance = create(serviceClass); - - Client client = WebClient.client(serviceInstance); - client.type(mediaType).accept(mediaType); - - ClientConfiguration config = WebClient.getConfig(client); - config.getRequestContext().put(HEADER_SPLIT_PROPERTY, true); - config.getRequestContext().put(URLConnectionHTTPConduit.HTTPURL_CONNECTION_METHOD_REFLECTION, true); - if (useCompression) { - config.getInInterceptors().add(new GZIPInInterceptor()); - config.getOutInterceptors().add(new GZIPOutInterceptor()); - } - - return serviceInstance; - } -} http://git-wip-us.apache.org/repos/asf/syncope/blob/521f51a9/client/lib/src/main/java/org/apache/syncope/client/lib/SyncopeClient.java ---------------------------------------------------------------------- diff --git a/client/lib/src/main/java/org/apache/syncope/client/lib/SyncopeClient.java b/client/lib/src/main/java/org/apache/syncope/client/lib/SyncopeClient.java index 7c61b10..a566419 100644 --- a/client/lib/src/main/java/org/apache/syncope/client/lib/SyncopeClient.java +++ b/client/lib/src/main/java/org/apache/syncope/client/lib/SyncopeClient.java @@ -21,7 +21,9 @@ package org.apache.syncope.client.lib; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; +import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Set; import javax.ws.rs.core.EntityTag; @@ -29,7 +31,14 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.commons.lang3.tuple.Pair; +import org.apache.cxf.jaxrs.client.Client; +import org.apache.cxf.jaxrs.client.ClientConfiguration; +import org.apache.cxf.jaxrs.client.JAXRSClientFactoryBean; import org.apache.cxf.jaxrs.client.WebClient; +import org.apache.cxf.transport.common.gzip.GZIPInInterceptor; +import org.apache.cxf.transport.common.gzip.GZIPOutInterceptor; +import org.apache.cxf.transport.http.URLConnectionHTTPConduit; +import org.apache.syncope.common.lib.SyncopeConstants; import org.apache.syncope.common.lib.search.AnyObjectFiqlSearchConditionBuilder; import org.apache.syncope.common.lib.search.OrderByClauseBuilder; import org.apache.syncope.common.lib.search.GroupFiqlSearchConditionBuilder; @@ -37,6 +46,7 @@ import org.apache.syncope.common.lib.search.UserFiqlSearchConditionBuilder; import org.apache.syncope.common.lib.to.UserTO; import org.apache.syncope.common.rest.api.Preference; import org.apache.syncope.common.rest.api.RESTHeaders; +import org.apache.syncope.common.rest.api.service.AccessTokenService; import org.apache.syncope.common.rest.api.service.UserSelfService; /** @@ -45,34 +55,99 @@ import org.apache.syncope.common.rest.api.service.UserSelfService; */ public class SyncopeClient { + private static final String HEADER_SPLIT_PROPERTY = "org.apache.cxf.http.header.split"; + private final MediaType mediaType; - private final RestClientFactoryBean restClientFactory; + private final JAXRSClientFactoryBean restClientFactory; private final RestClientExceptionMapper exceptionMapper; - private final String username; - - private final String password; - private final boolean useCompression; public SyncopeClient( final MediaType mediaType, - final RestClientFactoryBean restClientFactory, + final JAXRSClientFactoryBean restClientFactory, final RestClientExceptionMapper exceptionMapper, - final String username, final String password, + final AuthenticationHandler handler, final boolean useCompression) { this.mediaType = mediaType; this.restClientFactory = restClientFactory; + if (this.restClientFactory.getHeaders() == null) { + this.restClientFactory.setHeaders(new HashMap<String, String>()); + } this.exceptionMapper = exceptionMapper; - this.username = username; - this.password = password; + init(handler); this.useCompression = useCompression; } /** + * Initializes the provided {@code restClientFactory} with the authentication capabilities of the provided + * {@code handler}. + * + * Currently supports: + * <ul> + * <li>{@link JWTAuthenticationHandler}</li> + * <li>{@link AnonymousAuthenticationHandler}</li> + * <li>{@link BasicAuthenticationHandler}</li> + * </ul> + * More can be supported by subclasses. + * + * @param handler authentication handler + */ + protected void init(final AuthenticationHandler handler) { + cleanup(); + + if (handler instanceof AnonymousAuthenticationHandler) { + restClientFactory.setUsername(((AnonymousAuthenticationHandler) handler).getUsername()); + restClientFactory.setPassword(((AnonymousAuthenticationHandler) handler).getPassword()); + } else if (handler instanceof BasicAuthenticationHandler) { + restClientFactory.setUsername(((BasicAuthenticationHandler) handler).getUsername()); + restClientFactory.setPassword(((BasicAuthenticationHandler) handler).getPassword()); + + String jwt = getService(AccessTokenService.class).login().getHeaderString(RESTHeaders.TOKEN); + restClientFactory.getHeaders().put(RESTHeaders.TOKEN, Collections.singletonList(jwt)); + + restClientFactory.setUsername(null); + restClientFactory.setPassword(null); + } else if (handler instanceof JWTAuthenticationHandler) { + restClientFactory.getHeaders().put( + RESTHeaders.TOKEN, Collections.singletonList(((JWTAuthenticationHandler) handler).getJwt())); + } + } + + protected void cleanup() { + restClientFactory.getHeaders().remove(RESTHeaders.TOKEN); + restClientFactory.setUsername(null); + restClientFactory.setPassword(null); + } + + /** + * Attempts to extend the lifespan of the JWT currently in use. + */ + public void refresh() { + getService(AccessTokenService.class).refresh(); + } + + /** + * Invalidates the JWT currently in use. + */ + public void logout() { + getService(AccessTokenService.class).logout(); + cleanup(); + } + + /** + * (Re)initializes the current instance with the authentication capabilities of the provided {@code handler}. + * + * @param handler authentication handler + */ + public void login(final AuthenticationHandler handler) { + init(handler); + } + + /** * Returns a new instance of {@link UserFiqlSearchConditionBuilder}, for assisted building of FIQL queries. * * @return default instance of {@link UserFiqlSearchConditionBuilder} @@ -110,6 +185,31 @@ public class SyncopeClient { } /** + * Returns the JWT in used by this instance, passed with the {@link RESTHeaders#TOKEN} header in all requests. + * It can be null (in case {@link NoAuthenticationHandler} or {@link AnonymousAuthenticationHandler} were used). + * + * @return the JWT in used by this instance + */ + public String getJWT() { + List<String> headerValues = restClientFactory.getHeaders().get(RESTHeaders.TOKEN); + return headerValues == null || headerValues.isEmpty() + ? null + : headerValues.get(0); + } + + /** + * Returns the domain configured for this instance, or {@link SyncopeConstants#MASTER_DOMAIN} if not set. + * + * @return the domain configured for this instance + */ + public String getDomain() { + List<String> headerValues = restClientFactory.getHeaders().get(RESTHeaders.DOMAIN); + return headerValues == null || headerValues.isEmpty() + ? SyncopeConstants.MASTER_DOMAIN + : headerValues.get(0); + } + + /** * Creates an instance of the given service class, with configured content type and authentication. * * @param <T> any service class @@ -118,7 +218,21 @@ public class SyncopeClient { */ public <T> T getService(final Class<T> serviceClass) { synchronized (restClientFactory) { - return restClientFactory.createServiceInstance(serviceClass, mediaType, username, password, useCompression); + restClientFactory.setServiceClass(serviceClass); + T serviceInstance = restClientFactory.create(serviceClass); + + Client client = WebClient.client(serviceInstance); + client.type(mediaType).accept(mediaType); + + ClientConfiguration config = WebClient.getConfig(client); + config.getRequestContext().put(HEADER_SPLIT_PROPERTY, true); + config.getRequestContext().put(URLConnectionHTTPConduit.HTTPURL_CONNECTION_METHOD_REFLECTION, true); + if (useCompression) { + config.getInInterceptors().add(new GZIPInInterceptor()); + config.getOutInterceptors().add(new GZIPOutInterceptor()); + } + + return serviceInstance; } } @@ -126,8 +240,7 @@ public class SyncopeClient { public Pair<Map<String, Set<String>>, UserTO> self() { // Explicitly disable header value split because it interferes with JSON deserialization below UserSelfService service = getService(UserSelfService.class); - WebClient.getConfig(WebClient.client(service)). - getRequestContext().put(RestClientFactoryBean.HEADER_SPLIT_PROPERTY, false); + WebClient.getConfig(WebClient.client(service)).getRequestContext().put(HEADER_SPLIT_PROPERTY, false); Response response = service.read(); if (response.getStatusInfo().getStatusCode() != Response.Status.OK.getStatusCode()) { @@ -164,19 +277,6 @@ public class SyncopeClient { } /** - * Creates an instance of the given service class and sets the given header. - * - * @param <T> any service class - * @param serviceClass service class reference - * @param key HTTP header key - * @param values HTTP header values - * @return service instance of the given reference class, with given header set - */ - public <T> T header(final Class<T> serviceClass, final String key, final Object... values) { - return header(getService(serviceClass), key, values); - } - - /** * Sets the {@code Prefer} header on the give service instance. * * @param <T> any service class @@ -189,28 +289,16 @@ public class SyncopeClient { } /** - * Creates an instance of the given service class, with {@code Prefer} header set. - * - * @param <T> any service class - * @param serviceClass service class reference - * @param preference preference to be set via {@code Prefer} header - * @return service instance of the given reference class, with {@code Prefer} header set - */ - public <T> T prefer(final Class<T> serviceClass, final Preference preference) { - return header(serviceClass, RESTHeaders.PREFER, preference.toString()); - } - - /** * Asks for asynchronous propagation towards external resources with null priority. * * @param <T> any service class - * @param serviceClass service class reference + * @param service service class instance * @param nullPriorityAsync whether asynchronous propagation towards external resources with null priority is * requested * @return service instance of the given reference class, with related header set */ - public <T> T nullPriorityAsync(final Class<T> serviceClass, final boolean nullPriorityAsync) { - return header(serviceClass, RESTHeaders.NULL_PRIORITY_ASYNC, nullPriorityAsync); + public <T> T nullPriorityAsync(final T service, final boolean nullPriorityAsync) { + return header(service, RESTHeaders.NULL_PRIORITY_ASYNC, nullPriorityAsync); } /** @@ -240,18 +328,6 @@ public class SyncopeClient { } /** - * Creates an instance of the given service class, with {@code If-Match} header set. - * - * @param <T> any service class - * @param serviceClass service class reference - * @param etag ETag value - * @return given service instance, with {@code If-Match} set - */ - public <T> T ifMatch(final Class<T> serviceClass, final EntityTag etag) { - return match(getService(serviceClass), etag, false); - } - - /** * Sets the {@code If-None-Match} header on the given service instance. * * @param <T> any service class @@ -264,18 +340,6 @@ public class SyncopeClient { } /** - * Creates an instance of the given service class, with {@code If-None-Match} header set. - * - * @param <T> any service class - * @param serviceClass service class reference - * @param etag ETag value - * @return given service instance, with {@code If-None-Match} set - */ - public <T> T ifNoneMatch(final Class<T> serviceClass, final EntityTag etag) { - return match(getService(serviceClass), etag, true); - } - - /** * Fetches {@code ETag} header value from latest service run (if available). * * @param <T> any service class http://git-wip-us.apache.org/repos/asf/syncope/blob/521f51a9/client/lib/src/main/java/org/apache/syncope/client/lib/SyncopeClientFactoryBean.java ---------------------------------------------------------------------- diff --git a/client/lib/src/main/java/org/apache/syncope/client/lib/SyncopeClientFactoryBean.java b/client/lib/src/main/java/org/apache/syncope/client/lib/SyncopeClientFactoryBean.java index 6ea7b4f..c96f4d0 100644 --- a/client/lib/src/main/java/org/apache/syncope/client/lib/SyncopeClientFactoryBean.java +++ b/client/lib/src/main/java/org/apache/syncope/client/lib/SyncopeClientFactoryBean.java @@ -31,6 +31,7 @@ import javax.xml.bind.Marshaller; import org.apache.commons.lang3.StringUtils; import org.apache.cxf.feature.Feature; import org.apache.cxf.feature.LoggingFeature; +import org.apache.cxf.jaxrs.client.JAXRSClientFactoryBean; import org.apache.cxf.jaxrs.provider.JAXBElementProvider; import org.apache.cxf.staxutils.DocumentDepthProperties; import org.apache.syncope.common.lib.policy.AbstractPolicyTO; @@ -79,7 +80,7 @@ public class SyncopeClientFactoryBean { private boolean useCompression; - private RestClientFactoryBean restClientFactoryBean; + private JAXRSClientFactoryBean restClientFactoryBean; protected JacksonJaxbJsonProvider defaultJsonProvider() { ObjectMapper objectMapper = new ObjectMapper(); @@ -111,8 +112,9 @@ public class SyncopeClientFactoryBean { return new RestClientExceptionMapper(); } - protected RestClientFactoryBean defaultRestClientFactoryBean() { - RestClientFactoryBean defaultRestClientFactoryBean = new RestClientFactoryBean(); + protected JAXRSClientFactoryBean defaultRestClientFactoryBean() { + JAXRSClientFactoryBean defaultRestClientFactoryBean = new JAXRSClientFactoryBean(); + defaultRestClientFactoryBean.setHeaders(new HashMap<String, String>()); if (StringUtils.isBlank(address)) { throw new IllegalArgumentException("Property 'address' is missing"); @@ -120,7 +122,7 @@ public class SyncopeClientFactoryBean { defaultRestClientFactoryBean.setAddress(address); if (StringUtils.isNotBlank(domain)) { - defaultRestClientFactoryBean.setHeaders(Collections.singletonMap(RESTHeaders.DOMAIN, domain)); + defaultRestClientFactoryBean.getHeaders().put(RESTHeaders.DOMAIN, Collections.singletonList(domain)); } defaultRestClientFactoryBean.setThreadSafe(true); @@ -221,41 +223,62 @@ public class SyncopeClientFactoryBean { return useCompression; } - public RestClientFactoryBean getRestClientFactoryBean() { + public JAXRSClientFactoryBean getRestClientFactoryBean() { return restClientFactoryBean == null ? defaultRestClientFactoryBean() : restClientFactoryBean; } - public SyncopeClientFactoryBean setRestClientFactoryBean(final RestClientFactoryBean restClientFactoryBean) { + public SyncopeClientFactoryBean setRestClientFactoryBean(final JAXRSClientFactoryBean restClientFactoryBean) { this.restClientFactoryBean = restClientFactoryBean; return this; } /** - * Builds client instance with no authentication, for user self-registration and related queries (schema, - * resources, ...). + * Builds client instance with no authentication, for user self-registration and password reset. * * @return client instance with no authentication */ public SyncopeClient create() { - return create(null, null); + return create(new NoAuthenticationHandler()); } /** * Builds client instance with the given credentials. + * Such credentials will be used only to obtain a valid JWT in the {@link RESTHeaders#TOKEN} header; * * @param username username * @param password password * @return client instance with the given credentials */ public SyncopeClient create(final String username, final String password) { + return create(new BasicAuthenticationHandler(username, password)); + } + + /** + * Builds client instance which will be passing the provided value in the {@link RESTHeaders#TOKEN} + * request header. + * + * @param jwt value received after login, in the {@link RESTHeaders#TOKEN} response header + * @return client instance which will be passing the provided value in the {{@link RESTHeaders#TOKEN} + * request header + */ + public SyncopeClient create(final String jwt) { + return create(new JWTAuthenticationHandler(jwt)); + } + + /** + * Builds client instance with the given authentication handler. + * + * @param handler authentication handler + * @return client instance with the given authentication handler + */ + public SyncopeClient create(final AuthenticationHandler handler) { return new SyncopeClient( getContentType().getMediaType(), getRestClientFactoryBean(), getExceptionMapper(), - username, - password, + handler, useCompression); } } http://git-wip-us.apache.org/repos/asf/syncope/blob/521f51a9/client/lib/src/test/java/org/apache/syncope/client/lib/ConcurrencyTest.java ---------------------------------------------------------------------- diff --git a/client/lib/src/test/java/org/apache/syncope/client/lib/ConcurrencyTest.java b/client/lib/src/test/java/org/apache/syncope/client/lib/ConcurrencyTest.java index a20cb59..583109f 100644 --- a/client/lib/src/test/java/org/apache/syncope/client/lib/ConcurrencyTest.java +++ b/client/lib/src/test/java/org/apache/syncope/client/lib/ConcurrencyTest.java @@ -32,8 +32,7 @@ public class ConcurrencyTest { private static final int THREAD_NUMBER = 1000; - private static final SyncopeClient client = - new SyncopeClientFactoryBean().setAddress("http://url").create("username", "password"); + private static final SyncopeClient client = new SyncopeClientFactoryBean().setAddress("http://url").create(); @Test public void multiThreadTest() @@ -54,11 +53,11 @@ public class ConcurrencyTest { } } }; - try { - execution.start(); - } catch(OutOfMemoryError e) { - // ignore - } + try { + execution.start(); + } catch (OutOfMemoryError e) { + // ignore + } } Thread.sleep(THREAD_NUMBER); http://git-wip-us.apache.org/repos/asf/syncope/blob/521f51a9/common/lib/src/main/java/org/apache/syncope/common/lib/SyncopeConstants.java ---------------------------------------------------------------------- diff --git a/common/lib/src/main/java/org/apache/syncope/common/lib/SyncopeConstants.java b/common/lib/src/main/java/org/apache/syncope/common/lib/SyncopeConstants.java index 8d80f6d..5ab80d2 100644 --- a/common/lib/src/main/java/org/apache/syncope/common/lib/SyncopeConstants.java +++ b/common/lib/src/main/java/org/apache/syncope/common/lib/SyncopeConstants.java @@ -29,6 +29,8 @@ public final class SyncopeConstants { public static final String NS = "http://syncope.apache.org/2.0"; + public static final String JWT_CLAIM_REMOTE_HOST = "remoteHost"; + public static final String MASTER_DOMAIN = "Master"; public static final String ROOT_REALM = "/"; http://git-wip-us.apache.org/repos/asf/syncope/blob/521f51a9/common/lib/src/main/java/org/apache/syncope/common/lib/to/AccessTokenTO.java ---------------------------------------------------------------------- diff --git a/common/lib/src/main/java/org/apache/syncope/common/lib/to/AccessTokenTO.java b/common/lib/src/main/java/org/apache/syncope/common/lib/to/AccessTokenTO.java new file mode 100644 index 0000000..c172e88 --- /dev/null +++ b/common/lib/src/main/java/org/apache/syncope/common/lib/to/AccessTokenTO.java @@ -0,0 +1,88 @@ +/* + * 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.syncope.common.lib.to; + +import java.util.Date; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlType; +import org.apache.syncope.common.lib.AbstractBaseBean; + +@XmlRootElement(name = "accessToken") +@XmlType +public class AccessTokenTO extends AbstractBaseBean implements EntityTO { + + private static final long serialVersionUID = 6577639976115661357L; + + private String key; + + private String body; + + private Date expiryTime; + + private String owner; + + private String authorities; + + @Override + public String getKey() { + return key; + } + + @Override + public void setKey(final String key) { + this.key = key; + } + + public String getBody() { + return body; + } + + public void setBody(final String body) { + this.body = body; + } + + public Date getExpiryTime() { + return expiryTime == null + ? null + : new Date(expiryTime.getTime()); + } + + public void setExpiryTime(final Date expiryTime) { + this.expiryTime = expiryTime == null + ? null + : new Date(expiryTime.getTime()); + } + + public String getOwner() { + return owner; + } + + public void setOwner(final String owner) { + this.owner = owner; + } + + public String getAuthorities() { + return authorities; + } + + public void setAuthorities(final String authorities) { + this.authorities = authorities; + } + +} http://git-wip-us.apache.org/repos/asf/syncope/blob/521f51a9/common/lib/src/main/java/org/apache/syncope/common/lib/types/StandardEntitlement.java ---------------------------------------------------------------------- diff --git a/common/lib/src/main/java/org/apache/syncope/common/lib/types/StandardEntitlement.java b/common/lib/src/main/java/org/apache/syncope/common/lib/types/StandardEntitlement.java index 74c59b7..8f70ba1 100644 --- a/common/lib/src/main/java/org/apache/syncope/common/lib/types/StandardEntitlement.java +++ b/common/lib/src/main/java/org/apache/syncope/common/lib/types/StandardEntitlement.java @@ -240,6 +240,10 @@ public final class StandardEntitlement { public static final String SECURITY_QUESTION_DELETE = "SECURITY_QUESTION_DELETE"; + public static final String ACCESS_TOKEN_LIST = "TASK_LIST"; + + public static final String ACCESS_TOKEN_DELETE = "TASK_DELETE"; + private static final Set<String> VALUES; static { http://git-wip-us.apache.org/repos/asf/syncope/blob/521f51a9/common/rest-api/src/main/java/org/apache/syncope/common/rest/api/RESTHeaders.java ---------------------------------------------------------------------- diff --git a/common/rest-api/src/main/java/org/apache/syncope/common/rest/api/RESTHeaders.java b/common/rest-api/src/main/java/org/apache/syncope/common/rest/api/RESTHeaders.java index 9312543..0c54116 100644 --- a/common/rest-api/src/main/java/org/apache/syncope/common/rest/api/RESTHeaders.java +++ b/common/rest-api/src/main/java/org/apache/syncope/common/rest/api/RESTHeaders.java @@ -25,6 +25,8 @@ public final class RESTHeaders { public static final String DOMAIN = "X-Syncope-Domain"; + public static final String TOKEN = "X-Syncope-Token"; + public static final String OWNED_ENTITLEMENTS = "X-Syncope-Entitlements"; public static final String RESOURCE_KEY = "X-Syncope-Key"; http://git-wip-us.apache.org/repos/asf/syncope/blob/521f51a9/common/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/AccessTokenQuery.java ---------------------------------------------------------------------- diff --git a/common/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/AccessTokenQuery.java b/common/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/AccessTokenQuery.java new file mode 100644 index 0000000..abdea3f --- /dev/null +++ b/common/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/AccessTokenQuery.java @@ -0,0 +1,33 @@ +/* + * 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.syncope.common.rest.api.beans; + +public class AccessTokenQuery extends AbstractQuery { + + private static final long serialVersionUID = -8792519310029596796L; + + public static class Builder extends AbstractQuery.Builder<AccessTokenQuery, Builder> { + + @Override + protected AccessTokenQuery newInstance() { + return new AccessTokenQuery(); + } + } + +} http://git-wip-us.apache.org/repos/asf/syncope/blob/521f51a9/common/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/AccessTokenService.java ---------------------------------------------------------------------- diff --git a/common/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/AccessTokenService.java b/common/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/AccessTokenService.java new file mode 100644 index 0000000..e9f5ff3 --- /dev/null +++ b/common/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/AccessTokenService.java @@ -0,0 +1,82 @@ +/* + * 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.syncope.common.rest.api.service; + +import javax.ws.rs.BeanParam; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.core.Response; +import org.apache.syncope.common.lib.to.AccessTokenTO; +import org.apache.syncope.common.lib.to.PagedResult; +import org.apache.syncope.common.rest.api.beans.AccessTokenQuery; + +/** + * REST operations for access tokens. + */ +@Path("accessTokens") +public interface AccessTokenService extends JAXRSService { + + /** + * Returns an empty response bearing the X-Syncope-Token header value, in case of successful authentication. + * The provided value is a signed JSON Web Token. + * + * @return empty response bearing the X-Syncope-Token header value, in case of successful authentication + */ + @POST + @Path("login") + Response login(); + + /** + * Returns an empty response bearing the X-Syncope-Token header value, with extended lifetime. + * The provided value is a signed JSON Web Token. + * + * @return an empty response bearing the X-Syncope-Token header value, with extended lifetime + */ + @POST + @Path("refresh") + Response refresh(); + + /** + * Invalidates the access token of the requesting user. + */ + @POST + @Path("logout") + void logout(); + + /** + * Returns a paged list of existing access tokens matching the given query. + * + * @param query query conditions + * @return paged list of existing access tokens matching the given query + */ + @GET + PagedResult<AccessTokenTO> list(@BeanParam AccessTokenQuery query); + + /** + * Invalidates the access token matching the provided key. + * + * @param key access token key + */ + @DELETE + @Path("{key}") + void delete(@PathParam("key") String key); +} http://git-wip-us.apache.org/repos/asf/syncope/blob/521f51a9/core/logic/pom.xml ---------------------------------------------------------------------- diff --git a/core/logic/pom.xml b/core/logic/pom.xml index 6828b5b..7603b58 100644 --- a/core/logic/pom.xml +++ b/core/logic/pom.xml @@ -39,6 +39,16 @@ under the License. <dependencies> <dependency> + <groupId>org.apache.cxf</groupId> + <artifactId>cxf-rt-rs-security-jose</artifactId> + </dependency> + + <dependency> + <groupId>com.fasterxml.uuid</groupId> + <artifactId>java-uuid-generator</artifactId> + </dependency> + + <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-dbcp2</artifactId> </dependency> http://git-wip-us.apache.org/repos/asf/syncope/blob/521f51a9/core/logic/src/main/java/org/apache/syncope/core/logic/AccessTokenLogic.java ---------------------------------------------------------------------- diff --git a/core/logic/src/main/java/org/apache/syncope/core/logic/AccessTokenLogic.java b/core/logic/src/main/java/org/apache/syncope/core/logic/AccessTokenLogic.java new file mode 100644 index 0000000..6ca1b87 --- /dev/null +++ b/core/logic/src/main/java/org/apache/syncope/core/logic/AccessTokenLogic.java @@ -0,0 +1,187 @@ +/* + * 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.syncope.core.logic; + +import com.fasterxml.uuid.Generators; +import com.fasterxml.uuid.impl.RandomBasedGenerator; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import javax.annotation.Resource; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.Transformer; +import org.apache.cxf.rs.security.jose.common.JoseType; +import org.apache.cxf.rs.security.jose.jwa.SignatureAlgorithm; +import org.apache.cxf.rs.security.jose.jws.JwsHeaders; +import org.apache.cxf.rs.security.jose.jws.JwsJwtCompactConsumer; +import org.apache.cxf.rs.security.jose.jws.JwsJwtCompactProducer; +import org.apache.cxf.rs.security.jose.jws.JwsSignatureProvider; +import org.apache.cxf.rs.security.jose.jwt.JwtClaims; +import org.apache.cxf.rs.security.jose.jwt.JwtToken; +import org.apache.syncope.common.lib.SyncopeConstants; +import org.apache.syncope.common.lib.to.AccessTokenTO; +import org.apache.syncope.common.lib.types.StandardEntitlement; +import org.apache.syncope.core.persistence.api.dao.AccessTokenDAO; +import org.apache.syncope.core.persistence.api.dao.ConfDAO; +import org.apache.syncope.core.persistence.api.dao.NotFoundException; +import org.apache.syncope.core.persistence.api.dao.search.OrderByClause; +import org.apache.syncope.core.persistence.api.entity.AccessToken; +import org.apache.syncope.core.provisioning.api.data.AccessTokenDataBinder; +import org.apache.syncope.core.spring.security.AuthContextUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Component; + +@Component +public class AccessTokenLogic extends AbstractTransactionalLogic<AccessTokenTO> { + + private static final RandomBasedGenerator UUID_GENERATOR = Generators.randomBasedGenerator(); + + private static final JwsHeaders JWS_HEADERS = new JwsHeaders(JoseType.JWT, SignatureAlgorithm.HS512); + + @Resource(name = "jwtIssuer") + private String jwtIssuer; + + @Resource(name = "anonymousUser") + private String anonymousUser; + + @Autowired + private JwsSignatureProvider jwsSignatureProvider; + + @Autowired + private AccessTokenDataBinder binder; + + @Autowired + private AccessTokenDAO accessTokenDAO; + + @Autowired + private ConfDAO confDAO; + + @PreAuthorize("isAuthenticated()") + public String login(final String remoteHost) { + if (anonymousUser.equals(AuthContextUtils.getUsername())) { + throw new IllegalArgumentException(anonymousUser + " cannot be granted for an access token"); + } + + String body = null; + + AccessToken accessToken = accessTokenDAO.findByOwner(AuthContextUtils.getUsername()); + if (accessToken != null) { + body = accessToken.getBody(); + } + + if (body == null) { + Date now = new Date(); + Calendar expiry = Calendar.getInstance(); + expiry.setTime(now); + expiry.add(Calendar.MINUTE, + confDAO.find("jwt.lifetime.minutes", "120").getValues().get(0).getLongValue().intValue()); + + JwtClaims claims = new JwtClaims(); + claims.setTokenId(UUID_GENERATOR.generate().toString()); + claims.setSubject(AuthContextUtils.getUsername()); + claims.setIssuedAt(now.getTime()); + claims.setIssuer(jwtIssuer); + claims.setExpiryTime(expiry.getTime().getTime()); + claims.setNotBefore(now.getTime()); + claims.setClaim(SyncopeConstants.JWT_CLAIM_REMOTE_HOST, remoteHost); + + JwtToken token = new JwtToken(JWS_HEADERS, claims); + JwsJwtCompactProducer producer = new JwsJwtCompactProducer(token); + + body = producer.signWith(jwsSignatureProvider); + + binder.create(claims.getTokenId(), body, expiry.getTime()); + } + + return body; + } + + @PreAuthorize("isAuthenticated()") + public String refresh() { + AccessToken accessToken = accessTokenDAO.findByOwner(AuthContextUtils.getUsername()); + if (accessToken == null) { + throw new NotFoundException("AccessToken for " + AuthContextUtils.getUsername()); + } + + JwsJwtCompactConsumer consumer = new JwsJwtCompactConsumer(accessToken.getBody()); + + Date now = new Date(); + Calendar expiry = Calendar.getInstance(); + expiry.setTime(now); + expiry.add(Calendar.MINUTE, + confDAO.find("jwt.lifetime.minutes", "120").getValues().get(0).getLongValue().intValue()); + consumer.getJwtClaims().setExpiryTime(expiry.getTime().getTime()); + + JwtToken token = new JwtToken(JWS_HEADERS, consumer.getJwtClaims()); + JwsJwtCompactProducer producer = new JwsJwtCompactProducer(token); + + String body = producer.signWith(jwsSignatureProvider); + + binder.update(accessToken, body, expiry.getTime()); + + return body; + } + + @PreAuthorize("isAuthenticated()") + public void logout() { + AccessToken accessToken = accessTokenDAO.findByOwner(AuthContextUtils.getUsername()); + if (accessToken == null) { + throw new NotFoundException("AccessToken for " + AuthContextUtils.getUsername()); + } + + delete(accessToken.getKey()); + } + + @PreAuthorize("hasRole('" + StandardEntitlement.ACCESS_TOKEN_LIST + "')") + public int count() { + return accessTokenDAO.count(); + } + + @PreAuthorize("hasRole('" + StandardEntitlement.ACCESS_TOKEN_LIST + "')") + public List<AccessTokenTO> list( + final int page, + final int size, + final List<OrderByClause> orderByClauses) { + + return CollectionUtils.collect(accessTokenDAO.findAll(page, size, orderByClauses), + new Transformer<AccessToken, AccessTokenTO>() { + + @Override + public AccessTokenTO transform(final AccessToken input) { + return binder.getAccessTokenTO(input); + } + }, new ArrayList<AccessTokenTO>()); + } + + @PreAuthorize("hasRole('" + StandardEntitlement.ACCESS_TOKEN_DELETE + "')") + public void delete(final String key) { + accessTokenDAO.delete(key); + } + + @Override + protected AccessTokenTO resolveReference(final Method method, final Object... args) + throws UnresolvedReferenceException { + + throw new UnresolvedReferenceException(); + } + +} http://git-wip-us.apache.org/repos/asf/syncope/blob/521f51a9/core/logic/src/main/java/org/apache/syncope/core/logic/ReportLogic.java ---------------------------------------------------------------------- diff --git a/core/logic/src/main/java/org/apache/syncope/core/logic/ReportLogic.java b/core/logic/src/main/java/org/apache/syncope/core/logic/ReportLogic.java index 033dfa3..af8a29f 100644 --- a/core/logic/src/main/java/org/apache/syncope/core/logic/ReportLogic.java +++ b/core/logic/src/main/java/org/apache/syncope/core/logic/ReportLogic.java @@ -137,8 +137,7 @@ public class ReportLogic extends AbstractExecutableLogic<ReportTO> { @PreAuthorize("hasRole('" + StandardEntitlement.REPORT_LIST + "')") public List<ReportTO> list() { - return CollectionUtils.collect(reportDAO.findAll(), - new Transformer<Report, ReportTO>() { + return CollectionUtils.collect(reportDAO.findAll(), new Transformer<Report, ReportTO>() { @Override public ReportTO transform(final Report input) { http://git-wip-us.apache.org/repos/asf/syncope/blob/521f51a9/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/AccessTokenDAO.java ---------------------------------------------------------------------- diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/AccessTokenDAO.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/AccessTokenDAO.java new file mode 100644 index 0000000..ef07ee6 --- /dev/null +++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/AccessTokenDAO.java @@ -0,0 +1,42 @@ +/* + * 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.syncope.core.persistence.api.dao; + +import java.util.List; +import org.apache.syncope.core.persistence.api.dao.search.OrderByClause; +import org.apache.syncope.core.persistence.api.entity.AccessToken; + +public interface AccessTokenDAO extends DAO<AccessToken> { + + AccessToken find(String key); + + AccessToken findByOwner(String username); + + int count(); + + List<AccessToken> findAll(int page, int itemsPerPage, List<OrderByClause> orderByClauses); + + AccessToken save(AccessToken accessToken); + + void delete(String key); + + void delete(AccessToken accessToken); + + int deleteExpired(); +} http://git-wip-us.apache.org/repos/asf/syncope/blob/521f51a9/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/AccessToken.java ---------------------------------------------------------------------- diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/AccessToken.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/AccessToken.java new file mode 100644 index 0000000..e08e9e3 --- /dev/null +++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/AccessToken.java @@ -0,0 +1,36 @@ +/* + * 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.syncope.core.persistence.api.entity; + +import java.util.Date; + +public interface AccessToken extends ProvidedKeyEntity { + + String getBody(); + + void setBody(String body); + + Date getExpiryTime(); + + void setExpiryTime(Date expiryTime); + + String getOwner(); + + void setOwner(String owner); +} http://git-wip-us.apache.org/repos/asf/syncope/blob/521f51a9/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAAccessTokenDAO.java ---------------------------------------------------------------------- diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAAccessTokenDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAAccessTokenDAO.java new file mode 100644 index 0000000..01c53fd --- /dev/null +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAAccessTokenDAO.java @@ -0,0 +1,143 @@ +/* + * 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.syncope.core.persistence.jpa.dao; + +import java.util.Date; +import java.util.List; +import javax.persistence.NoResultException; +import javax.persistence.Query; +import javax.persistence.TypedQuery; +import org.apache.commons.lang3.StringUtils; +import org.apache.syncope.core.persistence.api.dao.AccessTokenDAO; +import org.apache.syncope.core.persistence.api.dao.search.OrderByClause; +import org.apache.syncope.core.persistence.api.entity.AccessToken; +import org.apache.syncope.core.persistence.jpa.entity.JPAAccessToken; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.ReflectionUtils; + +@Repository +public class JPAAccessTokenDAO extends AbstractDAO<AccessToken> implements AccessTokenDAO { + + @Transactional(readOnly = true) + @Override + public AccessToken find(final String key) { + return entityManager().find(JPAAccessToken.class, key); + } + + @Transactional(readOnly = true) + @Override + public AccessToken findByOwner(final String username) { + TypedQuery<AccessToken> query = entityManager().createQuery( + "SELECT e FROM " + JPAAccessToken.class.getSimpleName() + " e " + + "WHERE e.owner=:username", AccessToken.class); + query.setParameter("username", username); + + AccessToken result = null; + try { + result = query.getSingleResult(); + } catch (NoResultException e) { + LOG.debug("No token for user {} could be found", username, e); + } + + return result; + } + + private StringBuilder buildFindAllQuery() { + return new StringBuilder("SELECT e FROM "). + append(JPAAccessToken.class.getSimpleName()). + append(" e WHERE 1=1"); + } + + @Transactional(readOnly = true) + @Override + public int count() { + StringBuilder queryString = buildFindAllQuery(); + + Query query = entityManager().createQuery(StringUtils.replaceOnce( + queryString.toString(), "SELECT e", "SELECT COUNT(e)")); + return ((Number) query.getSingleResult()).intValue(); + } + + private String toOrderByStatement(final List<OrderByClause> orderByClauses) { + StringBuilder statement = new StringBuilder(); + + for (OrderByClause clause : orderByClauses) { + String field = clause.getField().trim(); + if (ReflectionUtils.findField(JPAAccessToken.class, field) != null) { + statement.append("e.").append(field).append(' ').append(clause.getDirection().name()); + } + } + + if (statement.length() == 0) { + statement.append("ORDER BY e.expiryTime DESC"); + } else { + statement.insert(0, "ORDER BY "); + } + return statement.toString(); + } + + @Transactional(readOnly = true) + @Override + public List<AccessToken> findAll(final int page, final int itemsPerPage, final List<OrderByClause> orderByClauses) { + StringBuilder queryString = buildFindAllQuery().append(toOrderByStatement(orderByClauses)); + + TypedQuery<AccessToken> query = entityManager().createQuery(queryString.toString(), AccessToken.class); + + query.setFirstResult(itemsPerPage * (page <= 0 + ? 0 + : page - 1)); + + if (itemsPerPage > 0) { + query.setMaxResults(itemsPerPage); + } + + return query.getResultList(); + } + + @Override + @Transactional(rollbackFor = Throwable.class) + public AccessToken save(final AccessToken accessToken) { + return entityManager().merge(accessToken); + } + + @Override + public void delete(final String key) { + AccessToken accessToken = find(key); + if (accessToken == null) { + return; + } + + delete(accessToken); + } + + @Override + public void delete(final AccessToken accessToken) { + entityManager().remove(accessToken); + } + + @Override + public int deleteExpired() { + Query query = entityManager().createQuery( + "DELETE FROM " + JPAAccessToken.class.getSimpleName() + " e " + + "WHERE e.expiryTime < :now"); + query.setParameter("now", new Date()); + return query.executeUpdate(); + } +} http://git-wip-us.apache.org/repos/asf/syncope/blob/521f51a9/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAUserDAO.java ---------------------------------------------------------------------- diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAUserDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAUserDAO.java index 039c279..3580136 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAUserDAO.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAUserDAO.java @@ -50,6 +50,7 @@ import org.apache.syncope.core.spring.security.DelegatedAdministrationException; import org.apache.syncope.core.spring.ApplicationContextProvider; import org.apache.syncope.core.persistence.api.ImplementationLookup; import org.apache.syncope.core.persistence.api.attrvalue.validation.InvalidEntityException; +import org.apache.syncope.core.persistence.api.dao.AccessTokenDAO; import org.apache.syncope.core.persistence.api.dao.AccountRule; import org.apache.syncope.core.persistence.api.dao.GroupDAO; import org.apache.syncope.core.persistence.api.dao.NotFoundException; @@ -57,6 +58,7 @@ import org.apache.syncope.core.persistence.api.dao.PasswordRule; import org.apache.syncope.core.persistence.api.dao.RealmDAO; import org.apache.syncope.core.persistence.api.dao.RoleDAO; import org.apache.syncope.core.persistence.api.dao.UserDAO; +import org.apache.syncope.core.persistence.api.entity.AccessToken; import org.apache.syncope.core.persistence.api.entity.AnyUtils; import org.apache.syncope.core.persistence.api.entity.Realm; import org.apache.syncope.core.persistence.api.entity.Role; @@ -96,6 +98,9 @@ public class JPAUserDAO extends AbstractAnyDAO<User> implements UserDAO { private RoleDAO roleDAO; @Autowired + private AccessTokenDAO accessTokenDAO; + + @Autowired private ImplementationLookup implementationLookup; @Resource(name = "adminUser") @@ -424,6 +429,11 @@ public class JPAUserDAO extends AbstractAnyDAO<User> implements UserDAO { group.getUDynMembership().getMembers().remove(user); } + AccessToken accessToken = accessTokenDAO.findByOwner(user.getUsername()); + if (accessToken != null) { + accessTokenDAO.delete(accessToken); + } + entityManager().remove(user); } http://git-wip-us.apache.org/repos/asf/syncope/blob/521f51a9/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAAccessToken.java ---------------------------------------------------------------------- diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAAccessToken.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAAccessToken.java new file mode 100644 index 0000000..05464d6 --- /dev/null +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAAccessToken.java @@ -0,0 +1,83 @@ +/* + * 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.syncope.core.persistence.jpa.entity; + +import java.util.Date; +import javax.persistence.Cacheable; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Lob; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; +import org.apache.syncope.core.persistence.api.entity.AccessToken; + +@Entity +@Table(name = JPAAccessToken.TABLE) +@Cacheable +public class JPAAccessToken extends AbstractProvidedKeyEntity implements AccessToken { + + public static final String TABLE = "AccessToken"; + + private static final long serialVersionUID = -8734194815582467949L; + + @Lob + private String body; + + @Temporal(TemporalType.TIMESTAMP) + private Date expiryTime; + + @Column(nullable = true) + private String owner; + + @Override + public String getBody() { + return body; + } + + @Override + public void setBody(final String body) { + this.body = body; + } + + @Override + public Date getExpiryTime() { + return expiryTime == null + ? null + : new Date(expiryTime.getTime()); + } + + @Override + public void setExpiryTime(final Date expiryTime) { + this.expiryTime = expiryTime == null + ? null + : new Date(expiryTime.getTime()); + } + + @Override + public String getOwner() { + return owner; + } + + @Override + public void setOwner(final String owner) { + this.owner = owner; + } + +} http://git-wip-us.apache.org/repos/asf/syncope/blob/521f51a9/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAEntityFactory.java ---------------------------------------------------------------------- diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAEntityFactory.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAEntityFactory.java index 30483fc..7d9660c 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAEntityFactory.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAEntityFactory.java @@ -18,6 +18,7 @@ */ package org.apache.syncope.core.persistence.jpa.entity; +import org.apache.syncope.core.persistence.api.entity.AccessToken; import org.apache.syncope.core.persistence.jpa.entity.policy.JPAPasswordPolicy; import org.apache.syncope.core.persistence.jpa.entity.policy.JPAPullPolicy; import org.apache.syncope.core.persistence.jpa.entity.policy.JPAAccountPolicy; @@ -252,6 +253,8 @@ public class JPAEntityFactory implements EntityFactory { result = (E) new JPAADynGroupMembership(); } else if (reference.equals(UDynGroupMembership.class)) { result = (E) new JPAUDynGroupMembership(); + } else if (reference.equals(AccessToken.class)) { + result = (E) new JPAAccessToken(); } else { throw new IllegalArgumentException("Could not find a JPA implementation of " + reference.getName()); }