001/**
002 * Copyright 2005-2017 The Kuali Foundation
003 *
004 * Licensed under the Educational Community 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.opensource.org/licenses/ecl2.php
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 */
016package org.kuali.rice.kew.doctype.service.impl;
017
018import org.apache.commons.collections.CollectionUtils;
019import org.apache.commons.lang.StringUtils;
020import org.apache.commons.lang.builder.EqualsBuilder;
021import org.apache.commons.lang.builder.HashCodeBuilder;
022import org.kuali.rice.core.api.CoreConstants;
023import org.kuali.rice.core.api.datetime.DateTimeService;
024import org.kuali.rice.core.api.reflect.ObjectDefinition;
025import org.kuali.rice.core.api.resourceloader.GlobalResourceLoader;
026import org.kuali.rice.core.api.util.KeyValue;
027import org.kuali.rice.kew.api.KewApiServiceLocator;
028import org.kuali.rice.kew.api.WorkflowRuntimeException;
029import org.kuali.rice.kew.api.document.Document;
030import org.kuali.rice.kew.api.document.search.DocumentSearchResult;
031import org.kuali.rice.kew.api.document.search.DocumentSearchResults;
032import org.kuali.rice.kew.api.extension.ExtensionDefinition;
033import org.kuali.rice.kew.api.extension.ExtensionRepositoryService;
034import org.kuali.rice.kew.doctype.DocumentTypeSecurity;
035import org.kuali.rice.kew.framework.KewFrameworkServiceLocator;
036import org.kuali.rice.kew.framework.document.security.DocumentSecurityDirective;
037import org.kuali.rice.kew.framework.document.security.DocumentSecurityHandlerService;
038import org.kuali.rice.kew.framework.document.security.DocumentSecurityAttribute;
039import org.kuali.rice.kew.doctype.SecurityPermissionInfo;
040import org.kuali.rice.kew.doctype.SecuritySession;
041import org.kuali.rice.kew.doctype.bo.DocumentType;
042import org.kuali.rice.kew.doctype.service.DocumentSecurityService;
043import org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue;
044import org.kuali.rice.kew.service.KEWServiceLocator;
045import org.kuali.rice.kew.user.UserUtils;
046import org.kuali.rice.kew.api.KewApiConstants;
047import org.kuali.rice.kim.api.group.Group;
048import org.kuali.rice.kim.api.services.KimApiServiceLocator;
049import org.springframework.util.LinkedMultiValueMap;
050import org.springframework.util.MultiValueMap;
051
052import java.lang.reflect.Field;
053import java.util.ArrayList;
054import java.util.Calendar;
055import java.util.Collection;
056import java.util.Collections;
057import java.util.HashMap;
058import java.util.HashSet;
059import java.util.Iterator;
060import java.util.List;
061import java.util.Map;
062import java.util.Set;
063
064public class DocumentSecurityServiceImpl implements DocumentSecurityService {
065
066    public static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(
067            DocumentSecurityServiceImpl.class);
068
069    private ExtensionRepositoryService extensionRepositoryService;
070
071    @Override
072    public boolean routeLogAuthorized(String principalId, DocumentRouteHeaderValue routeHeader,
073            SecuritySession securitySession) {
074        Document document = DocumentRouteHeaderValue.to(routeHeader);
075        if(document != null) {
076            Set<String> authorizationResults = checkAuthorizations(principalId, securitySession, Collections.singletonList(document));
077            return authorizationResults.contains(routeHeader.getDocumentId());
078        } else {
079            return false;
080        }
081    }
082
083    @Override
084    public Set<String> documentSearchResultAuthorized(String principalId, DocumentSearchResults results,
085            SecuritySession securitySession) {
086        List<Document> documents = new ArrayList<Document>();
087        for (DocumentSearchResult result : results.getSearchResults()) {
088            documents.add(result.getDocument());
089        }
090        return checkAuthorizations(principalId, securitySession, documents);
091    }
092
093    protected Set<String> checkAuthorizations(String principalId, SecuritySession securitySession,
094            List<Document> documents) {
095        Set<String> authorizations = new HashSet<String>();
096        // a list of documents which need to be processed with security extension attributes after the standard set of
097        // security has been attempted
098        List<Document> documentsRequiringExtensionProcessing = new ArrayList<Document>();
099        boolean admin = isAdmin(securitySession);
100        for (Document document : documents) {
101            if (admin) {
102                authorizations.add(document.getDocumentId());
103                continue;
104            }
105            DocumentTypeSecurity security = null;
106            try {
107                security = getDocumentTypeSecurity(document.getDocumentTypeName(), securitySession);
108                if (security == null || !security.isActive() || checkStandardAuthorization(security, principalId,
109                        document, securitySession)) {
110                    authorizations.add(document.getDocumentId());
111                } else {
112                    // if we get to this point, it means we aren't authorized yet, last chance for authorization will be
113                    // security extension attributes, so prepare for execution of those after the main loop is complete
114                    if (CollectionUtils.isNotEmpty(security.getSecurityAttributeExtensionNames())) {
115                        documentsRequiringExtensionProcessing.add(document);
116                    }
117                }
118            } catch (Exception e) {
119                LOG.warn(
120                        "Not able to retrieve DocumentTypeSecurity from remote system for documentTypeName: " + document
121                                .getDocumentTypeName(), e);
122                continue;
123            }
124        }
125        processDocumentRequiringExtensionProcessing(documentsRequiringExtensionProcessing, securitySession,
126                authorizations);
127        return authorizations;
128    }
129
130    protected void processDocumentRequiringExtensionProcessing(List<Document> documentsRequiringExtensionProcessing,
131            SecuritySession securitySession, Set<String> authorizations) {
132        if (CollectionUtils.isNotEmpty(documentsRequiringExtensionProcessing)) {
133            LOG.info("Beginning processing of documents requiring extension processing (total: "
134                    + documentsRequiringExtensionProcessing.size()
135                    + " documents)");
136            long start = System.currentTimeMillis();
137            MultiValueMap<PartitionKey, Document> partitions = partitionDocumentsForSecurity(
138                    documentsRequiringExtensionProcessing, securitySession);
139            MultiValueMap<String, DocumentSecurityDirective> applicationSecurityDirectives =
140                    new LinkedMultiValueMap<String, DocumentSecurityDirective>();
141            for (PartitionKey partitionKey : partitions.keySet()) {
142                DocumentSecurityDirective directive = DocumentSecurityDirective.create(
143                        partitionKey.getDocumentSecurityAttributeNameList(), partitions.get(partitionKey));
144                applicationSecurityDirectives.add(partitionKey.applicationId, directive);
145            }
146            for (String applicationId : applicationSecurityDirectives.keySet()) {
147                List<DocumentSecurityDirective> documentSecurityDirectives = applicationSecurityDirectives.get(
148                        applicationId);
149                DocumentSecurityHandlerService securityHandler = loadSecurityHandler(applicationId);
150                List<String> authorizedDocumentIds = securityHandler.getAuthorizedDocumentIds(
151                        securitySession.getPrincipalId(), documentSecurityDirectives);
152                if (CollectionUtils.isNotEmpty(authorizedDocumentIds)) {
153                    authorizations.addAll(authorizedDocumentIds);
154                }
155            }
156            long end = System.currentTimeMillis();
157            LOG.info("Finished processing of documents requiring extension processing (total time: "
158                    + (start - end)
159                    + ")");
160        }
161    }
162
163    protected MultiValueMap<PartitionKey, Document> partitionDocumentsForSecurity(List<Document> documents,
164            SecuritySession securitySession) {
165        MultiValueMap<PartitionKey, Document> partitions = new LinkedMultiValueMap<PartitionKey, Document>();
166        for (Document document : documents) {
167            DocumentTypeSecurity security = getDocumentTypeSecurity(document.getDocumentTypeName(), securitySession);
168            MultiValueMap<String, ExtensionDefinition> securityAttributeExtensionDefinitions = loadExtensionDefinitions(
169                    security, securitySession);
170            for (String applicationId : securityAttributeExtensionDefinitions.keySet()) {
171                List<ExtensionDefinition> extensionDefinitions = securityAttributeExtensionDefinitions.get(
172                        applicationId);
173                PartitionKey key = new PartitionKey(applicationId, extensionDefinitions);
174                partitions.add(key, document);
175            }
176        }
177        return partitions;
178    }
179
180    protected MultiValueMap<String, ExtensionDefinition> loadExtensionDefinitions(DocumentTypeSecurity security,
181            SecuritySession securitySession) {
182        MultiValueMap<String, ExtensionDefinition> securityAttributeExtensionDefinitions =
183                new LinkedMultiValueMap<String, ExtensionDefinition>();
184        List<String> securityAttributeExtensionNames = security.getSecurityAttributeExtensionNames();
185        for (String securityAttributeExtensionName : securityAttributeExtensionNames) {
186            ExtensionDefinition extensionDefinition = extensionRepositoryService.getExtensionByName(
187                    securityAttributeExtensionName);
188            securityAttributeExtensionDefinitions.add(extensionDefinition.getApplicationId(), extensionDefinition);
189        }
190        return securityAttributeExtensionDefinitions;
191    }
192
193    protected DocumentSecurityHandlerService loadSecurityHandler(String applicationId) {
194        DocumentSecurityHandlerService service = KewFrameworkServiceLocator.getDocumentSecurityHandlerService(
195                applicationId);
196        if (service == null) {
197            throw new WorkflowRuntimeException(
198                    "Failed to locate DocumentSecurityHandlerService for applicationId: " + applicationId);
199        }
200        return service;
201    }
202
203    protected boolean isAdmin(SecuritySession session) {
204        if (session.getPrincipalId() == null) {
205            return false;
206        }
207        return KimApiServiceLocator.getPermissionService().isAuthorized(session.getPrincipalId(),
208                KewApiConstants.KEW_NAMESPACE, KewApiConstants.PermissionNames.UNRESTRICTED_DOCUMENT_SEARCH, new HashMap<String, String>());
209    }
210
211    protected boolean checkStandardAuthorization(DocumentTypeSecurity security, String principalId, Document document,
212            SecuritySession securitySession) {
213        String documentId = document.getDocumentId();
214        String initiatorPrincipalId = document.getInitiatorPrincipalId();
215
216        LOG.debug("auth check user=" + principalId + " docId=" + documentId);
217
218        // Doc Initiator Authorization
219        if (security.getInitiatorOk() != null && security.getInitiatorOk()) {
220            boolean isInitiator = StringUtils.equals(initiatorPrincipalId, principalId);
221            if (isInitiator) {
222                return true;
223            }
224        }
225
226        // Permission Authorization
227        List<SecurityPermissionInfo> securityPermissions = security.getPermissions();
228        if (securityPermissions != null) {
229            for (SecurityPermissionInfo securityPermission : securityPermissions) {
230                if (isAuthenticatedByPermission(documentId, securityPermission.getPermissionNamespaceCode(),
231                        securityPermission.getPermissionName(), securityPermission.getPermissionDetails(),
232                        securityPermission.getQualifications(), securitySession)) {
233                    return true;
234                }
235            }
236        }
237
238        //  Group Authorization
239        List<Group> securityWorkgroups = security.getWorkgroups();
240        if (securityWorkgroups != null) {
241            for (Group securityWorkgroup : securityWorkgroups) {
242                if (isGroupAuthenticated(securityWorkgroup.getNamespaceCode(), securityWorkgroup.getName(),
243                        securitySession)) {
244                    return true;
245                }
246            }
247        }
248
249        // Searchable Attribute Authorization
250        Collection searchableAttributes = security.getSearchableAttributes();
251        if (searchableAttributes != null) {
252            for (Iterator iterator = searchableAttributes.iterator(); iterator.hasNext(); ) {
253                KeyValue searchableAttr = (KeyValue) iterator.next();
254                String attrName = searchableAttr.getKey();
255                String idType = searchableAttr.getValue();
256                String idValue = UserUtils.getIdValue(idType, principalId);
257                if (!StringUtils.isEmpty(idValue)) {
258                    if (KEWServiceLocator.getRouteHeaderService().hasSearchableAttributeValue(documentId, attrName,
259                            idValue)) {
260                        return true;
261                    }
262                }
263            }
264        }
265
266        // Route Log Authorization
267        if (security.getRouteLogAuthenticatedOk() != null && security.getRouteLogAuthenticatedOk()) {
268            boolean isInitiator = StringUtils.equals(initiatorPrincipalId, principalId);
269            if (isInitiator) {
270                return true;
271            }
272            boolean hasTakenAction = KEWServiceLocator.getActionTakenService().hasUserTakenAction(principalId,
273                    documentId);
274            if (hasTakenAction) {
275                return true;
276            }
277            boolean hasRequest = KEWServiceLocator.getActionRequestService().doesPrincipalHaveRequest(principalId,
278                    documentId);
279            if (hasRequest) {
280                return true;
281            }
282        }
283
284        // local security attribute authorization
285        List<DocumentSecurityAttribute> immediateSecurityAttributes = getImmediateSecurityAttributes(document, security,
286                securitySession);
287        if (immediateSecurityAttributes != null) {
288            for (DocumentSecurityAttribute immediateSecurityAttribute : immediateSecurityAttributes) {
289                boolean isAuthorized = immediateSecurityAttribute.isAuthorizedForDocument(principalId, document);
290                if (isAuthorized) {
291                    return true;
292                }
293            }
294        }
295
296        LOG.debug("user not authorized");
297        return false;
298    }
299
300    protected List<DocumentSecurityAttribute> getImmediateSecurityAttributes(Document document, DocumentTypeSecurity security,
301            SecuritySession securitySession) {
302        List<DocumentSecurityAttribute> securityAttributes = new ArrayList<DocumentSecurityAttribute>();
303        for (String securityAttributeClassName : security.getSecurityAttributeClassNames()) {
304            DocumentSecurityAttribute securityAttribute = securitySession.getSecurityAttributeForClass(
305                    securityAttributeClassName);
306            if (securityAttribute == null) {
307                securityAttribute = GlobalResourceLoader.getObject(new ObjectDefinition(securityAttributeClassName));
308                securitySession.setSecurityAttributeForClass(securityAttributeClassName, securityAttribute);
309            }
310            securityAttributes.add(securityAttribute);
311        }
312        return securityAttributes;
313    }
314
315    protected DocumentTypeSecurity getDocumentTypeSecurity(String documentTypeName, SecuritySession session) {
316        DocumentTypeSecurity security = session.getDocumentTypeSecurity().get(documentTypeName);
317        if (security == null) {
318            DocumentType docType = KEWServiceLocator.getDocumentTypeService().findByName(documentTypeName);
319            if (docType != null) {
320                security = docType.getDocumentTypeSecurity();
321                session.getDocumentTypeSecurity().put(documentTypeName, security);
322            }
323        }
324        return security;
325    }
326
327    protected boolean isGroupAuthenticated(String namespace, String groupName, SecuritySession session) {
328        String key = namespace.trim() + KewApiConstants.KIM_GROUP_NAMESPACE_NAME_DELIMITER_CHARACTER + groupName.trim();
329        Boolean existingAuth = session.getAuthenticatedWorkgroups().get(key);
330        if (existingAuth != null) {
331            return existingAuth;
332        }
333        boolean memberOfGroup = isMemberOfGroupWithName(namespace, groupName, session.getPrincipalId());
334        session.getAuthenticatedWorkgroups().put(key, memberOfGroup);
335        return memberOfGroup;
336    }
337
338    private boolean isMemberOfGroupWithName(String namespace, String groupName, String principalId) {
339        for (Group group : KimApiServiceLocator.getGroupService().getGroupsByPrincipalId(principalId)) {
340            if (StringUtils.equals(namespace, group.getNamespaceCode()) && StringUtils.equals(groupName,
341                    group.getName())) {
342                return true;
343            }
344        }
345        return false;
346    }
347
348    protected boolean isAuthenticatedByPermission(String documentId, String permissionNamespaceCode,
349            String permissionName, Map<String, String> permissionDetails, Map<String, String> qualification,
350            SecuritySession session) {
351
352        Document document;
353        try {
354            document = KewApiServiceLocator.getWorkflowDocumentService().getDocument(documentId);
355
356            for (String qualificationKey : qualification.keySet()) {
357                String qualificationValue = qualification.get(qualificationKey);
358                String replacementValue = getReplacementString(document, qualificationValue);
359                qualification.put(qualificationKey, replacementValue);
360            }
361
362            for (String permissionDetailKey : permissionDetails.keySet()) {
363                String detailValue = qualification.get(permissionDetailKey);
364                String replacementValue = getReplacementString(document, detailValue);
365                qualification.put(permissionDetailKey, replacementValue);
366            }
367        } catch (Exception e) {
368            LOG.error(e.getMessage(), e);
369            return false;
370        }
371        return KimApiServiceLocator.getPermissionService().isAuthorized(session.getPrincipalId(),
372                permissionNamespaceCode, permissionName, qualification);
373    }
374
375    private String getReplacementString(Document document, String value) throws Exception {
376        String startsWith = "${document.";
377        String endsWith = "}";
378        if (value.startsWith(startsWith)) {
379            int tokenStart = value.indexOf(startsWith);
380            int tokenEnd = value.indexOf(endsWith, tokenStart + startsWith.length());
381            if (tokenEnd == -1) {
382                throw new RuntimeException("No ending bracket on token in value " + value);
383            }
384            String token = value.substring(tokenStart + startsWith.length(), tokenEnd);
385
386            return getRouteHeaderVariableValue(document, token);
387        }
388        return value;
389
390    }
391
392    private String getRouteHeaderVariableValue(Document document, String variableName) throws Exception {
393        Field field;
394        try {
395            field = document.getClass().getDeclaredField(variableName);
396        } catch (NoSuchFieldException nsfe) {
397            LOG.error("Field '" + variableName + "' not found on Document object.");
398            // instead of raising an exception, return null as a value
399            // this leaves it up to proper permission configuration to fail the check if a field value
400            // is required
401            return null;
402        }
403        field.setAccessible(true);
404        Object fieldValue = field.get(document);
405        Class<?> clazzType = field.getType();
406        if (clazzType.equals(String.class)) {
407            return (String) fieldValue;
408        } else if (clazzType.getName().equals("boolean") || clazzType.getName().equals("java.lang.Boolean")) {
409            if ((Boolean) fieldValue) {
410                return "Y";
411            }
412            return "N";
413        } else if (clazzType.getName().equals("java.util.Calendar")) {
414
415            DateTimeService dateTimeService = GlobalResourceLoader.getService(CoreConstants.Services.DATETIME_SERVICE);
416            return dateTimeService.toDateString(((Calendar) fieldValue).getTime());
417        }
418        return String.valueOf(fieldValue);
419    }
420
421    public ExtensionRepositoryService getExtensionRepositoryService() {
422        return extensionRepositoryService;
423    }
424
425    public void setExtensionRepositoryService(ExtensionRepositoryService extensionRepositoryService) {
426        this.extensionRepositoryService = extensionRepositoryService;
427    }
428
429    /**
430     * Simple class which defines the key of a partition of security attributes associated with an application id.
431     *
432     * <p>This class allows direct field access since it is intended for internal use only.</p>
433     */
434    private static final class PartitionKey {
435        String applicationId;
436        Set<String> documentSecurityAttributeNames;
437
438        PartitionKey(String applicationId, Collection<ExtensionDefinition> extensionDefinitions) {
439            this.applicationId = applicationId;
440            this.documentSecurityAttributeNames = new HashSet<String>();
441            for (ExtensionDefinition extensionDefinition : extensionDefinitions) {
442                this.documentSecurityAttributeNames.add(extensionDefinition.getName());
443            }
444        }
445
446        List<String> getDocumentSecurityAttributeNameList() {
447            return new ArrayList<String>(documentSecurityAttributeNames);
448        }
449
450        @Override
451        public boolean equals(Object o) {
452            if (!(o instanceof PartitionKey)) {
453                return false;
454            }
455            PartitionKey key = (PartitionKey) o;
456            EqualsBuilder builder = new EqualsBuilder();
457            builder.append(applicationId, key.applicationId);
458            builder.append(documentSecurityAttributeNames, key.documentSecurityAttributeNames);
459            return builder.isEquals();
460        }
461
462        @Override
463        public int hashCode() {
464            HashCodeBuilder builder = new HashCodeBuilder();
465            builder.append(applicationId);
466            builder.append(documentSecurityAttributeNames);
467            return builder.hashCode();
468        }
469    }
470
471}