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