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