001: /*
002: * Copyright 2004 Outerthought bvba and Schaubroeck nv
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: package org.outerj.daisy.repository.serverimpl.acl;
017:
018: import org.outerj.daisy.repository.RepositoryException;
019: import org.outerj.daisy.repository.Document;
020: import org.outerj.daisy.repository.Repository;
021: import org.outerj.daisy.repository.query.QueryException;
022: import org.outerj.daisy.repository.user.Role;
023: import org.outerj.daisy.repository.acl.*;
024: import org.outerj.daisy.repository.commonimpl.acl.*;
025: import org.outerj.daisy.query.model.Tristate;
026: import org.outerj.daisy.query.model.PredicateExpr;
027: import org.outerj.daisy.query.model.ExprDocData;
028: import org.outerj.daisy.query.EvaluationInfo;
029: import org.outerj.daisy.query.ExtQueryContext;
030:
031: /**
032: * Evaluates ACL's. This code has not directly been put into the AclImpl/AclObjectImpl
033: * classes because those would otherwise have been dependent on code only existing
034: * in the server implementation.
035: */
036: public class AclEvaluator {
037: private AclStrategy aclStrategy;
038: private AclImpl.IntimateAccess aclInt;
039: private AclEvaluationContext aclEvaluationContext;
040: private Repository repository;
041:
042: public AclEvaluator(AclImpl acl, AclStrategy aclStrategy,
043: AclEvaluationContext aclEvaluationContext,
044: Repository repository) {
045: this .aclStrategy = aclStrategy;
046: this .aclInt = acl.getIntimateAccess(aclStrategy);
047: this .aclEvaluationContext = aclEvaluationContext;
048: this .repository = repository;
049: }
050:
051: public boolean hasPotentialWriteAccess(long userId, long[] roleIds,
052: long documentTypeId, long collectionId)
053: throws RepositoryException {
054: if (roleIds.length < 1)
055: throw new RepositoryException(
056: "Checking of potential write access requires at least one role.");
057:
058: try {
059: if (hasRole(roleIds, Role.ADMINISTRATOR)) {
060: return true;
061: }
062:
063: EvaluationInfo evaluationInfo = new EvaluationInfo(
064: new ExtQueryContext(repository));
065:
066: // the results array will contain the ACL evaluation result for each of the given roles
067: boolean[] results = new boolean[roleIds.length]; // initialized to false
068: for (AclObjectImpl object : aclInt.getObjects()) {
069: // Note: later rules overwrite earlier ones
070: checkPotentialWriteAccess(object, results, userId,
071: roleIds, documentTypeId, collectionId,
072: evaluationInfo);
073: }
074:
075: // if the result for at least one role is true, then return true
076: for (boolean result : results)
077: if (result)
078: return true;
079: return false;
080: } catch (Throwable e) {
081: throw new RepositoryException("Error evaluating ACL.", e);
082: }
083: }
084:
085: private void checkPotentialWriteAccess(AclObjectImpl aclObject,
086: boolean[] results, long userId, long[] roleIds,
087: long documentTypeId, long collectionId,
088: EvaluationInfo evaluationInfo) throws RepositoryException {
089: AclObjectImpl.IntimateAccess aclObjectInt = aclObject
090: .getIntimateAccess(aclStrategy);
091: assureExpressionCompiled(aclObject, aclObjectInt);
092: Tristate appliesTo;
093: try {
094: PredicateExpr predicateExpr = (PredicateExpr) aclObjectInt
095: .getCompiledExpression();
096: appliesTo = predicateExpr.appliesTo(new ExprDocData(
097: new DummyDocForAppliesToTest(documentTypeId,
098: collectionId), null), evaluationInfo);
099: } catch (QueryException e) {
100: throw new RepositoryException(
101: "Exception evaluating ACL object expression.", e);
102: }
103:
104: if (appliesTo == Tristate.NO)
105: return;
106:
107: // NOTE: the logic below is similar to that in completeAclInfo(), so if you adjust
108: // it here, keep it in sync over there
109: for (AclEntryImpl entry : aclObjectInt.getEntries()) {
110:
111: for (int r = 0; r < roleIds.length; r++) {
112: boolean relevant = false;
113: if (entry.getSubjectType() == AclSubjectType.EVERYONE) {
114: relevant = true;
115: } else if (entry.getSubjectType() == AclSubjectType.ROLE) {
116: if (roleIds[r] == entry.getSubjectValue())
117: relevant = true;
118: } else if (entry.getSubjectType() == AclSubjectType.USER) {
119: if (userId != -1
120: && userId == entry.getSubjectValue())
121: relevant = true;
122: }
123:
124: if (relevant) {
125: AclActionType entryAction = entry
126: .get(AclPermission.WRITE);
127: // granting applies always, denying only if the object expression surely applies (and not 'maybe')
128: if (entryAction == AclActionType.GRANT) {
129: results[r] = true;
130: } else if (entryAction == AclActionType.DENY
131: && appliesTo == Tristate.YES) {
132: results[r] = false;
133: }
134: }
135: }
136: }
137: }
138:
139: public AclResultInfo getAclInfo(long userId, long[] roleIds,
140: Document document) throws RepositoryException {
141: if (roleIds.length < 1)
142: throw new RepositoryException(
143: "Evaluation of ACL requires at least one role");
144:
145: try {
146: AclResultInfo result = new AclResultInfoImpl(userId,
147: roleIds, document.getId(), document.getBranchId(),
148: document.getLanguageId());
149:
150: if (hasRole(roleIds, Role.ADMINISTRATOR)) {
151: final String message = "granted because role is Administrator (role id "
152: + Role.ADMINISTRATOR + ")";
153: for (AclPermission permission : AclPermission.values())
154: result.set(permission, AclActionType.GRANT,
155: message, message);
156: return result;
157: }
158:
159: if ((document.isPrivate() && userId == -1)
160: || (document.isPrivate() && userId != -1 && document
161: .getOwner() != userId)) {
162: final String message = "denied because document is marked as private";
163: for (AclPermission permission : AclPermission.values())
164: result.set(permission, AclActionType.DENY, message,
165: message);
166: return result;
167: }
168:
169: for (AclPermission permission : AclPermission.values())
170: result.set(permission, AclActionType.DENY,
171: "denied by default", "denied by default");
172:
173: // For the actual ACL evaluation itself, we evaluate it for each role individually and
174: // afterwards merge the results to take the most permissive ones (i.e. if at least one
175: // role allows it, it is allowed, or vice versa, a permission is only denied if all
176: // roles deny it)
177: AclResultInfo[] results = new AclResultInfo[roleIds.length];
178: for (int i = 0; i < results.length; i++)
179: results[i] = result.clone();
180:
181: for (AclObjectImpl object : aclInt.getObjects()) {
182: completeAclInfo(object, results, userId, roleIds,
183: document);
184: }
185:
186: result = merge(results);
187:
188: if (!result.isNonLiveAllowed(AclPermission.READ)
189: && document.isRetired()) {
190: final String message = "cannot read a retired document if only access to live versions is allowed";
191: result.set(AclPermission.READ, AclActionType.DENY,
192: message, message);
193: }
194:
195: if (!result.isFullyAllowed(AclPermission.READ)) {
196: if (result.isAllowed(AclPermission.WRITE)) {
197: final String message = "cannot have write access if no full read access";
198: result.set(AclPermission.WRITE, AclActionType.DENY,
199: message, message);
200: }
201: if (result.isAllowed(AclPermission.PUBLISH)) {
202: final String message = "cannot have publish access if no full read access";
203: result.set(AclPermission.PUBLISH,
204: AclActionType.DENY, message, message);
205: }
206: if (result.isAllowed(AclPermission.DELETE)) {
207: final String message = "cannot have delete access if no write access";
208: result.set(AclPermission.DELETE,
209: AclActionType.DENY, message, message);
210: }
211: }
212:
213: // Note: the document.getId() == null check below is because:
214: // - new documents have no ID yet
215: // - for a new document, one is always the owner
216: // - so one would always be allowed write access for new documents
217: if (document.getId() != null && userId != -1
218: && document.getOwner() == userId) {
219: final String message = "granted because user is owner of the document";
220: result.set(AclPermission.READ, AclActionType.GRANT,
221: message, message);
222: result.set(AclPermission.WRITE, AclActionType.GRANT,
223: message, message);
224: result.set(AclPermission.DELETE, AclActionType.GRANT,
225: message, message);
226: // Note: an owner of a document does not automatically get publish rights, instead
227: // these are determined by the ACL
228: }
229:
230: return result;
231: } catch (Throwable e) {
232: throw new RepositoryException("Error evaluating ACL.", e);
233: }
234: }
235:
236: private void assureExpressionCompiled(AclObject aclObject,
237: AclObjectImpl.IntimateAccess aclObjectInt)
238: throws RepositoryException {
239: if (aclObjectInt.getCompiledExpression() == null) {
240: Object compiledExpr = aclEvaluationContext
241: .compileObjectExpression(aclObject.getObjectExpr(),
242: repository);
243: aclObjectInt.setCompiledExpression(compiledExpr);
244: }
245: }
246:
247: private boolean appliesTo(AclObjectImpl aclObject,
248: AclObjectImpl.IntimateAccess aclObjectInt, Document document)
249: throws RepositoryException {
250: assureExpressionCompiled(aclObject, aclObjectInt);
251: return aclEvaluationContext.checkObjectExpression(aclObjectInt
252: .getCompiledExpression(), document, repository);
253: }
254:
255: private void completeAclInfo(AclObjectImpl aclObject,
256: AclResultInfo[] results, long userId, long[] roleIds,
257: Document document) throws RepositoryException {
258: AclObjectImpl.IntimateAccess aclObjectInt = aclObject
259: .getIntimateAccess(aclStrategy);
260: // NOTE: the logic below is similar to that in hasPotentialWriteAccess(), so if you adjust
261: // it here, keep it in sync over there
262: if (appliesTo(aclObject, aclObjectInt, document)) {
263: for (AclEntryImpl entry : aclObjectInt.getEntries()) {
264:
265: for (int r = 0; r < roleIds.length; r++) {
266: String subjectReason = null;
267: if (entry.getSubjectType() == AclSubjectType.EVERYONE) {
268: subjectReason = "everyone";
269: } else if (entry.getSubjectType() == AclSubjectType.ROLE) {
270: if (roleIds[r] == entry.getSubjectValue())
271: subjectReason = "role is "
272: + entry.getSubjectValue();
273: } else if (entry.getSubjectType() == AclSubjectType.USER) {
274: if (userId != -1
275: && userId == entry.getSubjectValue())
276: subjectReason = "user is " + userId;
277: }
278:
279: if (subjectReason != null) {
280: for (AclPermission permission : AclPermission
281: .values()) {
282: AclActionType entryAction = entry
283: .get(permission);
284: if (entryAction != AclActionType.DO_NOTHING) {
285: // Access details are currently read-permission specific, so makes no
286: // sense to handle them for other permisssions
287: if (permission == AclPermission.READ) {
288: AccessDetails details = entry
289: .getDetails(permission);
290: AccessDetails newDetails;
291: if (details == null) { // grant with no details => full grant
292: newDetails = new AccessDetailsImpl(
293: null,
294: AclActionType.GRANT);
295: } else { // grant with details => merge with previous details
296: newDetails = results[r]
297: .getAccessDetails(permission);
298: if (newDetails == null)
299: newDetails = new AccessDetailsImpl(
300: null,
301: AclActionType.GRANT);
302: newDetails.overwrite(details);
303: }
304: results[r].set(permission,
305: entryAction, newDetails,
306: aclObject.getObjectExpr(),
307: subjectReason);
308: } else {
309: results[r].set(permission,
310: entryAction, aclObject
311: .getObjectExpr(),
312: subjectReason);
313: }
314: }
315: }
316: }
317: }
318: }
319: }
320: }
321:
322: private boolean hasRole(long[] availableRoles, long roleId) {
323: for (long availableRole : availableRoles)
324: if (availableRole == roleId)
325: return true;
326: return false;
327: }
328:
329: /**
330: * Merge the given AclResultInfo's so that the most permissive result is obtained.
331: * @param results array with at least one entry
332: */
333: private AclResultInfo merge(AclResultInfo[] results) {
334: if (results.length == 1)
335: return results[0];
336:
337: AclResultInfo mergedResult = new AclResultInfoImpl(results[0]
338: .getUserId(), results[0].getRoleIds(), results[0]
339: .getDocumentId(), results[0].getBranchId(), results[0]
340: .getLanguageId());
341:
342: for (AclPermission permission : AclPermission.values()) {
343: for (AclResultInfo result : results) {
344: if (result.isAllowed(permission)) {
345: AccessDetails details = result
346: .getAccessDetails(permission);
347: if (details == null) {
348: mergedResult.set(permission,
349: AclActionType.GRANT, result
350: .getObjectExpr(permission),
351: result.getSubjectReason(permission));
352: break;
353: }
354:
355: AccessDetails existingDetails = mergedResult
356: .getAccessDetails(permission);
357: if (existingDetails != null) {
358: details = new AccessDetailsImpl(null, details);
359: details.makeUnion(existingDetails);
360: }
361: mergedResult.set(permission, AclActionType.GRANT,
362: details, result.getObjectExpr(permission),
363: result.getSubjectReason(permission));
364: // if there are details, don't break but search further
365: }
366: }
367: }
368:
369: return mergedResult;
370: }
371:
372: }
|