View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.syncope.client.enduser;
20  
21  import java.security.AccessControlException;
22  import java.text.DateFormat;
23  import java.util.Collections;
24  import java.util.HashMap;
25  import java.util.Map;
26  import java.util.Optional;
27  import java.util.concurrent.Callable;
28  import java.util.concurrent.CompletableFuture;
29  import java.util.concurrent.Future;
30  import java.util.stream.Collectors;
31  import javax.ws.rs.BadRequestException;
32  import javax.ws.rs.ForbiddenException;
33  import javax.ws.rs.core.EntityTag;
34  import javax.ws.rs.core.MediaType;
35  import javax.xml.ws.WebServiceException;
36  import org.apache.commons.lang3.StringUtils;
37  import org.apache.commons.lang3.exception.ExceptionUtils;
38  import org.apache.commons.lang3.time.FastDateFormat;
39  import org.apache.cxf.jaxrs.client.WebClient;
40  import org.apache.syncope.client.lib.SyncopeAnonymousClient;
41  import org.apache.syncope.client.lib.SyncopeClient;
42  import org.apache.syncope.client.lib.SyncopeClientFactoryBean;
43  import org.apache.syncope.client.ui.commons.BaseSession;
44  import org.apache.syncope.client.ui.commons.DateOps;
45  import org.apache.syncope.common.lib.SyncopeClientException;
46  import org.apache.syncope.common.lib.SyncopeConstants;
47  import org.apache.syncope.common.lib.info.PlatformInfo;
48  import org.apache.syncope.common.lib.to.UserTO;
49  import org.apache.syncope.common.lib.types.ClientExceptionType;
50  import org.apache.syncope.common.lib.types.IdRepoEntitlement;
51  import org.apache.syncope.common.rest.api.RESTHeaders;
52  import org.apache.wicket.Session;
53  import org.apache.wicket.authroles.authentication.AuthenticatedWebSession;
54  import org.apache.wicket.authroles.authorization.strategies.role.Roles;
55  import org.apache.wicket.request.Request;
56  import org.slf4j.Logger;
57  import org.slf4j.LoggerFactory;
58  
59  public class SyncopeEnduserSession extends AuthenticatedWebSession implements BaseSession {
60  
61      private static final long serialVersionUID = 747562246415852166L;
62  
63      public enum Error {
64          INVALID_SECURITY_ANSWER("invalid.security.answer", "Invalid Security Answer"),
65          SESSION_EXPIRED("error.session.expired", "Session expired: please login again"),
66          AUTHORIZATION("error.authorization", "Insufficient access rights when performing the requested operation"),
67          REST("error.rest", "There was an error while contacting the Core server");
68  
69          private final String key;
70  
71          private final String fallback;
72  
73          Error(final String key, final String fallback) {
74              this.key = key;
75              this.fallback = fallback;
76          }
77  
78          public String key() {
79              return key;
80          }
81  
82          public String fallback() {
83              return fallback;
84          }
85      }
86  
87      protected static final Logger LOG = LoggerFactory.getLogger(SyncopeEnduserSession.class);
88  
89      public static SyncopeEnduserSession get() {
90          return (SyncopeEnduserSession) Session.get();
91      }
92  
93      protected final SyncopeClientFactoryBean clientFactory;
94  
95      protected final Map<Class<?>, Object> services = Collections.synchronizedMap(new HashMap<>());
96  
97      protected String domain;
98  
99      protected SyncopeClient client;
100 
101     protected SyncopeAnonymousClient anonymousClient;
102 
103     protected UserTO selfTO;
104 
105     public SyncopeEnduserSession(final Request request) {
106         super(request);
107 
108         clientFactory = SyncopeWebApplication.get().newClientFactory();
109     }
110 
111     protected String message(final SyncopeClientException sce) {
112         Error error = null;
113         if (sce.getType() == ClientExceptionType.InvalidSecurityAnswer) {
114             error = Error.INVALID_SECURITY_ANSWER;
115         }
116         if (error == null) {
117             return sce.getType().name() + ": " + sce.getElements().stream().collect(Collectors.joining(", "));
118         }
119         return getApplication().getResourceSettings().getLocalizer().
120                 getString(error.key(), null, null, null, null, error.fallback());
121     }
122 
123     /**
124      * Extract and localize (if translation available) the actual message from the given exception; then, report it
125      * via {@link Session#error(java.io.Serializable)}.
126      *
127      * @see org.apache.syncope.client.lib.RestClientExceptionMapper
128      *
129      * @param e raised exception
130      */
131     @Override
132     public void onException(final Exception e) {
133         Throwable root = ExceptionUtils.getRootCause(e);
134         String message = root.getMessage();
135 
136         if (root instanceof SyncopeClientException) {
137             SyncopeClientException sce = (SyncopeClientException) root;
138             message = sce.isComposite()
139                     ? sce.asComposite().getExceptions().stream().map(this::message).collect(Collectors.joining("; "))
140                     : message(sce);
141         } else if (root instanceof AccessControlException || root instanceof ForbiddenException) {
142             Error error = StringUtils.containsIgnoreCase(message, "expired")
143                     ? Error.SESSION_EXPIRED
144                     : Error.AUTHORIZATION;
145             message = getApplication().getResourceSettings().getLocalizer().
146                     getString(error.key(), null, null, null, null, error.fallback());
147         } else if (root instanceof BadRequestException || root instanceof WebServiceException) {
148             message = getApplication().getResourceSettings().getLocalizer().
149                     getString(Error.REST.key(), null, null, null, null, Error.REST.fallback());
150         }
151 
152         message = getApplication().getResourceSettings().getLocalizer().
153                 getString(message, null, null, null, null, message);
154         error(message);
155     }
156 
157     public MediaType getMediaType() {
158         return clientFactory.getContentType().getMediaType();
159     }
160 
161     @Override
162     public <T> Future<T> execute(final Callable<T> command) {
163         try {
164             return CompletableFuture.completedFuture(command.call());
165         } catch (Exception e) {
166             LOG.error("Could not execute {}", command, e);
167         }
168         return new CompletableFuture<>();
169     }
170 
171     @Override
172     public void setDomain(final String domain) {
173         this.domain = domain;
174     }
175 
176     @Override
177     public String getDomain() {
178         return StringUtils.isBlank(domain) ? SyncopeConstants.MASTER_DOMAIN : domain;
179     }
180 
181     @Override
182     public String getJWT() {
183         return client == null ? null : client.getJWT();
184     }
185 
186     @Override
187     public Roles getRoles() {
188         throw new UnsupportedOperationException("Not supported yet.");
189     }
190 
191     public PlatformInfo getPlatformInfo() {
192         return getAnonymousClient().platform();
193     }
194 
195     @Override
196     public boolean authenticate(final String username, final String password) {
197         boolean authenticated = false;
198         if (SyncopeWebApplication.get().getAdminUser().equalsIgnoreCase(username)) {
199             return authenticated;
200         }
201 
202         try {
203             client = clientFactory.setDomain(getDomain()).create(username, password);
204 
205             refreshAuth(username);
206 
207             authenticated = true;
208         } catch (Exception e) {
209             LOG.error("Authentication failed", e);
210         }
211 
212         return authenticated;
213     }
214 
215     public boolean authenticate(final String jwt) {
216         boolean authenticated = false;
217 
218         try {
219             client = clientFactory.setDomain(getDomain()).create(jwt);
220 
221             refreshAuth(null);
222 
223             authenticated = true;
224         } catch (Exception e) {
225             LOG.error("Authentication failed", e);
226         }
227 
228         return authenticated;
229     }
230 
231     protected void refreshAuth(final String username) {
232         try {
233             anonymousClient = SyncopeWebApplication.get().newAnonymousClient(getDomain());
234 
235             selfTO = client.self().getRight();
236         } catch (ForbiddenException e) {
237             LOG.warn("Could not read self(), probably in a {} scenario", IdRepoEntitlement.MUST_CHANGE_PASSWORD, e);
238 
239             selfTO = new UserTO();
240             selfTO.setUsername(username);
241             selfTO.setMustChangePassword(true);
242         }
243 
244         // bind explicitly this session to have a stateful behavior during http requests, unless session will
245         // expire at each request
246         this.bind();
247     }
248 
249     protected boolean isAuthenticated() {
250         return client != null && client.getJWT() != null;
251     }
252 
253     protected boolean isMustChangePassword() {
254         return selfTO != null && selfTO.isMustChangePassword();
255     }
256 
257     public void cleanup() {
258         anonymousClient = null;
259 
260         client = null;
261         selfTO = null;
262         services.clear();
263     }
264 
265     @Override
266     public void invalidate() {
267         if (isAuthenticated()) {
268             try {
269                 client.logout();
270             } catch (Exception e) {
271                 LOG.debug("Unexpected exception while logging out", e);
272             } finally {
273                 client = null;
274                 selfTO = null;
275             }
276         }
277         super.invalidate();
278     }
279 
280     public UserTO getSelfTO() {
281         return getSelfTO(false);
282     }
283 
284     public UserTO getSelfTO(final boolean reload) {
285         if (reload) {
286             refreshAuth(selfTO.getUsername());
287         }
288         return selfTO;
289     }
290 
291     @Override
292     public SyncopeAnonymousClient getAnonymousClient() {
293         return Optional.ofNullable(anonymousClient).
294                 orElseGet(() -> SyncopeWebApplication.get().newAnonymousClient(getDomain()));
295     }
296 
297     @Override
298     public <T> T getAnonymousService(final Class<T> serviceClass) {
299         return getAnonymousClient().getService(serviceClass);
300     }
301 
302     @SuppressWarnings("unchecked")
303     private <T> T getCachedService(final Class<T> serviceClass) {
304         T service;
305         if (services.containsKey(serviceClass)) {
306             service = (T) services.get(serviceClass);
307         } else {
308             service = client.getService(serviceClass);
309             services.put(serviceClass, service);
310         }
311 
312         WebClient.client(service).type(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON);
313 
314         return service;
315     }
316 
317     @Override
318     public <T> T getService(final Class<T> serviceClass) {
319         T service = (client == null || !isAuthenticated())
320                 ? getAnonymousClient().getService(serviceClass)
321                 : client.getService(serviceClass);
322         WebClient.client(service).header(RESTHeaders.DOMAIN, getDomain());
323         return service;
324     }
325 
326     @Override
327     public <T> T getService(final String etag, final Class<T> serviceClass) {
328         T serviceInstance = getService(serviceClass);
329         WebClient.client(serviceInstance).match(new EntityTag(etag), false).
330                 type(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON);
331 
332         return serviceInstance;
333     }
334 
335     @Override
336     public <T> void resetClient(final Class<T> service) {
337         T serviceInstance = getCachedService(service);
338         WebClient.client(serviceInstance).reset();
339     }
340 
341     @Override
342     public DateOps.Format getDateFormat() {
343         return new DateOps.Format(FastDateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, getLocale()));
344     }
345 }