/* * Copyright 2012 Glencoe Software, Inc. All rights reserved. * Use is subject to license terms supplied in LICENSE.txt * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ package ome.security.basic; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.hibernate.Session; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import ome.conditions.ApiUsageException; import ome.conditions.GroupSecurityViolation; import ome.conditions.InternalException; import ome.conditions.SecurityViolation; import ome.model.IObject; import ome.model.internal.Permissions; import ome.model.internal.Permissions.Right; import ome.model.internal.Permissions.Role; import ome.model.meta.ExperimenterGroup; import ome.security.ChmodStrategy; import ome.services.messages.ContextMessage; import ome.services.messages.EventLogMessage; import ome.system.OmeroContext; import ome.tools.hibernate.ExtendedMetadata; import ome.tools.hibernate.SessionFactory; import ome.util.SqlAction; import ome.util.Utils; /** * {@link ChmodStrategy} which only permits modifying * the permissions on groups. * * @author Josh Moore, josh at glencoesoftware.com * @since 4.4 */ public class GroupChmodStrategy implements ChmodStrategy, ApplicationContextAware { /** * States whether or not the permissions passed in have a reduction of * one of the read permissions. If so, then more checks will be needed * during {@link GroupChmodStrategy#check(IObject, Object)}. */ private static class PermDrop { static final Role u = Role.USER; static final Role g = Role.GROUP; static final Role a = Role.WORLD; static final Right r = Right.READ; final Permissions oldPerms; // = trusted.getDetails().getPermissions(); final Permissions newPerms; // = Permissions.parseString(permissions); final boolean reduceGroup; // Dropping world permissions should not incur any issues since // this simply means that external users will no longer be allowed // to log into the group. There data can remain in the group // and still be viewed by other group members. // final boolean reduceWorld; // IGNORED. PermDrop(ExperimenterGroup trusted, String permissions) { oldPerms = trusted.getDetails().getPermissions(); newPerms = Permissions.parseString(permissions); if (!newPerms.isGranted(u, r)) { throw new GroupSecurityViolation("Cannot remove user read: " + trusted); } if (oldPerms.isGranted(g, r) && !newPerms.isGranted(g, r)) { reduceGroup = true; } else { reduceGroup = false; } } boolean found() { return reduceGroup; } } /** * Opaque object passed out to consumers of the * {@link ChmodStrategy#getChecks(IObject, String)} method. When passed * back in, these are responsible for checking what DB state may possible * disallow a chmod to be performed. */ private static class Check { final long groupID; final String perms; final Class<?> k; final String[][] lockChecks; final PermDrop drop; Check(long groupID, String perms, Class<?> k, String[][] lockChecks, PermDrop drop) { this.groupID = groupID; this.perms = perms; this.k = k; this.lockChecks = lockChecks; this.drop = drop; } public Map<String, Long> run(Session session, ExtendedMetadata em) { StringBuilder sb = new StringBuilder(); sb.append("x.details.group.id = "); sb.append(groupID); sb.append(" and "); sb.append("y.details.group.id = "); sb.append(groupID); if (drop.reduceGroup) { sb.append(" and x.details.owner.id <> y.details.owner.id"); } return em.countLocks(session, null, lockChecks, sb.toString()); } } private final static Logger log = LoggerFactory.getLogger(GroupChmodStrategy.class); private final BasicACLVoter voter; private final SessionFactory osf; private final SqlAction sql; private final ExtendedMetadata em; private/* final */OmeroContext ctx; public GroupChmodStrategy(BasicACLVoter voter, SessionFactory osf, SqlAction sql, ExtendedMetadata em) { this.voter = voter; this.osf = osf; this.sql = sql; this.em = em; } public void setApplicationContext(ApplicationContext ctx) throws BeansException { this.ctx = (OmeroContext) ctx; } @SuppressWarnings({ "unchecked", "rawtypes" }) public Object[] getChecks(IObject obj, String permissions) { ExperimenterGroup trusted = load(obj); if (!voter.allowChmod(trusted)) { throw new SecurityViolation("chmod not permitted"); } PermDrop drop = new PermDrop(trusted, permissions); if (!drop.found()) { return new Object[0]; // none needed. } List<Object> checks = new ArrayList<Object>(); Collection<String> classeNames = em.getClasses(); for (String className : classeNames) { Class k = em.getHibernateClass(className); if (voter.sysTypes.isSystemType(k)) { continue; // Skip experimenters, etc. } String[][] lockChecks = em.getLockChecks(k); checks.add(new Check(trusted.getId(), permissions, k, lockChecks, drop)); } return checks.toArray(new Object[checks.size()]); } public void chmod(IObject obj, String permissions) { handleGroupChange(obj, Permissions.parseString(permissions)); } /** * Here we used the checks returned from {@link ExtendedMetadata} to iterate * through every non-system table and check that it has no FKs which point * to back to its rows and violate the read permissions which are being * reduced. */ public void check(IObject obj, Object check) { if (!(check instanceof Check)) { throw new InternalException("Bad check:" + check); } Check c = ((Check) check); Map<String, Long> counts = performRun(c); long total = counts.get("*"); if (total > 0) { throw new SecurityViolation(String.format( "Cannot change permissions on %s to %s due to locks:\n%s", obj, c.perms, counts)); } } private Map<String, Long> performRun(Check c) { // Perform the operation across all groups. Map<String, Long> counts = null; Map<String, String> grpCtx = new HashMap<String, String>(); grpCtx.put("omero.group", "-1"); try { ctx.publishMessage(new ContextMessage.Push(this, grpCtx)); try { counts = c.run(osf.getSession(), em); } finally { ctx.publishMessage(new ContextMessage.Pop(this, grpCtx)); } } catch (Throwable t) { log.error("Could not perform check!", t); throw new InternalException("Could not perform check! See server logs"); } return counts; } // Helpers // ========================================================================= private ExperimenterGroup load(IObject obj) { if (!(obj instanceof ExperimenterGroup)) { throw new SecurityViolation("Only groups allowed"); } if (obj.getId() == null) { throw new ApiUsageException("ID cannot be null"); } final Session s = osf.getSession(); return (ExperimenterGroup) s.get(ExperimenterGroup.class, obj.getId()); } private void handleGroupChange(IObject obj, Permissions newPerms) { final ExperimenterGroup group = load(obj); if (newPerms == null) { throw new ApiUsageException("PERMS cannot be null"); } final Permissions oldPerms = group.getDetails().getPermissions(); if (oldPerms.sameRights(newPerms)) { log.debug(String.format("Ignoring unchanged permissions: %s", newPerms)); return; } final Long internal = (Long) Utils.internalForm(newPerms); sql.changeGroupPermissions(obj.getId(), internal); log.info(String.format("Changed permissions for %s to %s", obj.getId(), internal)); eventlog(obj.getId(), newPerms.toString()); } private void eventlog(long id, String perms) { EventLogMessage elm = new EventLogMessage(this, String.format( "CHMOD(%s)", perms), ExperimenterGroup.class, Collections.singletonList(id)); try { ctx.publishMessage(elm); } catch (Throwable t) { if (t instanceof RuntimeException) { throw (RuntimeException) t; } else { throw new RuntimeException(t); } } } }