001/*
002 * (C) Copyright 2015 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Vladimir Pasquier <vpasquier@nuxeo.com>
018 */
019
020package org.nuxeo.shibboleth.invitation;
021
022import java.io.Serializable;
023import java.util.ArrayList;
024import java.util.HashMap;
025import java.util.List;
026import java.util.Map;
027
028import com.google.common.base.MoreObjects;
029import com.google.common.collect.BiMap;
030import org.nuxeo.ecm.core.api.DocumentModel;
031import org.nuxeo.ecm.core.api.DocumentModelList;
032import org.nuxeo.ecm.core.api.IdRef;
033import org.nuxeo.ecm.core.api.NuxeoException;
034import org.nuxeo.ecm.core.api.NuxeoPrincipal;
035import org.nuxeo.ecm.core.api.UnrestrictedSessionRunner;
036import org.nuxeo.ecm.core.api.impl.DocumentModelListImpl;
037import org.nuxeo.ecm.core.api.repository.RepositoryManager;
038import org.nuxeo.ecm.core.api.security.ACE;
039import org.nuxeo.ecm.core.api.security.ACL;
040import org.nuxeo.ecm.platform.shibboleth.service.ShibbolethAuthenticationService;
041import org.nuxeo.ecm.platform.usermanager.UserManager;
042import org.nuxeo.ecm.user.invite.UserInvitationService;
043import org.nuxeo.ecm.user.registration.UserRegistrationService;
044import org.nuxeo.runtime.api.Framework;
045import org.nuxeo.runtime.transaction.TransactionHelper;
046import org.nuxeo.usermapper.extension.UserMapper;
047import org.slf4j.Logger;
048import org.slf4j.LoggerFactory;
049
050/**
051 * User mapper for handling user post creation when authenticating with Shibboleth (by invitation)
052 *
053 * @since 7.4
054 */
055public class ShibbolethUserMapper implements UserMapper {
056
057    private static final Logger log = LoggerFactory.getLogger(ShibbolethUserMapper.class);
058
059    public static final String DEFAULT_REGISTRATION = "default_registration";
060
061    protected static String userSchemaName = "user";
062
063    protected static String groupSchemaName = "group";
064
065    protected UserManager userManager;
066
067    @Override
068    public NuxeoPrincipal getOrCreateAndUpdateNuxeoPrincipal(Object userObject) {
069        return getOrCreateAndUpdateNuxeoPrincipal(userObject, true, true, null);
070    }
071
072    protected UserInvitationService fetchService() {
073        return Framework.getService(UserRegistrationService.class);
074    }
075
076    @Override
077    public NuxeoPrincipal getOrCreateAndUpdateNuxeoPrincipal(Object userObject, boolean createIfNeeded, boolean update,
078            Map<String, Serializable> params) {
079
080        // Fetching keys from the shibboleth configuration in nuxeo
081        ShibbolethAuthenticationService shiboService = Framework.getService(ShibbolethAuthenticationService.class);
082        BiMap<String, String> metadata = shiboService.getUserMetadata();
083        String usernameKey = MoreObjects.firstNonNull(metadata.get("username"), "username");
084        String lastNameKey = MoreObjects.firstNonNull(metadata.get("lastName"), "lastName");
085        String firstNameKey = MoreObjects.firstNonNull(metadata.get("firstName"), "firstName");
086        String emailKey = MoreObjects.firstNonNull(metadata.get("email"), "email");
087        String companyKey = MoreObjects.firstNonNull(metadata.get("company"), "company");
088        String passwordKey = MoreObjects.firstNonNull(metadata.get("password"), "password");
089
090        String email = (String) ((Map) userObject).get(emailKey);
091        ShibbolethUserInfo userInfo = new ShibbolethUserInfo((String) ((Map) userObject).get(usernameKey),
092                (String) ((Map) userObject).get(passwordKey), (String) ((Map) userObject).get(firstNameKey),
093                (String) ((Map) userObject).get(lastNameKey), (String) ((Map) userObject).get(companyKey), email);
094
095        // Check if email has been provided and if invitation has been assigned to a user with email as username
096        DocumentModel userDoc = null;
097        String userName = userInfo.getUserName();
098        if (email != null && !email.isEmpty()) {
099            userDoc = findUser(userManager.getUserIdField(), email);
100        }
101        if (userDoc != null && userName != null) {
102            DocumentModel finalUserDoc = userDoc; // Effectively final
103            TransactionHelper.runInTransaction(() -> updateACP(userName, email, finalUserDoc));
104        } else {
105            userDoc = Framework.doPrivileged(() -> userManager.getUserModel(userName));
106        }
107        if (userDoc == null) {
108            userDoc = createUser(userInfo);
109        } else {
110            updateUser(userDoc, userInfo);
111        }
112
113        String userId = (String) userDoc.getPropertyValue(userManager.getUserIdField());
114        return userManager.getPrincipal(userId);
115    }
116
117    protected void updateACP(String userName, String email, DocumentModel userDoc) {
118        new UnrestrictedSessionRunner(getTargetRepositoryName()) {
119            @Override
120            public void run() {
121
122                NuxeoPrincipal principal = userManager.getPrincipal(
123                        (String) userDoc.getProperty(userSchemaName, "username"));
124                ArrayList<String> groups = new ArrayList<>(principal.getGroups());
125
126                userManager.deleteUser(userDoc);
127                userDoc.setPropertyValue("user:username", userName);
128                userDoc.setPropertyValue("user:groups", groups);
129                userManager.createUser(userDoc);
130                // Fetching the registrations
131                UserInvitationService userInvitationService = Framework.getService(UserRegistrationService.class);
132                DocumentModelList registrationDocuments = new DocumentModelListImpl();
133                String query = "SELECT * FROM Document WHERE ecm:currentLifeCycleState != 'validated' AND "
134                        + "ecm:mixinType = '"
135                        + userInvitationService.getConfiguration(DEFAULT_REGISTRATION).getRequestDocType() + "' AND "
136                        + userInvitationService.getConfiguration(DEFAULT_REGISTRATION).getUserInfoUsernameField()
137                        + " = '%s' AND ecm:isVersion = 0";
138                query = String.format(query, email);
139                registrationDocuments.addAll(session.query(query));
140                Map<String, DocumentModel> targetDocuments = new HashMap<>();
141                // Fetching the target documents
142                for (DocumentModel doc : registrationDocuments) {
143                    String docId = (String) doc.getPropertyValue("docinfo:documentId");
144                    if (docId != null && !targetDocuments.keySet().contains(docId))
145                        targetDocuments.put(docId, session.getDocument(new IdRef(docId)));
146                }
147                // Update target document ACLs;
148                List<DocumentModel> targetDocs = new ArrayList<>(targetDocuments.values());
149                for (DocumentModel targetDoc : targetDocs) {
150                    for (ACL acl : targetDoc.getACP().getACLs()) {
151                        for (ACE oldACE : acl.getACEs()) {
152                            if (oldACE.getUsername().equals(email)) {
153                                ACE newACE = ACE.builder(userName, oldACE.getPermission())
154                                                .creator(oldACE.getCreator())
155                                                .begin(oldACE.getBegin())
156                                                .end(oldACE.getEnd())
157                                                .build();
158                                session.replaceACE(targetDoc.getRef(), acl.getName(), oldACE, newACE);
159                            }
160                        }
161                    }
162                }
163            }
164        }.runUnrestricted();
165    }
166
167    protected DocumentModel createUser(ShibbolethUserInfo userInfo) {
168        DocumentModel userDoc;
169        try {
170            userDoc = userManager.getBareUserModel();
171            userDoc.setPropertyValue(userManager.getUserIdField(), userInfo.getUserName());
172            userDoc.setPropertyValue(userManager.getUserEmailField(), userInfo.getUserName());
173            Framework.doPrivileged(() -> userManager.createUser(userDoc));
174        } catch (NuxeoException e) {
175            String message = "Error while creating user [" + userInfo.getUserName() + "] in UserManager";
176            log.error(message, e);
177            throw new RuntimeException(message);
178        }
179        return userDoc;
180    }
181
182    @Override
183    public void init(Map<String, String> params) throws Exception {
184        userManager = Framework.getService(UserManager.class);
185        userSchemaName = userManager.getUserSchemaName();
186        groupSchemaName = userManager.getGroupSchemaName();
187    }
188
189    private DocumentModel findUser(String field, String userName) {
190        Map<String, Serializable> query = new HashMap<>();
191        query.put(field, userName);
192        DocumentModelList users = Framework.doPrivileged(() -> userManager.searchUsers(query, null));
193
194        if (users.isEmpty()) {
195            return null;
196        }
197        return users.get(0);
198    }
199
200    private void updateUser(DocumentModel userDoc, ShibbolethUserInfo userInfo) {
201        userDoc.setPropertyValue(userManager.getUserEmailField(), userInfo.getEmail());
202        userDoc.setProperty(userSchemaName, "firstName", userInfo.getFirstName());
203        userDoc.setProperty(userSchemaName, "lastName", userInfo.getLastName());
204        userDoc.setProperty(userSchemaName, "password", userInfo.getPassword());
205        userDoc.setProperty(userSchemaName, "company", userInfo.getCompany());
206        Framework.doPrivileged(() -> userManager.updateUser(userDoc));
207    }
208
209    @Override
210    public Object wrapNuxeoPrincipal(NuxeoPrincipal principal, Object nativePrincipal,
211            Map<String, Serializable> params) {
212        throw new UnsupportedOperationException();
213    }
214
215    @Override
216    public void release() {
217    }
218
219    public String getTargetRepositoryName() {
220        return Framework.getService(RepositoryManager.class).getDefaultRepositoryName();
221    }
222}