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 org.nuxeo.ecm.core.api.DocumentModel;
029import org.nuxeo.ecm.core.api.DocumentModelList;
030import org.nuxeo.ecm.core.api.IdRef;
031import org.nuxeo.ecm.core.api.NuxeoException;
032import org.nuxeo.ecm.core.api.NuxeoPrincipal;
033import org.nuxeo.ecm.core.api.UnrestrictedSessionRunner;
034import org.nuxeo.ecm.core.api.impl.DocumentModelListImpl;
035import org.nuxeo.ecm.core.api.repository.RepositoryManager;
036import org.nuxeo.ecm.core.api.security.ACE;
037import org.nuxeo.ecm.core.api.security.ACL;
038import org.nuxeo.ecm.platform.usermanager.UserManager;
039import org.nuxeo.ecm.user.invite.UserInvitationService;
040import org.nuxeo.ecm.user.registration.UserRegistrationService;
041import org.nuxeo.runtime.api.Framework;
042import org.nuxeo.usermapper.extension.UserMapper;
043import org.slf4j.Logger;
044import org.slf4j.LoggerFactory;
045
046/**
047 * User mapper for handling user post creation when authenticating with Shibboleth (by invitation)
048 *
049 * @since 7.4
050 */
051public class ShibbolethUserMapper implements UserMapper {
052
053    private static final Logger log = LoggerFactory.getLogger(ShibbolethUserMapper.class);
054
055
056    public static final String DEFAULT_REGISTRATION = "default_registration";
057
058    protected static String userSchemaName = "user";
059
060    protected static String groupSchemaName = "group";
061
062    protected UserManager userManager;
063
064    @Override
065    public NuxeoPrincipal getOrCreateAndUpdateNuxeoPrincipal(Object userObject) {
066        return getOrCreateAndUpdateNuxeoPrincipal(userObject, true, true, null);
067    }
068
069    protected UserInvitationService fetchService() {
070        return Framework.getLocalService(UserRegistrationService.class);
071    }
072
073    @Override
074    public NuxeoPrincipal getOrCreateAndUpdateNuxeoPrincipal(Object userObject, boolean createIfNeeded, boolean update,
075            Map<String, Serializable> params) {
076
077        String email = (String) ((Map) userObject).get("email");
078        ShibbolethUserInfo userInfo = new ShibbolethUserInfo((String) ((Map) userObject).get("username"),
079                (String) ((Map) userObject).get("password"), (String) ((Map) userObject).get("firstName"),
080                (String) ((Map) userObject).get("lastName"), (String) ((Map) userObject).get("company"), email);
081
082        // Check if email has been provided and if invitation has been assigned to a user with email as username
083        DocumentModel userDoc = null;
084        String userName = userInfo.getUserName();
085        if (email != null && !email.isEmpty()) {
086            userDoc = findUser(email);
087        }
088        if (userDoc != null && userName != null) {
089            updateACP(userName, email, userDoc);
090        } else {
091            userDoc = findUser(userInfo.getUserName());
092        }
093        if (userDoc == null) {
094            userDoc = createUser(userInfo);
095        }
096        // Update user with related infos
097        userDoc = updateUser(userDoc, userInfo);
098
099        String userId = (String) userDoc.getPropertyValue(userManager.getUserIdField());
100        return userManager.getPrincipal(userId);
101    }
102
103    protected void updateACP(String userName, String email, DocumentModel userDoc) {
104        new UnrestrictedSessionRunner(getTargetRepositoryName()) {
105            @Override
106            public void run() {
107                userManager.deleteUser(userDoc);
108                userDoc.setPropertyValue("user:username",userName);
109                userManager.createUser(userDoc);
110                // Fetching the registrations
111                UserInvitationService userInvitationService = Framework.getLocalService(UserRegistrationService.class);
112                DocumentModelList registrationDocuments = new DocumentModelListImpl();
113                String query = "SELECT * FROM Document WHERE ecm:currentLifeCycleState != 'validated' AND "
114                        + "ecm:mixinType = '"
115                        + userInvitationService.getConfiguration(DEFAULT_REGISTRATION).getRequestDocType() + "' AND "
116                        + userInvitationService.getConfiguration(DEFAULT_REGISTRATION).getUserInfoUsernameField()
117                        + " = '%s' AND ecm:isCheckedInVersion = 0";
118                query = String.format(query, email);
119                registrationDocuments.addAll(session.query(query));
120                Map<String, DocumentModel> targetDocuments = new HashMap<>();
121                // Fetching the target documents
122                for (DocumentModel doc : registrationDocuments) {
123                    String docId = (String) doc.getPropertyValue("docinfo:documentId");
124                    if (docId != null && !targetDocuments.keySet().contains(docId))
125                        targetDocuments.put(docId, session.getDocument(new IdRef(docId)));
126                }
127                // Update target document ACLs;
128                List<DocumentModel> targetDocs = new ArrayList<>(targetDocuments.values());
129                for (DocumentModel targetDoc : targetDocs) {
130                    for (ACL acl : targetDoc.getACP().getACLs()) {
131                        for (ACE oldACE : acl.getACEs()) {
132                            if (oldACE.getUsername().equals(email)) {
133                                ACE newACE = ACE.builder(userName, oldACE.getPermission())
134                                                .creator(oldACE.getCreator())
135                                                .begin(oldACE.getBegin())
136                                                .end(oldACE.getEnd())
137                                                .build();
138                                session.replaceACE(targetDoc.getRef(), acl.getName(), oldACE, newACE);
139                            }
140                        }
141                    }
142                }
143            }
144        }.runUnrestricted();
145    }
146
147    protected DocumentModel createUser(ShibbolethUserInfo userInfo) {
148        DocumentModel userDoc;
149        try {
150            userDoc = userManager.getBareUserModel();
151            userDoc.setPropertyValue(userManager.getUserIdField(), userInfo.getUserName());
152            userDoc.setPropertyValue(userManager.getUserEmailField(), userInfo.getUserName());
153            userManager.createUser(userDoc);
154        } catch (NuxeoException e) {
155            String message = "Error while creating user [" + userInfo.getUserName() + "] in UserManager";
156            log.error(message, e);
157            throw new RuntimeException(message);
158        }
159        return userDoc;
160    }
161
162    @Override
163    public void init(Map<String, String> params) throws Exception {
164        userManager = Framework.getLocalService(UserManager.class);
165        userSchemaName = userManager.getUserSchemaName();
166        groupSchemaName = userManager.getGroupSchemaName();
167    }
168
169    private DocumentModel findUser(String userName) {
170        Map<String, Serializable> query = new HashMap<>();
171        query.put(userManager.getUserIdField(), userName);
172        DocumentModelList users = userManager.searchUsers(query, null);
173
174        if (users.isEmpty()) {
175            return null;
176        }
177        return users.get(0);
178    }
179
180    private DocumentModel updateUser(DocumentModel userDoc, ShibbolethUserInfo userInfo) {
181        userDoc.setPropertyValue(userManager.getUserEmailField(), userInfo.getEmail());
182        userDoc.setProperty(userSchemaName, "firstName", userInfo.getFirstName());
183        userDoc.setProperty(userSchemaName, "lastName", userInfo.getLastName());
184        userDoc.setProperty(userSchemaName, "password", userInfo.getPassword());
185        userDoc.setProperty(userSchemaName, "company", userInfo.getCompany());
186        userManager.updateUser(userDoc);
187        return userDoc;
188    }
189
190    @Override
191    public Object wrapNuxeoPrincipal(NuxeoPrincipal principal, Object nativePrincipal, Map<String, Serializable> params) {
192        throw new UnsupportedOperationException();
193    }
194
195    @Override
196    public void release() {
197    }
198
199    public String getTargetRepositoryName() {
200        return Framework.getService(RepositoryManager.class).getDefaultRepositoryName();
201    }
202}