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.console;
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.List;
26  import java.util.Map;
27  import java.util.Optional;
28  import java.util.Set;
29  import java.util.concurrent.Callable;
30  import java.util.concurrent.CompletableFuture;
31  import java.util.concurrent.Future;
32  import java.util.stream.Collectors;
33  import javax.ws.rs.BadRequestException;
34  import javax.ws.rs.ForbiddenException;
35  import javax.ws.rs.core.EntityTag;
36  import javax.ws.rs.core.MediaType;
37  import javax.xml.ws.WebServiceException;
38  import org.apache.commons.lang3.ArrayUtils;
39  import org.apache.commons.lang3.StringUtils;
40  import org.apache.commons.lang3.exception.ExceptionUtils;
41  import org.apache.commons.lang3.time.FastDateFormat;
42  import org.apache.commons.lang3.tuple.Pair;
43  import org.apache.commons.lang3.tuple.Triple;
44  import org.apache.cxf.jaxrs.client.WebClient;
45  import org.apache.syncope.client.console.commons.RealmsUtils;
46  import org.apache.syncope.client.lib.SyncopeAnonymousClient;
47  import org.apache.syncope.client.lib.SyncopeClient;
48  import org.apache.syncope.client.lib.SyncopeClientFactoryBean;
49  import org.apache.syncope.client.lib.batch.BatchRequest;
50  import org.apache.syncope.client.ui.commons.BaseSession;
51  import org.apache.syncope.client.ui.commons.Constants;
52  import org.apache.syncope.client.ui.commons.DateOps;
53  import org.apache.syncope.common.lib.SyncopeClientException;
54  import org.apache.syncope.common.lib.SyncopeConstants;
55  import org.apache.syncope.common.lib.info.PlatformInfo;
56  import org.apache.syncope.common.lib.info.SystemInfo;
57  import org.apache.syncope.common.lib.to.UserTO;
58  import org.apache.syncope.common.lib.types.IdRepoEntitlement;
59  import org.apache.wicket.Session;
60  import org.apache.wicket.authroles.authentication.AuthenticatedWebSession;
61  import org.apache.wicket.authroles.authorization.strategies.role.Roles;
62  import org.apache.wicket.request.Request;
63  import org.slf4j.Logger;
64  import org.slf4j.LoggerFactory;
65  import org.springframework.core.task.TaskRejectedException;
66  import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
67  import org.springframework.util.CollectionUtils;
68  
69  public class SyncopeConsoleSession extends AuthenticatedWebSession implements BaseSession {
70  
71      private static final long serialVersionUID = 747562246415852166L;
72  
73      public enum Error {
74          SESSION_EXPIRED("error.session.expired", "Session expired: please login again"),
75          AUTHORIZATION("error.authorization", "Insufficient access rights when performing the requested operation"),
76          REST("error.rest", "There was an error while contacting the Core server");
77  
78          private final String key;
79  
80          private final String fallback;
81  
82          Error(final String key, final String fallback) {
83              this.key = key;
84              this.fallback = fallback;
85          }
86  
87          public String key() {
88              return key;
89          }
90  
91          public String fallback() {
92              return fallback;
93          }
94      }
95  
96      protected static final Logger LOG = LoggerFactory.getLogger(SyncopeConsoleSession.class);
97  
98      public static SyncopeConsoleSession get() {
99          return (SyncopeConsoleSession) Session.get();
100     }
101 
102     protected final SyncopeClientFactoryBean clientFactory;
103 
104     protected final Map<Class<?>, Object> services = Collections.synchronizedMap(new HashMap<>());
105 
106     protected final ThreadPoolTaskExecutor executor;
107 
108     protected String domain;
109 
110     protected SyncopeClient client;
111 
112     protected SyncopeAnonymousClient anonymousClient;
113 
114     protected Pair<String, String> gitAndBuildInfo;
115 
116     protected PlatformInfo platformInfo;
117 
118     protected SystemInfo systemInfo;
119 
120     protected UserTO selfTO;
121 
122     protected Map<String, Set<String>> auth;
123 
124     protected List<String> delegations;
125 
126     protected String delegatedBy;
127 
128     protected Roles roles;
129 
130     public SyncopeConsoleSession(final Request request) {
131         super(request);
132 
133         clientFactory = SyncopeWebApplication.get().newClientFactory();
134 
135         executor = SyncopeWebApplication.get().newThreadPoolTaskExecutor();
136     }
137 
138     protected String message(final SyncopeClientException sce) {
139         return sce.getType().name() + ": " + sce.getElements().stream().collect(Collectors.joining(", "));
140     }
141 
142     @Override
143     public void onException(final Exception e) {
144         Throwable root = ExceptionUtils.getRootCause(e);
145         String message = root.getMessage();
146 
147         if (root instanceof SyncopeClientException) {
148             SyncopeClientException sce = (SyncopeClientException) root;
149             message = sce.isComposite()
150                     ? sce.asComposite().getExceptions().stream().map(this::message).collect(Collectors.joining("; "))
151                     : message(sce);
152         } else if (root instanceof AccessControlException || root instanceof ForbiddenException) {
153             Error error = StringUtils.containsIgnoreCase(message, "expired")
154                     ? Error.SESSION_EXPIRED
155                     : Error.AUTHORIZATION;
156             message = getApplication().getResourceSettings().getLocalizer().
157                     getString(error.key(), null, null, null, null, error.fallback());
158         } else if (root instanceof BadRequestException || root instanceof WebServiceException) {
159             message = getApplication().getResourceSettings().getLocalizer().
160                     getString(Error.REST.key(), null, null, null, null, Error.REST.fallback());
161         }
162 
163         message = getApplication().getResourceSettings().getLocalizer().
164                 getString(message, null, null, null, null, message);
165         error(message);
166     }
167 
168     public MediaType getMediaType() {
169         return clientFactory.getContentType().getMediaType();
170     }
171 
172     public void execute(final Runnable command) {
173         try {
174             executor.execute(command);
175         } catch (TaskRejectedException e) {
176             LOG.error("Could not execute {}", command, e);
177         }
178     }
179 
180     @Override
181     public <T> Future<T> execute(final Callable<T> command) {
182         try {
183             return executor.submit(command);
184         } catch (TaskRejectedException e) {
185             LOG.error("Could not execute {}", command, e);
186 
187             return new CompletableFuture<>();
188         }
189     }
190 
191     public Pair<String, String> gitAndBuildInfo() {
192         return gitAndBuildInfo;
193     }
194 
195     public PlatformInfo getPlatformInfo() {
196         return platformInfo;
197     }
198 
199     public SystemInfo getSystemInfo() {
200         return systemInfo;
201     }
202 
203     @Override
204     public void setDomain(final String domain) {
205         this.domain = domain;
206     }
207 
208     @Override
209     public String getDomain() {
210         return StringUtils.isBlank(domain) ? SyncopeConstants.MASTER_DOMAIN : domain;
211     }
212 
213     @Override
214     public String getJWT() {
215         return Optional.ofNullable(client).map(SyncopeClient::getJWT).orElse(null);
216     }
217 
218     @Override
219     public boolean authenticate(final String username, final String password) {
220         boolean authenticated = false;
221 
222         try {
223             client = clientFactory.setDomain(getDomain()).create(username, password);
224 
225             refreshAuth(username);
226 
227             authenticated = true;
228         } catch (Exception e) {
229             LOG.error("Authentication failed", e);
230         }
231 
232         return authenticated;
233     }
234 
235     public boolean authenticate(final String jwt) {
236         boolean authenticated = false;
237 
238         try {
239             client = clientFactory.setDomain(getDomain()).create(jwt);
240 
241             refreshAuth(null);
242 
243             authenticated = true;
244         } catch (Exception e) {
245             LOG.error("Authentication failed", e);
246         }
247 
248         if (authenticated) {
249             bind();
250         }
251         signIn(authenticated);
252 
253         return authenticated;
254     }
255 
256     public void cleanup() {
257         anonymousClient = null;
258         gitAndBuildInfo = null;
259         platformInfo = null;
260         systemInfo = null;
261 
262         client = null;
263         auth = null;
264         delegations = null;
265         delegatedBy = null;
266         selfTO = null;
267         services.clear();
268     }
269 
270     @Override
271     public void invalidate() {
272         if (getJWT() != null) {
273             if (client != null) {
274                 client.logout();
275             }
276             cleanup();
277         }
278         executor.shutdown();
279         super.invalidate();
280     }
281 
282     public UserTO getSelfTO() {
283         return selfTO;
284     }
285 
286     public List<String> getAuthRealms() {
287         return auth.values().stream().flatMap(Set::stream).distinct().sorted().collect(Collectors.toList());
288     }
289 
290     public List<String> getSearchableRealms() {
291         Set<String> roots = auth.get(IdRepoEntitlement.REALM_SEARCH);
292         return CollectionUtils.isEmpty(roots)
293                 ? List.of()
294                 : roots.stream().sorted().collect(Collectors.toList());
295     }
296 
297     public Optional<String> getRootRealm(final String initial) {
298         List<String> searchable = getSearchableRealms();
299         return searchable.isEmpty()
300                 ? Optional.empty()
301                 : initial != null && searchable.stream().anyMatch(initial::startsWith)
302                 ? Optional.of(initial)
303                 : searchable.stream().findFirst();
304     }
305 
306     public boolean owns(final String entitlements, final String... realms) {
307         if (StringUtils.isEmpty(entitlements)) {
308             return true;
309         }
310 
311         if (auth == null) {
312             return false;
313         }
314 
315         Set<String> requested = ArrayUtils.isEmpty(realms)
316                 ? Set.of()
317                 : Set.of(realms);
318 
319         for (String entitlement : entitlements.split(",")) {
320             if (auth.containsKey(entitlement)) {
321                 boolean owns = false;
322 
323                 Set<String> owned = auth.get(entitlement).stream().
324                         map(RealmsUtils::getFullPath).collect(Collectors.toSet());
325                 if (requested.isEmpty()) {
326                     return !owned.isEmpty();
327                 } else {
328                     for (String realm : requested) {
329                         if (realm.startsWith(SyncopeConstants.ROOT_REALM)) {
330                             owns |= owned.stream().anyMatch(realm::startsWith);
331                         } else {
332                             owns |= owned.contains(realm);
333                         }
334                     }
335                 }
336 
337                 return owns;
338             }
339         }
340 
341         return false;
342     }
343 
344     @Override
345     public Roles getRoles() {
346         if (isSignedIn() && roles == null && auth != null) {
347             roles = new Roles(auth.keySet().toArray(String[]::new));
348             roles.add(Constants.ROLE_AUTHENTICATED);
349         }
350 
351         return roles;
352     }
353 
354     public List<String> getDelegations() {
355         return delegations;
356     }
357 
358     public String getDelegatedBy() {
359         return delegatedBy;
360     }
361 
362     public void setDelegatedBy(final String delegatedBy) {
363         this.delegatedBy = delegatedBy;
364 
365         this.client.delegatedBy(delegatedBy);
366 
367         refreshAuth(null);
368     }
369 
370     public void refreshAuth(final String username) {
371         try {
372             anonymousClient = SyncopeWebApplication.get().newAnonymousClient(getDomain());
373             gitAndBuildInfo = anonymousClient.gitAndBuildInfo();
374             platformInfo = anonymousClient.platform();
375             systemInfo = anonymousClient.system();
376 
377             Triple<Map<String, Set<String>>, List<String>, UserTO> self = client.self();
378             auth = self.getLeft();
379             delegations = self.getMiddle();
380             selfTO = self.getRight();
381             roles = null;
382         } catch (ForbiddenException e) {
383             LOG.warn("Could not read self(), probably in a {} scenario", IdRepoEntitlement.MUST_CHANGE_PASSWORD, e);
384 
385             selfTO = new UserTO();
386             selfTO.setUsername(username);
387             selfTO.setMustChangePassword(true);
388         }
389     }
390 
391     @Override
392     public SyncopeAnonymousClient getAnonymousClient() {
393         return Optional.ofNullable(anonymousClient).
394                 orElseGet(() -> SyncopeWebApplication.get().newAnonymousClient(getDomain()));
395     }
396 
397     @Override
398     public <T> T getAnonymousService(final Class<T> serviceClass) {
399         return getAnonymousClient().getService(serviceClass);
400     }
401 
402     @SuppressWarnings("unchecked")
403     protected <T> T getCachedService(final Class<T> serviceClass) {
404         T service;
405         if (services.containsKey(serviceClass)) {
406             service = (T) services.get(serviceClass);
407         } else {
408             service = client.getService(serviceClass);
409             services.put(serviceClass, service);
410         }
411 
412         WebClient.client(service).type(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON);
413 
414         return service;
415     }
416 
417     @Override
418     public <T> T getService(final Class<T> serviceClass) {
419         return getCachedService(serviceClass);
420     }
421 
422     @Override
423     public <T> T getService(final String etag, final Class<T> serviceClass) {
424         T serviceInstance = getCachedService(serviceClass);
425         WebClient.client(serviceInstance).match(new EntityTag(etag), false);
426 
427         return serviceInstance;
428     }
429 
430     public BatchRequest batch() {
431         return client.batch();
432     }
433 
434     @Override
435     public <T> void resetClient(final Class<T> service) {
436         T serviceInstance = getCachedService(service);
437         WebClient.client(serviceInstance).reset();
438     }
439 
440     @Override
441     public DateOps.Format getDateFormat() {
442         return new DateOps.Format(FastDateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, getLocale()));
443     }
444 }