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