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.core.logic.saml2;
20  
21  import java.text.ParseException;
22  import java.util.ArrayList;
23  import java.util.Collections;
24  import java.util.List;
25  import java.util.Map;
26  import java.util.Objects;
27  import java.util.Optional;
28  import java.util.concurrent.ConcurrentHashMap;
29  import java.util.stream.Collectors;
30  import org.apache.commons.lang3.SerializationUtils;
31  import org.apache.commons.lang3.tuple.Pair;
32  import org.apache.syncope.common.lib.AnyOperations;
33  import org.apache.syncope.common.lib.Attr;
34  import org.apache.syncope.common.lib.SyncopeConstants;
35  import org.apache.syncope.common.lib.request.UserCR;
36  import org.apache.syncope.common.lib.request.UserUR;
37  import org.apache.syncope.common.lib.saml2.SAML2LoginResponse;
38  import org.apache.syncope.common.lib.to.Item;
39  import org.apache.syncope.common.lib.to.PropagationStatus;
40  import org.apache.syncope.common.lib.to.UserTO;
41  import org.apache.syncope.common.lib.types.AnyTypeKind;
42  import org.apache.syncope.core.persistence.api.dao.ImplementationDAO;
43  import org.apache.syncope.core.persistence.api.dao.SAML2SP4UIIdPDAO;
44  import org.apache.syncope.core.persistence.api.dao.UserDAO;
45  import org.apache.syncope.core.persistence.api.entity.Implementation;
46  import org.apache.syncope.core.persistence.api.entity.SAML2SP4UIIdP;
47  import org.apache.syncope.core.persistence.api.entity.user.User;
48  import org.apache.syncope.core.provisioning.api.IntAttrName;
49  import org.apache.syncope.core.provisioning.api.IntAttrNameParser;
50  import org.apache.syncope.core.provisioning.api.SAML2SP4UIIdPActions;
51  import org.apache.syncope.core.provisioning.api.UserProvisioningManager;
52  import org.apache.syncope.core.provisioning.api.data.ItemTransformer;
53  import org.apache.syncope.core.provisioning.api.data.UserDataBinder;
54  import org.apache.syncope.core.provisioning.java.pushpull.InboundMatcher;
55  import org.apache.syncope.core.provisioning.java.utils.MappingUtils;
56  import org.apache.syncope.core.provisioning.java.utils.TemplateUtils;
57  import org.apache.syncope.core.spring.implementation.ImplementationManager;
58  import org.slf4j.Logger;
59  import org.slf4j.LoggerFactory;
60  import org.springframework.transaction.annotation.Propagation;
61  import org.springframework.transaction.annotation.Transactional;
62  
63  public class SAML2SP4UIUserManager {
64  
65      private static final Logger LOG = LoggerFactory.getLogger(SAML2SP4UIUserManager.class);
66  
67      private static final String SAML2SP_CONTEXT = "SAML 2.0 SP";
68  
69      protected final SAML2SP4UIIdPDAO idpDAO;
70  
71      protected final InboundMatcher inboundMatcher;
72  
73      protected final UserDAO userDAO;
74  
75      protected final ImplementationDAO implementationDAO;
76  
77      protected final IntAttrNameParser intAttrNameParser;
78  
79      protected final TemplateUtils templateUtils;
80  
81      protected final UserProvisioningManager provisioningManager;
82  
83      protected final UserDataBinder binder;
84  
85      protected final Map<String, SAML2SP4UIIdPActions> perContextActions = new ConcurrentHashMap<>();
86  
87      public SAML2SP4UIUserManager(
88              final SAML2SP4UIIdPDAO idpDAO,
89              final InboundMatcher inboundMatcher,
90              final UserDAO userDAO,
91              final ImplementationDAO implementationDAO,
92              final IntAttrNameParser intAttrNameParser,
93              final TemplateUtils templateUtils,
94              final UserProvisioningManager provisioningManager,
95              final UserDataBinder binder) {
96  
97          this.idpDAO = idpDAO;
98          this.inboundMatcher = inboundMatcher;
99          this.userDAO = userDAO;
100         this.implementationDAO = implementationDAO;
101         this.intAttrNameParser = intAttrNameParser;
102         this.templateUtils = templateUtils;
103         this.provisioningManager = provisioningManager;
104         this.binder = binder;
105     }
106 
107     @Transactional(readOnly = true)
108     public List<String> findMatchingUser(final String connObjectKeyValue, final String idpKey) {
109         SAML2SP4UIIdP idp = idpDAO.find(idpKey);
110         if (idp == null) {
111             LOG.warn("Invalid IdP: {}", idpKey);
112             return List.of();
113         }
114         if (idp.getConnObjectKeyItem().isEmpty()) {
115             LOG.warn("Unable to determine conn object key item for  IdP: {}", idpKey);
116             return Collections.emptyList();
117         }
118         return inboundMatcher.matchByConnObjectKeyValue(
119                 idp.getConnObjectKeyItem().get(), connObjectKeyValue, AnyTypeKind.USER, false, null).stream().
120                 filter(match -> match.getAny() != null).
121                 map(match -> ((User) match.getAny()).getUsername()).
122                 collect(Collectors.toList());
123     }
124 
125     protected List<SAML2SP4UIIdPActions> getActions(final SAML2SP4UIIdP idp) {
126         List<SAML2SP4UIIdPActions> result = new ArrayList<>();
127 
128         idp.getActions().forEach(impl -> {
129             try {
130                 result.add(ImplementationManager.build(
131                         impl,
132                         () -> perContextActions.get(impl.getKey()),
133                         instance -> perContextActions.put(impl.getKey(), instance)));
134             } catch (Exception e) {
135                 LOG.warn("While building {}", impl, e);
136             }
137         });
138 
139         return result;
140     }
141 
142     protected List<Implementation> getTransformers(final Item item) {
143         return item.getTransformers().stream().
144                 map(implementationDAO::find).
145                 filter(Objects::nonNull).
146                 collect(Collectors.toList());
147     }
148 
149     public void fill(final String idpKey, final SAML2LoginResponse loginResponse, final UserTO userTO) {
150         SAML2SP4UIIdP idp = idpDAO.find(idpKey);
151         if (idp == null) {
152             LOG.warn("Invalid IdP: {}", idpKey);
153             return;
154         }
155 
156         idp.getItems().forEach(item -> {
157             List<String> values = List.of();
158             Optional<Attr> samlAttr = loginResponse.getAttr(item.getExtAttrName());
159             if (samlAttr.isPresent() && !samlAttr.get().getValues().isEmpty()) {
160                 values = samlAttr.get().getValues();
161 
162                 List<Object> transformed = new ArrayList<>(values);
163                 for (ItemTransformer transformer : MappingUtils.getItemTransformers(item, getTransformers(item))) {
164                     transformed = transformer.beforePull(null, userTO, transformed);
165                 }
166                 values.clear();
167                 for (Object value : transformed) {
168                     values.add(value.toString());
169                 }
170             }
171 
172             IntAttrName intAttrName = null;
173             try {
174                 intAttrName = intAttrNameParser.parse(item.getIntAttrName(), AnyTypeKind.USER);
175             } catch (ParseException e) {
176                 LOG.error("Invalid intAttrName '{}' specified, ignoring", item.getIntAttrName(), e);
177             }
178 
179             if (intAttrName != null && intAttrName.getField() != null) {
180                 switch (intAttrName.getField()) {
181                     case "username":
182                         if (!values.isEmpty()) {
183                             userTO.setUsername(values.get(0));
184                         }
185                         break;
186 
187                     default:
188                         LOG.warn("Unsupported: {}", intAttrName.getField());
189                 }
190             } else if (intAttrName != null && intAttrName.getSchemaType() != null) {
191                 switch (intAttrName.getSchemaType()) {
192                     case PLAIN:
193                         Optional<Attr> attr = userTO.getPlainAttr(intAttrName.getSchema().getKey());
194                         if (attr.isPresent()) {
195                             attr.get().getValues().clear();
196                         } else {
197                             attr = Optional.of(new Attr.Builder(intAttrName.getSchema().getKey()).build());
198                             userTO.getPlainAttrs().add(attr.get());
199                         }
200                         attr.get().getValues().addAll(values);
201                         break;
202 
203                     default:
204                         LOG.warn("Unsupported: {} {}", intAttrName.getSchemaType(), intAttrName.getSchema().getKey());
205                 }
206             }
207         });
208     }
209 
210     @Transactional(propagation = Propagation.REQUIRES_NEW)
211     public String create(final SAML2SP4UIIdP idp, final SAML2LoginResponse loginResponse, final String nameID) {
212         UserCR userCR = new UserCR();
213         userCR.setStorePassword(false);
214 
215         if (idp.getUserTemplate() != null) {
216             templateUtils.apply(userCR, idp.getUserTemplate().get());
217         }
218 
219         UserTO userTO = new UserTO();
220         fill(idp.getKey(), loginResponse, userTO);
221 
222         Optional.ofNullable(userTO.getUsername()).ifPresent(userCR::setUsername);
223         userCR.getPlainAttrs().addAll(userTO.getPlainAttrs());
224 
225         if (userCR.getRealm() == null) {
226             userCR.setRealm(SyncopeConstants.ROOT_REALM);
227         }
228         if (userCR.getUsername() == null) {
229             userCR.setUsername(nameID);
230         }
231 
232         List<SAML2SP4UIIdPActions> actions = getActions(idp);
233         for (SAML2SP4UIIdPActions action : actions) {
234             userCR = action.beforeCreate(userCR, loginResponse);
235         }
236 
237         Pair<String, List<PropagationStatus>> created =
238                 provisioningManager.create(userCR, false, userCR.getUsername(), SAML2SP_CONTEXT);
239         userTO = binder.getUserTO(created.getKey());
240 
241         for (SAML2SP4UIIdPActions action : actions) {
242             userTO = action.afterCreate(userTO, loginResponse);
243         }
244 
245         return userTO.getUsername();
246     }
247 
248     @Transactional(propagation = Propagation.REQUIRES_NEW)
249     public String update(final String username, final SAML2SP4UIIdP idp, final SAML2LoginResponse loginResponse) {
250         UserTO userTO = binder.getUserTO(userDAO.findKey(username));
251         UserTO original = SerializationUtils.clone(userTO);
252 
253         fill(idp.getKey(), loginResponse, userTO);
254 
255         UserUR userUR = AnyOperations.diff(userTO, original, true);
256 
257         List<SAML2SP4UIIdPActions> actions = getActions(idp);
258         for (SAML2SP4UIIdPActions action : actions) {
259             userUR = action.beforeUpdate(userUR, loginResponse);
260         }
261 
262         Pair<UserUR, List<PropagationStatus>> updated =
263                 provisioningManager.update(userUR, false, userTO.getUsername(), SAML2SP_CONTEXT);
264         userTO = binder.getUserTO(updated.getLeft().getKey());
265 
266         for (SAML2SP4UIIdPActions action : actions) {
267             userTO = action.afterUpdate(userTO, loginResponse);
268         }
269 
270         return userTO.getUsername();
271     }
272 }