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.base.Objects;
030import com.google.common.collect.BiMap;
031import org.nuxeo.ecm.core.api.DocumentModel;
032import org.nuxeo.ecm.core.api.DocumentModelList;
033import org.nuxeo.ecm.core.api.IdRef;
034import org.nuxeo.ecm.core.api.NuxeoException;
035import org.nuxeo.ecm.core.api.NuxeoPrincipal;
036import org.nuxeo.ecm.core.api.UnrestrictedSessionRunner;
037import org.nuxeo.ecm.core.api.impl.DocumentModelListImpl;
038import org.nuxeo.ecm.core.api.repository.RepositoryManager;
039import org.nuxeo.ecm.core.api.security.ACE;
040import org.nuxeo.ecm.core.api.security.ACL;
041import org.nuxeo.ecm.platform.shibboleth.service.ShibbolethAuthenticationService;
042import org.nuxeo.ecm.platform.usermanager.UserManager;
043import org.nuxeo.ecm.user.invite.UserInvitationService;
044import org.nuxeo.ecm.user.registration.UserRegistrationService;
045import org.nuxeo.runtime.api.Framework;
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.getLocalService(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.getUserEmailField(), email);
100        }
101        if (userDoc != null && userName != null) {
102            updateACP(userName, email, userDoc);
103        } else {
104            userDoc = findUser(userManager.getUserIdField(), userInfo.getUserName());
105        }
106        if (userDoc == null) {
107            userDoc = createUser(userInfo);
108        } else {
109            userDoc = updateUser(userDoc, userInfo);
110        }
111
112        String userId = (String) userDoc.getPropertyValue(userManager.getUserIdField());
113        return userManager.getPrincipal(userId);
114    }
115
116    protected void updateACP(String userName, String email, DocumentModel userDoc) {
117        new UnrestrictedSessionRunner(getTargetRepositoryName()) {
118            @Override
119            public void run() {
120
121                NuxeoPrincipal principal = userManager.getPrincipal(
122                        (String) userDoc.getProperty(userSchemaName, "username"));
123                ArrayList<String> groups = new ArrayList<>(principal.getGroups());
124
125                userManager.deleteUser(userDoc);
126                userDoc.setPropertyValue("user:username", userName);
127                userDoc.setPropertyValue("user:groups", groups);
128                userManager.createUser(userDoc);
129                // Fetching the registrations
130                UserInvitationService userInvitationService = Framework.getLocalService(UserRegistrationService.class);
131                DocumentModelList registrationDocuments = new DocumentModelListImpl();
132                String query = "SELECT * FROM Document WHERE ecm:currentLifeCycleState != 'validated' AND "
133                        + "ecm:mixinType = '"
134                        + userInvitationService.getConfiguration(DEFAULT_REGISTRATION).getRequestDocType() + "' AND "
135                        + userInvitationService.getConfiguration(DEFAULT_REGISTRATION).getUserInfoUsernameField()
136                        + " = '%s' AND ecm:isCheckedInVersion = 0";
137                query = String.format(query, email);
138                registrationDocuments.addAll(session.query(query));
139                Map<String, DocumentModel> targetDocuments = new HashMap<>();
140                // Fetching the target documents
141                for (DocumentModel doc : registrationDocuments) {
142                    String docId = (String) doc.getPropertyValue("docinfo:documentId");
143                    if (docId != null && !targetDocuments.keySet().contains(docId))
144                        targetDocuments.put(docId, session.getDocument(new IdRef(docId)));
145                }
146                // Update target document ACLs;
147                List<DocumentModel> targetDocs = new ArrayList<>(targetDocuments.values());
148                for (DocumentModel targetDoc : targetDocs) {
149                    for (ACL acl : targetDoc.getACP().getACLs()) {
150                        for (ACE oldACE : acl.getACEs()) {
151                            if (oldACE.getUsername().equals(email)) {
152                                ACE newACE = ACE.builder(userName, oldACE.getPermission())
153                                                .creator(oldACE.getCreator())
154                                                .begin(oldACE.getBegin())
155                                                .end(oldACE.getEnd())
156                                                .build();
157                                session.replaceACE(targetDoc.getRef(), acl.getName(), oldACE, newACE);
158                            }
159                        }
160                    }
161                }
162            }
163        }.runUnrestricted();
164    }
165
166    protected DocumentModel createUser(ShibbolethUserInfo userInfo) {
167        DocumentModel userDoc;
168        try {
169            userDoc = userManager.getBareUserModel();
170            userDoc.setPropertyValue(userManager.getUserIdField(), userInfo.getUserName());
171            userDoc.setPropertyValue(userManager.getUserEmailField(), userInfo.getUserName());
172            userManager.createUser(userDoc);
173        } catch (NuxeoException e) {
174            String message = "Error while creating user [" + userInfo.getUserName() + "] in UserManager";
175            log.error(message, e);
176            throw new RuntimeException(message);
177        }
178        return userDoc;
179    }
180
181    @Override
182    public void init(Map<String, String> params) throws Exception {
183        userManager = Framework.getLocalService(UserManager.class);
184        userSchemaName = userManager.getUserSchemaName();
185        groupSchemaName = userManager.getGroupSchemaName();
186    }
187
188    private DocumentModel findUser(String field, String userName) {
189        Map<String, Serializable> query = new HashMap<>();
190        query.put(field, userName);
191        DocumentModelList users = userManager.searchUsers(query, null);
192
193        if (users.isEmpty()) {
194            return null;
195        }
196        return users.get(0);
197    }
198
199    private DocumentModel updateUser(DocumentModel userDoc, ShibbolethUserInfo userInfo) {
200        userDoc.setPropertyValue(userManager.getUserEmailField(), userInfo.getEmail());
201        userDoc.setProperty(userSchemaName, "firstName", userInfo.getFirstName());
202        userDoc.setProperty(userSchemaName, "lastName", userInfo.getLastName());
203        userDoc.setProperty(userSchemaName, "password", userInfo.getPassword());
204        userDoc.setProperty(userSchemaName, "company", userInfo.getCompany());
205        userManager.updateUser(userDoc);
206        return 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}