All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.tentackle.security.DefaultSecurityManager Maven / Gradle / Ivy

/*
 * Tentackle - https://tentackle.org
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package org.tentackle.security;

import org.tentackle.log.Logger;
import org.tentackle.pdo.DomainContext;
import org.tentackle.pdo.Pdo;
import org.tentackle.pdo.PdoListener;
import org.tentackle.security.pdo.Security;
import org.tentackle.security.pdo.SecurityDomain;
import org.tentackle.security.pdo.SecurityPersistence;
import org.tentackle.session.ModificationEvent;
import org.tentackle.session.ModificationListener;
import org.tentackle.session.ModificationTracker;
import org.tentackle.session.Session;
import org.tentackle.session.SessionUtilities;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
import java.util.Objects;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;

/**
 * An ACL-based SecurityManager.
 * 

* The rules themselves are PDOs ({@link Security}) and thus are stored in the database. *

* The list of rules is interpreted as an ACL (access control list). * Whenever a permission is checked, the list is processed * until a rule fires. If no rule fires, the default result is returned, * which is either granted or denied. * * @author harald */ public class DefaultSecurityManager implements SecurityManager { private static final Logger LOGGER = Logger.get(DefaultSecurityManager.class); /** * Security rule wrapper to hold the lazily generated security result. */ private class Rule { private final Security security; private SecurityResult result; public Rule(Security security) { this.security = security; } /** * Gets the cached result. * * @return the result, never null */ public SecurityResult getResult() { if (result == null) { SecurityPersistence spo = (SecurityPersistence) security.getPersistenceDelegate(); // bypass IH to speed up if (spo.isAllowed()) { result = createAcceptedSecurityResult(spo.getMessage(), false); } else { result = createDeniedSecurityResult(spo.getMessage(), false); } } return result; } } /** * key for a non-pdo class-only rule. */ private static class ClassKey implements Comparable { private final int granteeClassId; private final long granteeId; private final String className; private final int priority; // construct the entry public ClassKey(Security sec) { this.granteeClassId = sec.getGranteeClassId(); this.granteeId = sec.getGranteeId(); this.className = sec.getObjectClassName(); this.priority = sec.getPriority(); } // to get the first/last key public ClassKey(GranteeDescriptor grantee, String className, int priority) { this.granteeClassId = grantee.getGranteeClassId(); this.granteeId = grantee.getGranteeId(); this.className = className; this.priority = priority; } @Override public int compareTo(ClassKey k) { int rv = granteeClassId - k.granteeClassId; if (rv == 0) { rv = Long.compare(granteeId, k.granteeId); if (rv == 0) { rv = className.compareTo(k.className); if (rv == 0) { rv = priority - k.priority; } } } return rv; } @Override public int hashCode() { int hash = 3; hash = 67 * hash + granteeClassId; hash = 67 * hash + Long.hashCode(granteeId); hash = 67 * hash + Objects.hashCode(className); hash = 67 * hash + priority; return hash; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final ClassKey other = (ClassKey) obj; if (this.granteeClassId != other.granteeClassId) { return false; } if (this.granteeId != other.granteeId) { return false; } if (this.priority != other.priority) { return false; } return Objects.equals(this.className, other.className); } } /** * key for a pdo rule. */ private static class PdoKey implements Comparable { private final int granteeClassId; private final long granteeId; private final int objectClassId; private final long objectId; private final int priority; // construct the entry public PdoKey(Security sec) { this.granteeClassId = sec.getGranteeClassId(); this.granteeId = sec.getGranteeId(); this.objectClassId = sec.getObjectClassId(); this.objectId = sec.getObjectId(); this.priority = sec.getPriority(); } // to get the first/last key public PdoKey(GranteeDescriptor grantee, int objectClassId, long objectId, int priority) { this.granteeClassId = grantee.getGranteeClassId(); this.granteeId = grantee.getGranteeId(); this.objectClassId = objectClassId; this.objectId = objectId; this.priority = priority; } @Override public int compareTo(PdoKey k) { int rv = granteeClassId - k.granteeClassId; if (rv == 0) { rv = Long.compare(granteeId, k.granteeId); if (rv == 0) { rv = objectClassId - k.objectClassId; if (rv == 0) { rv = Long.compare(objectId, k.objectId); if (rv == 0) { rv = priority - k.priority; } } } } return rv; } @Override public int hashCode() { int hash = 7; hash = 73 * hash + granteeClassId; hash = 73 * hash + Long.hashCode(granteeId); hash = 73 * hash + objectClassId; hash = 73 * hash + Long.hashCode(objectId); hash = 73 * hash + priority; return hash; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final PdoKey other = (PdoKey) obj; if (this.granteeClassId != other.granteeClassId) { return false; } if (this.granteeId != other.granteeId) { return false; } if (this.objectClassId != other.objectClassId) { return false; } if (this.objectId != other.objectId) { return false; } return this.priority == other.priority; } } /** * key for a pdo class-only rule. */ private static class PdoClassKey implements Comparable { private final int granteeClassId; private final long granteeId; private final int objectClassId; private final int priority; // construct the entry public PdoClassKey(Security sec) { this.granteeClassId = sec.getGranteeClassId(); this.granteeId = sec.getGranteeId(); this.objectClassId = sec.getObjectClassId(); this.priority = sec.getPriority(); } // to get the first/last key public PdoClassKey(GranteeDescriptor grantee, int objectClassId, int priority) { this.granteeClassId = grantee.getGranteeClassId(); this.granteeId = grantee.getGranteeId(); this.objectClassId = objectClassId; this.priority = priority; } @Override public int compareTo(PdoClassKey k) { int rv = granteeClassId - k.granteeClassId; if (rv == 0) { rv = Long.compare(granteeId, k.granteeId); if (rv == 0) { rv = objectClassId - k.objectClassId; if (rv == 0) { rv = priority - k.priority; } } } return rv; } @Override public int hashCode() { int hash = 5; hash = 37 * hash + granteeClassId; hash = 37 * hash + Long.hashCode(granteeId); hash = 37 * hash + objectClassId; hash = 37 * hash + priority; return hash; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final PdoClassKey other = (PdoClassKey) obj; if (this.granteeClassId != other.granteeClassId) { return false; } if (this.granteeId != other.granteeId) { return false; } if (this.objectClassId != other.objectClassId) { return false; } return this.priority == other.priority; } } // messages for predefined results private static final String DISABLED_SECURITY_MANAGER_MESSAGE = "security manager is disabled"; private static final String NO_IDENTIFIABLE_RULES_MESSAGE = "no identifiable-rules match"; private static final String NO_CLASS_RULES_MESSAGE = "no class-rules match"; private static final String NO_GRANTEE_MESSAGE = "no grantee"; private final Map> granteeMap; // maps root grantee to grantee descriptors private volatile TreeMap classMap; // non-pdo class rules (immutable map once created) private volatile TreeMap pdoMap; // object rules (immutable map once created) private volatile TreeMap pdoClassMap; // pdo class-only rules (immutable map once created) private volatile boolean invalid; // true if cache needs initialization private boolean enabled; // true if security manager is enabled private boolean acceptByDefault; // true if isAccepted() by default private ModificationListener securityListener; // the listener to invalidate the security manager private final SecurityResult disabledResult; // accept result if sec manager is disabled -> grants all private final SecurityResult identifiableAcceptedResult; // accept result if no identifiable rules found private final SecurityResult identifiableDeniedResult; // denied result if no identifiable rules found private final SecurityResult classAcceptedResult; // accept result if no class rules found private final SecurityResult classDeniedResult; // denied result if no class rules found private final SecurityResult noGranteeResult; // accept result if no grantee (usually the system user) /** * Creates a security manager.
* The manager is disabled by default and must be enabled when the application is started up. * When disabled, all requests are granted, no matter if accept by default or deny by default. */ public DefaultSecurityManager() { invalid = true; acceptByDefault = true; granteeMap = new ConcurrentHashMap<>(); // create some predefined security results disabledResult = createAcceptedSecurityResult(DISABLED_SECURITY_MANAGER_MESSAGE, true); identifiableAcceptedResult = createAcceptedSecurityResult(NO_IDENTIFIABLE_RULES_MESSAGE, true); identifiableDeniedResult = createDeniedSecurityResult(NO_IDENTIFIABLE_RULES_MESSAGE, true); classAcceptedResult = createAcceptedSecurityResult(NO_CLASS_RULES_MESSAGE, true); classDeniedResult = createDeniedSecurityResult(NO_CLASS_RULES_MESSAGE, true); noGranteeResult = createAcceptedSecurityResult(NO_GRANTEE_MESSAGE, true); } @Override public void invalidate() { invalid = true; } @Override public boolean isEnabled() { return enabled; } @Override public void setEnabled(boolean enabled) { this.enabled = enabled; } @Override public boolean isAcceptByDefault() { return acceptByDefault; } @Override public void setAcceptByDefault(boolean acceptByDefault) { this.acceptByDefault = acceptByDefault; } @Override public SecurityResult evaluate(DomainContext context, Permission permission, int objectClassId, long objectId) { SecurityResult result = evaluateImpl(context, permission, objectClassId, objectId, null); if (result == null && objectId != 0) { // no explicit object rule fired: check class rules result = evaluateImpl(context, permission, objectClassId, 0, null); } if (result == null) { result = acceptByDefault ? identifiableAcceptedResult : identifiableDeniedResult; } return result; } @Override public SecurityResult evaluate(DomainContext context, Permission permission, Class clazz) { SecurityResult result = evaluateImpl(context, permission, 0, 0, clazz); if (result == null) { result = acceptByDefault ? classAcceptedResult : classDeniedResult; } return result; } @Override public void removeObsoleteRules(Session session) { long txVoucher = session.begin("removeObsoleteRules"); try { for (Security rule: createSecurityInstance(Pdo.createDomainContext(session)).selectAll()) { if (rule.getObjectId() != 0 && // pdo vanished Pdo.create(SessionUtilities.getInstance().getClassName( rule.getObjectClassId()), session).selectSerial(rule.getObjectId()) == -1 || rule.getGranteeId() != 0 && // grantee vanished Pdo.create(SessionUtilities.getInstance().getClassName( rule.getGranteeClassId()), session).selectSerial(rule.getGranteeId()) == -1) { rule.delete(); } } session.commit(txVoucher); // this will invalidate via listener } catch (RuntimeException ex) { session.rollback(txVoucher); LOGGER.severe("removing rules failed", ex); } } /** * Determines the session grantee. *

* If the grantee is null (for example a system user) all permissions will be granted. * * @param context the domain context * @return the grantee, null if accept all permissions */ protected GranteeDescriptor determineGrantee(DomainContext context) { long granteeId = context.getSessionInfo().getUserId(); if (granteeId <= 0) { return null; // system user -> always ok } int granteeClassId = context.getSessionInfo().getUserClassId(); if (granteeClassId == 0) { throw new SecurityException("missing grantee's class id"); } return new GranteeDescriptor(granteeClassId, granteeId); } /** * Determines the grantees to check.

* Override this method to implement user groups, for example. * * @param context the domain context * @param grantee the session's grantee * @return the grantee descriptors */ protected Collection determineGranteesToCheck(DomainContext context, GranteeDescriptor grantee) { Collection grantees = new ArrayList<>(); // check rules for grantee grantees.add(grantee); // check rules for all grantees.add(new GranteeDescriptor(0, 0)); return grantees; } /** * Creates an accepting security result. * * @param message the message * @param byDefault true if accepted by default * @return the result */ protected SecurityResult createAcceptedSecurityResult(String message, boolean byDefault) { return new DefaultSecurityResult(message, true, byDefault); } /** * Creates a denying security result. * * @param message the message * @param byDefault true if denied by default * @return the result */ protected SecurityResult createDeniedSecurityResult(String message, boolean byDefault) { return new DefaultSecurityResult(message, false, byDefault); } /** * Evaluates a permission for a pdo or regular class. * * @param context the domain context * @param permission the requested permission * @param objectClassId the class id, 0 if non-pdo class rule * @param objectId the object id, 0 for all objects of given class * @param clazz the non-pdo class, null pdo rule * @return the SecurityResult, null if no matching rule found */ protected SecurityResult evaluateImpl(DomainContext context, Permission permission, int objectClassId, long objectId, Class clazz) { if (isEnabled()) { if (context == null) { throw new SecurityException("invalid domain context: null"); } if (permission == null) { throw new SecurityException("invalid permission: null"); } if (objectId < 0) { throw new SecurityException("invalid object ID: " + objectId); } if (objectClassId == 0 && clazz == null) { throw new SecurityException("no class or classId given"); } GranteeDescriptor sessionGrantee = determineGrantee(context); if (sessionGrantee == null) { return noGranteeResult; } if (invalid) { if (context.getSession().isRemote()) { createSecurityInstance(context).assertRemoteSecurityManagerInitialized(); } initialize(context.getSession()); } // determine source map only once (must not change during loop below) TreeMap localPdoMap; TreeMap localClassMap; TreeMap localPdoClassMap; if (clazz != null) { localClassMap = classMap; if (localClassMap.isEmpty()) { return null; } localPdoMap = null; localPdoClassMap = null; } else if (objectId == 0) { localPdoClassMap = pdoClassMap; if (localPdoClassMap.isEmpty()) { return null; } localPdoMap = null; localClassMap = null; } else { localPdoMap = pdoMap; if (localPdoMap.isEmpty()) { return null; } localClassMap = null; localPdoClassMap = null; } boolean fineLoggable = LOGGER.isFineLoggable(); // a little speedup Collection granteesToCheck = granteeMap.computeIfAbsent(sessionGrantee, g -> determineGranteesToCheck(context, g)); for (GranteeDescriptor grantee : granteesToCheck) { if (fineLoggable) { LOGGER.fine("Checking grantee={0}[{1}], context={2}, permission={3}, object={4}[{5}], class={6}", sessionGrantee.getGranteeClassId(), sessionGrantee.getGranteeId(), context, permission, objectClassId, objectId, clazz); } // the map of applicable security rules SortedMap map; if (localClassMap != null) { map = localClassMap.subMap(new ClassKey(grantee, clazz.getName(), 0), new ClassKey(grantee, clazz.getName(), Integer.MAX_VALUE)); } else if (localPdoClassMap != null) { map = localPdoClassMap.subMap(new PdoClassKey(grantee, objectClassId, 0), new PdoClassKey(grantee, objectClassId, Integer.MAX_VALUE)); } else { map = localPdoMap.subMap(new PdoKey(grantee, objectClassId, objectId, 0), new PdoKey(grantee, objectClassId, objectId, Integer.MAX_VALUE)); } // walk through the security settings of the map // they are sorted by priority! for (Rule rule : map.values()) { Security sec = rule.security; if (fineLoggable) { LOGGER.fine("evaluate {0}", sec); } // check if rule fires (bypassing IH for speed) if (((SecurityDomain) sec.getDomainDelegate()).evaluate(context, permission)) { if (fineLoggable) { LOGGER.fine(sec.isAllowed() ? "-> ACCEPT" : "-> DENY"); } return rule.getResult(); } } } return null; } return disabledResult; } /** * Initializes the security manager. * * @param session the session to load the security rules */ protected synchronized void initialize(Session session) { if (invalid) { // double check (invalid is volatile) // new instances to allow walking along the subMap during init (see evaluateImpl above) TreeMap newClassMap = new TreeMap<>(); TreeMap newPdoMap = new TreeMap<>(); TreeMap newPdoClassMap = new TreeMap<>(); // add all rules Collection secRules = createSecurityInstance(Pdo.createDomainContext(session)).selectAllCached(); for (Security sec : secRules) { Rule rule = new Rule(sec); if (sec.getObjectClassId() == 0) { newClassMap.put(new ClassKey(sec), rule); } else { if (sec.getObjectId() == 0) { newPdoClassMap.put(new PdoClassKey(sec), rule); } else { newPdoMap.put(new PdoKey(sec), rule); } } } if (securityListener == null) { // register listener once securityListener = createModificationListener(); ModificationTracker.getInstance().addModificationListener(securityListener); } // switch to new maps, "invalid" flag cleared below! classMap = newClassMap; pdoMap = newPdoMap; pdoClassMap = newPdoClassMap; invalid = false; LOGGER.fine("security manager initialized"); } } /** * Creates the modification listener. * * @return the listener */ protected ModificationListener createModificationListener() { return new PdoListener(Security.class) { @Override public void dataChanged(ModificationEvent ev) { invalidate(); } }; } /** * Creates a security instance. * * @param context the domain context of the security instance * @return the security instance */ protected Security createSecurityInstance(DomainContext context) { return Pdo.create(Security.class, context); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy