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.wa.bootstrap;
20  
21  import java.util.HashMap;
22  import java.util.HashSet;
23  import java.util.Map;
24  import java.util.Objects;
25  import java.util.Set;
26  import java.util.TreeMap;
27  import java.util.stream.Collectors;
28  import java.util.stream.Stream;
29  import org.apache.commons.lang3.ArrayUtils;
30  import org.apache.commons.lang3.StringUtils;
31  import org.apache.commons.lang3.tuple.Pair;
32  import org.apache.syncope.client.lib.SyncopeClient;
33  import org.apache.syncope.common.lib.OIDCScopeConstants;
34  import org.apache.syncope.common.lib.to.OIDCRPClientAppTO;
35  import org.apache.syncope.common.rest.api.service.AttrRepoService;
36  import org.apache.syncope.common.rest.api.service.AuthModuleService;
37  import org.apache.syncope.common.rest.api.service.wa.WAClientAppService;
38  import org.apache.syncope.common.rest.api.service.wa.WAConfigService;
39  import org.apache.syncope.wa.bootstrap.mapping.AttrReleaseMapper;
40  import org.apache.syncope.wa.bootstrap.mapping.AttrRepoPropertySourceMapper;
41  import org.apache.syncope.wa.bootstrap.mapping.AuthModulePropertySourceMapper;
42  import org.apereo.cas.configuration.model.support.oidc.OidcDiscoveryProperties;
43  import org.apereo.cas.oidc.claims.OidcAddressScopeAttributeReleasePolicy;
44  import org.apereo.cas.oidc.claims.OidcEmailScopeAttributeReleasePolicy;
45  import org.apereo.cas.oidc.claims.OidcPhoneScopeAttributeReleasePolicy;
46  import org.apereo.cas.oidc.claims.OidcProfileScopeAttributeReleasePolicy;
47  import org.apereo.cas.services.BaseMappedAttributeReleasePolicy;
48  import org.apereo.cas.services.ChainingAttributeReleasePolicy;
49  import org.apereo.cas.services.RegisteredServiceAttributeReleasePolicy;
50  import org.apereo.cas.services.ReturnAllowedAttributeReleasePolicy;
51  import org.apereo.cas.util.crypto.CipherExecutor;
52  import org.slf4j.Logger;
53  import org.slf4j.LoggerFactory;
54  import org.springframework.cloud.bootstrap.config.PropertySourceLocator;
55  import org.springframework.core.annotation.Order;
56  import org.springframework.core.env.Environment;
57  import org.springframework.core.env.MapPropertySource;
58  import org.springframework.core.env.PropertySource;
59  
60  @Order
61  public class WAPropertySourceLocator implements PropertySourceLocator {
62  
63      protected static final Logger LOG = LoggerFactory.getLogger(WAPropertySourceLocator.class);
64  
65      protected final WARestClient waRestClient;
66  
67      protected final AuthModulePropertySourceMapper authModulePropertySourceMapper;
68  
69      protected final AttrRepoPropertySourceMapper attrRepoPropertySourceMapper;
70  
71      protected final AttrReleaseMapper attrReleaseMapper;
72  
73      protected final CipherExecutor<String, String> configurationCipher;
74  
75      public WAPropertySourceLocator(
76              final WARestClient waRestClient,
77              final AuthModulePropertySourceMapper authModulePropertySourceMapper,
78              final AttrRepoPropertySourceMapper attrRepoPropertySourceMapper,
79              final AttrReleaseMapper attrReleaseMapper,
80              final CipherExecutor<String, String> configurationCipher) {
81  
82          this.waRestClient = waRestClient;
83          this.authModulePropertySourceMapper = authModulePropertySourceMapper;
84          this.attrRepoPropertySourceMapper = attrRepoPropertySourceMapper;
85          this.attrReleaseMapper = attrReleaseMapper;
86          this.configurationCipher = configurationCipher;
87      }
88  
89      protected Map<String, Object> index(final Map<String, Object> map, final Map<String, Integer> prefixes) {
90          Map<String, Object> indexed = map;
91  
92          if (!map.isEmpty()) {
93              String prefix = map.keySet().iterator().next();
94              if (prefix.contains("[]")) {
95                  prefix = StringUtils.substringBefore(prefix, "[]");
96                  Integer index = prefixes.getOrDefault(prefix, 0);
97  
98                  indexed = map.entrySet().stream().
99                          map(e -> Pair.of(e.getKey().replace("[]", "[" + index + "]"), e.getValue())).
100                         collect(Collectors.toMap(Pair::getKey, Pair::getValue));
101 
102                 prefixes.put(prefix, index + 1);
103             }
104         }
105 
106         return indexed;
107     }
108 
109     @Override
110     public PropertySource<?> locate(final Environment environment) {
111         SyncopeClient syncopeClient = waRestClient.getSyncopeClient();
112         if (syncopeClient == null) {
113             LOG.warn("Application context is not ready to bootstrap WA configuration");
114             return null;
115         }
116 
117         LOG.info("Bootstrapping WA configuration");
118         Map<String, Object> properties = new TreeMap<>();
119         Map<String, Integer> prefixes = new HashMap<>();
120 
121         syncopeClient.getService(AuthModuleService.class).list().forEach(authModuleTO -> {
122             LOG.debug("Mapping auth module {} ", authModuleTO.getKey());
123 
124             Map<String, Object> map = authModuleTO.getConf().map(authModuleTO, authModulePropertySourceMapper);
125             properties.putAll(index(map, prefixes));
126         });
127 
128         syncopeClient.getService(AttrRepoService.class).list().forEach(attrRepoTO -> {
129             LOG.debug("Mapping attr repo {} ", attrRepoTO.getKey());
130 
131             Map<String, Object> map = attrRepoTO.getConf().map(attrRepoTO, attrRepoPropertySourceMapper);
132             properties.putAll(index(map, prefixes));
133         });
134 
135         Set<String> customClaims = syncopeClient.getService(WAClientAppService.class).list().stream().
136                 filter(clientApp -> clientApp.getAttrReleasePolicy() != null
137                 && clientApp.getClientAppTO() instanceof OIDCRPClientAppTO).
138                 flatMap(clientApp -> {
139                     OIDCRPClientAppTO rp = OIDCRPClientAppTO.class.cast(clientApp.getClientAppTO());
140 
141                     RegisteredServiceAttributeReleasePolicy attributeReleasePolicy =
142                             attrReleaseMapper.build(clientApp.getAttrReleasePolicy());
143 
144                     Set<String> claims = new HashSet<>();
145                     if (attributeReleasePolicy instanceof BaseMappedAttributeReleasePolicy) {
146                         claims.addAll(((BaseMappedAttributeReleasePolicy) attributeReleasePolicy).
147                                 getAllowedAttributes().values().stream().
148                                 map(Objects::toString).collect(Collectors.toSet()));
149                     } else if (attributeReleasePolicy instanceof ReturnAllowedAttributeReleasePolicy) {
150                         claims.addAll(((ReturnAllowedAttributeReleasePolicy) attributeReleasePolicy).
151                                 getAllowedAttributes().stream().collect(Collectors.toSet()));
152                     } else if (attributeReleasePolicy instanceof ChainingAttributeReleasePolicy) {
153                         ((ChainingAttributeReleasePolicy) attributeReleasePolicy).getPolicies().stream().
154                                 filter(ReturnAllowedAttributeReleasePolicy.class::isInstance).
155                                 findFirst().map(ReturnAllowedAttributeReleasePolicy.class::cast).
156                                 map(p -> p.getAllowedAttributes().stream().collect(Collectors.toSet())).
157                                 ifPresent(claims::addAll);
158                     }
159                     if (rp.getScopes().contains(OIDCScopeConstants.PROFILE)) {
160                         claims.removeAll(OidcProfileScopeAttributeReleasePolicy.ALLOWED_CLAIMS);
161                     }
162                     if (rp.getScopes().contains(OIDCScopeConstants.ADDRESS)) {
163                         claims.removeAll(OidcAddressScopeAttributeReleasePolicy.ALLOWED_CLAIMS);
164                     }
165                     if (rp.getScopes().contains(OIDCScopeConstants.EMAIL)) {
166                         claims.removeAll(OidcEmailScopeAttributeReleasePolicy.ALLOWED_CLAIMS);
167                     }
168                     if (rp.getScopes().contains(OIDCScopeConstants.PHONE)) {
169                         claims.removeAll(OidcPhoneScopeAttributeReleasePolicy.ALLOWED_CLAIMS);
170                     }
171 
172                     return claims.stream();
173                 }).collect(Collectors.toSet());
174         if (!customClaims.isEmpty()) {
175             Stream.concat(new OidcDiscoveryProperties().getClaims().stream(), customClaims.stream()).
176                     collect(Collectors.joining(","));
177 
178             properties.put("cas.authn.oidc.discovery.claims",
179                     Stream.concat(new OidcDiscoveryProperties().getClaims().stream(), customClaims.stream()).
180                             collect(Collectors.joining(",")));
181             properties.put("cas.authn.oidc.core.user-defined-scopes.syncope",
182                     customClaims.stream().collect(Collectors.joining(",")));
183         }
184 
185         syncopeClient.getService(WAConfigService.class).list().forEach(attr -> properties.put(
186                 attr.getSchema(), attr.getValues().stream().collect(Collectors.joining(","))));
187 
188         LOG.debug("Collected WA properties: {}", properties);
189         Map<String, Object> decodedProperties = configurationCipher.decode(properties, ArrayUtils.EMPTY_OBJECT_ARRAY);
190         LOG.debug("Decoded WA properties: {}", decodedProperties);
191         return new MapPropertySource(getClass().getName(), decodedProperties);
192     }
193 }