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 com.fasterxml.jackson.core.type.TypeReference;
22  import com.fasterxml.jackson.databind.json.JsonMapper;
23  import com.giffing.wicket.spring.boot.starter.app.WicketBootSecuredWebApplication;
24  import de.agilecoders.wicket.core.Bootstrap;
25  import de.agilecoders.wicket.core.settings.BootstrapSettings;
26  import de.agilecoders.wicket.core.settings.IBootstrapSettings;
27  import de.agilecoders.wicket.core.settings.SingleThemeProvider;
28  import java.io.InputStream;
29  import java.util.List;
30  import java.util.Map;
31  import org.apache.syncope.client.enduser.init.ClassPathScanImplementationLookup;
32  import org.apache.syncope.client.enduser.layout.UserFormLayoutInfo;
33  import org.apache.syncope.client.enduser.pages.BasePage;
34  import org.apache.syncope.client.enduser.pages.Dashboard;
35  import org.apache.syncope.client.enduser.pages.Login;
36  import org.apache.syncope.client.enduser.pages.MustChangePassword;
37  import org.apache.syncope.client.enduser.pages.SelfConfirmPasswordReset;
38  import org.apache.syncope.client.enduser.panels.Sidebar;
39  import org.apache.syncope.client.lib.SyncopeAnonymousClient;
40  import org.apache.syncope.client.lib.SyncopeClientFactoryBean;
41  import org.apache.syncope.client.ui.commons.SyncopeUIRequestCycleListener;
42  import org.apache.syncope.client.ui.commons.annotations.Resource;
43  import org.apache.syncope.client.ui.commons.themes.AdminLTE;
44  import org.apache.syncope.common.keymaster.client.api.ServiceOps;
45  import org.apache.syncope.common.keymaster.client.api.model.NetworkService;
46  import org.apache.wicket.Page;
47  import org.apache.wicket.Session;
48  import org.apache.wicket.WicketRuntimeException;
49  import org.apache.wicket.authorization.IAuthorizationStrategy;
50  import org.apache.wicket.authorization.IAuthorizationStrategy.AllowAllAuthorizationStrategy;
51  import org.apache.wicket.authroles.authentication.AbstractAuthenticatedWebSession;
52  import org.apache.wicket.markup.html.WebPage;
53  import org.apache.wicket.protocol.http.ResourceIsolationRequestCycleListener;
54  import org.apache.wicket.protocol.http.WebApplication;
55  import org.apache.wicket.protocol.http.servlet.XForwardedRequestWrapperFactory;
56  import org.apache.wicket.request.Request;
57  import org.apache.wicket.request.Response;
58  import org.apache.wicket.request.component.IRequestableComponent;
59  import org.apache.wicket.request.component.IRequestablePage;
60  import org.apache.wicket.request.cycle.IRequestCycleListener;
61  import org.apache.wicket.request.cycle.RequestCycle;
62  import org.apache.wicket.request.http.WebResponse;
63  import org.apache.wicket.request.mapper.parameter.PageParameters;
64  import org.apache.wicket.request.resource.IResource;
65  import org.apache.wicket.request.resource.ResourceReference;
66  import org.slf4j.Logger;
67  import org.slf4j.LoggerFactory;
68  import org.springframework.aop.support.AopUtils;
69  import org.springframework.core.io.ResourceLoader;
70  
71  public class SyncopeWebApplication extends WicketBootSecuredWebApplication {
72  
73      protected static final Logger LOG = LoggerFactory.getLogger(SyncopeWebApplication.class);
74  
75      protected static final JsonMapper MAPPER = JsonMapper.builder().findAndAddModules().build();
76  
77      public static SyncopeWebApplication get() {
78          return (SyncopeWebApplication) WebApplication.get();
79      }
80  
81      protected final ResourceLoader resourceLoader;
82  
83      protected final EnduserProperties props;
84  
85      protected final ClassPathScanImplementationLookup lookup;
86  
87      protected final ServiceOps serviceOps;
88  
89      protected final List<IResource> resources;
90  
91      protected UserFormLayoutInfo customFormLayout;
92  
93      public SyncopeWebApplication(
94              final ResourceLoader resourceLoader,
95              final EnduserProperties props,
96              final ClassPathScanImplementationLookup lookup,
97              final ServiceOps serviceOps,
98              final List<IResource> resources) {
99  
100         this.resourceLoader = resourceLoader;
101         this.props = props;
102         this.lookup = lookup;
103         this.serviceOps = serviceOps;
104         this.resources = resources;
105     }
106 
107     protected SyncopeUIRequestCycleListener buildSyncopeUIRequestCycleListener() {
108         return new SyncopeUIRequestCycleListener() {
109 
110             @Override
111             protected boolean isSignedIn() {
112                 return SyncopeEnduserSession.get().isAuthenticated();
113             }
114 
115             @Override
116             protected void invalidateSession() {
117                 SyncopeEnduserSession.get().invalidate();
118             }
119 
120             @Override
121             protected IRequestablePage getErrorPage(final PageParameters errorParameters) {
122                 return new Login(errorParameters);
123             }
124         };
125     }
126 
127     protected void initSecurity() {
128         if (props.isxForward()) {
129             XForwardedRequestWrapperFactory.Config config = new XForwardedRequestWrapperFactory.Config();
130             config.setProtocolHeader(props.getxForwardProtocolHeader());
131             config.setHttpServerPort(props.getxForwardHttpPort());
132             config.setHttpsServerPort(props.getxForwardHttpsPort());
133 
134             XForwardedRequestWrapperFactory factory = new XForwardedRequestWrapperFactory();
135             factory.setConfig(config);
136             getFilterFactoryManager().add(factory);
137         }
138 
139         if (props.isCsrf()) {
140             getRequestCycleListeners().add(new ResourceIsolationRequestCycleListener());
141         }
142 
143         getCspSettings().blocking().unsafeInline();
144 
145         getRequestCycleListeners().add(new IRequestCycleListener() {
146 
147             @Override
148             public void onEndRequest(final RequestCycle cycle) {
149                 if (cycle.getResponse() instanceof WebResponse) {
150                     props.getSecurityHeaders().
151                             forEach((name, value) -> ((WebResponse) cycle.getResponse()).setHeader(name, value));
152                 }
153             }
154         });
155     }
156 
157     @Override
158     protected void init() {
159         super.init();
160 
161         // Application settings
162         IBootstrapSettings settings = new BootstrapSettings();
163 
164         // set theme provider
165         settings.setThemeProvider(new SingleThemeProvider(new AdminLTE()));
166 
167         // install application settings
168         Bootstrap.install(this, settings);
169 
170         getResourceSettings().setUseMinifiedResources(true);
171         getResourceSettings().setUseDefaultOnMissingResource(true);
172         getResourceSettings().setThrowExceptionOnMissingResource(false);
173 
174         getSecuritySettings().setAuthorizationStrategy(getAuthorizationStrategy());
175 
176         getMarkupSettings().setStripWicketTags(true);
177         getMarkupSettings().setCompressWhitespace(true);
178 
179         getRequestCycleListeners().add(buildSyncopeUIRequestCycleListener());
180 
181         initSecurity();
182 
183         // Confirm password reset page
184         mountPage("/confirmpasswordreset", SelfConfirmPasswordReset.class);
185 
186         for (IResource resource : resources) {
187             Class<?> resourceClass = AopUtils.getTargetClass(resource);
188             Resource annotation = resourceClass.getAnnotation(Resource.class);
189             if (annotation == null) {
190                 LOG.error("No @Resource annotation found, ignoring {}", resourceClass.getName());
191             } else {
192                 LOG.debug("Mounting {} under {}", resourceClass.getName(), annotation.path());
193 
194                 mountResource(annotation.path(), new ResourceReference(annotation.key()) {
195 
196                     private static final long serialVersionUID = -128426276529456602L;
197 
198                     @Override
199                     public IResource getResource() {
200                         return resource;
201                     }
202                 });
203             }
204         }
205 
206         try (InputStream is = resourceLoader.getResource(props.getCustomFormLayout()).getInputStream()) {
207             customFormLayout = MAPPER.readValue(is, new TypeReference<>() {
208             });
209         } catch (Exception e) {
210             throw new WicketRuntimeException("Could not read " + props.getCustomFormLayout(), e);
211         }
212 
213         // enable component path
214         if (getDebugSettings().isAjaxDebugModeEnabled()) {
215             getDebugSettings().setComponentPathAttributeName("syncope-path");
216         }
217     }
218 
219     protected IAuthorizationStrategy getAuthorizationStrategy() {
220         return new AllowAllAuthorizationStrategy() {
221 
222             @Override
223             public <T extends IRequestableComponent> boolean isInstantiationAuthorized(final Class<T> componentClass) {
224                 if (BasePage.class.isAssignableFrom(componentClass)) {
225                     return props.getPage().entrySet().stream().
226                             filter(entry -> componentClass.equals(entry.getValue())).
227                             map(Map.Entry::getKey).findFirst().
228                             map(k -> SyncopeEnduserSession.get().isAuthenticated()).
229                             orElse(true);
230                 }
231                 return true;
232             }
233         };
234     }
235 
236     @Override
237     public Class<? extends Page> getHomePage() {
238         return SyncopeEnduserSession.get().isAuthenticated()
239                 && SyncopeEnduserSession.get().isMustChangePassword()
240                 ? MustChangePassword.class
241                 : SyncopeEnduserSession.get().isAuthenticated()
242                 ? getPageClass("profile", Dashboard.class)
243                 : getSignInPageClass();
244     }
245 
246     public ClassPathScanImplementationLookup getLookup() {
247         return lookup;
248     }
249 
250     public UserFormLayoutInfo getCustomFormLayout() {
251         return customFormLayout;
252     }
253 
254     public Class<? extends Sidebar> getSidebar() {
255         return props.getSidebar();
256     }
257 
258     @Override
259     public Session newSession(final Request request, final Response response) {
260         return new SyncopeEnduserSession(request);
261     }
262 
263     public SyncopeAnonymousClient newAnonymousClient(final String domain) {
264         return newClientFactory().
265                 setDomain(domain).
266                 createAnonymous(props.getAnonymousUser(), props.getAnonymousKey());
267     }
268 
269     public SyncopeClientFactoryBean newClientFactory() {
270         return new SyncopeClientFactoryBean().
271                 setAddress(serviceOps.get(NetworkService.Type.CORE).getAddress()).
272                 setUseCompression(props.isUseGZIPCompression());
273     }
274 
275     public Class<? extends BasePage> getPageClass(final String name) {
276         return props.getPage().get(name);
277     }
278 
279     public Class<? extends BasePage> getPageClass(final String name, final Class<? extends BasePage> defaultValue) {
280         return props.getPage().getOrDefault(name, defaultValue);
281     }
282 
283     @Override
284     protected Class<? extends AbstractAuthenticatedWebSession> getWebSessionClass() {
285         return SyncopeEnduserSession.class;
286     }
287 
288     @Override
289     protected Class<? extends WebPage> getSignInPageClass() {
290         return Login.class;
291     }
292 
293     public String getAdminUser() {
294         return props.getAdminUser();
295     }
296 
297     public String getAnonymousUser() {
298         return props.getAnonymousUser();
299     }
300 
301     public boolean isCaptchaEnabled() {
302         return props.isCaptcha();
303     }
304 
305     public boolean isReportPropagationErrors() {
306         return props.isReportPropagationErrors();
307     }
308 
309     public boolean isReportPropagationErrorDetails() {
310         return props.isReportPropagationErrorDetails();
311     }
312 
313     public long getMaxWaitTimeInSeconds() {
314         return props.getMaxWaitTimeOnApplyChanges();
315     }
316 
317     public Integer getMaxUploadFileSizeMB() {
318         return props.getMaxUploadFileSizeMB();
319     }
320 }