1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 }