/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.openejb.core.security; import org.apache.openejb.BeanContext; import org.apache.openejb.InterfaceType; import org.apache.openejb.api.resource.DestroyableResource; import org.apache.openejb.core.ThreadContext; import org.apache.openejb.core.ThreadContextListener; import org.apache.openejb.core.security.jaas.GroupPrincipal; import org.apache.openejb.core.security.jacc.BasicJaccProvider; import org.apache.openejb.core.security.jacc.BasicPolicyConfiguration; import org.apache.openejb.loader.SystemInstance; import org.apache.openejb.spi.CallerPrincipal; import org.apache.openejb.spi.SecurityService; import org.apache.openejb.util.JavaSecurityManagers; import javax.security.auth.Subject; import javax.security.auth.login.LoginException; import javax.security.jacc.EJBMethodPermission; import javax.security.jacc.PolicyConfigurationFactory; import javax.servlet.http.HttpServletRequest; import java.io.Serializable; import java.lang.reflect.Method; import java.security.AccessControlContext; import java.security.AccessControlException; import java.security.AccessController; import java.security.Policy; import java.security.Principal; import java.security.PrivilegedAction; import java.util.ArrayList; import java.util.Collections; import java.util.Enumeration; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; /** * This security service chooses a UUID as its token as this can be serialized * to clients, is mostly secure, and can be deserialized in a client vm without * addition openejb-core classes. */ public abstract class AbstractSecurityService implements DestroyableResource, SecurityService<UUID>, ThreadContextListener, BasicPolicyConfiguration.RoleResolver { private static final Map<Object, Identity> identities = new ConcurrentHashMap<Object, Identity>(); protected static final ThreadLocal<Identity> clientIdentity = new ThreadLocal<Identity>(); protected String defaultUser = "guest"; private String realmName = "PropertiesLogin"; protected Subject defaultSubject; protected SecurityContext defaultContext; public AbstractSecurityService() { this(autoJaccProvider()); } public AbstractSecurityService(final String jaccProvider) { JavaSecurityManagers.setSystemProperty(JaccProvider.class.getName(), jaccProvider); installJacc(); ThreadContext.addThreadContextListener(this); // set the default subject and the default context updateSecurityContext(); SystemInstance.get().setComponent(BasicPolicyConfiguration.RoleResolver.class, this); } @Override public void destroyResource() { // no-op } public void onLogout(final HttpServletRequest request) { clientIdentity.remove(); } public String getRealmName() { return realmName; } public void setRealmName(final String realmName) { this.realmName = realmName; } /** * @return the defaultUser */ public String getDefaultUser() { return defaultUser; } /** * @param defaultUser the defaultUser to set */ public void setDefaultUser(final String defaultUser) { this.defaultUser = defaultUser; // set the default subject and the default context for the new default user updateSecurityContext(); } // update the current subject and security context private void updateSecurityContext() { defaultSubject = createSubject(defaultUser, defaultUser); defaultContext = new SecurityContext(defaultSubject); } @Override public void init(final Properties props) throws Exception { } @Override public UUID login(final String username, final String password) throws LoginException { return login(realmName, username, password); } @Override public Set<String> getLogicalRoles(final Principal[] principals, final Set<String> logicalRoles) { final LinkedHashSet<String> roles = new LinkedHashSet<String>(principals.length); for (final Principal principal : principals) { final String name = principal.getName(); if (logicalRoles.contains(name)) { roles.add(name); } } return roles; } @Override public void contextEntered(final ThreadContext oldContext, final ThreadContext newContext) { final String moduleID = newContext.getBeanContext().getModuleID(); JavaSecurityManagers.setContextID(moduleID); final ProvidedSecurityContext providedSecurityContext = newContext.get(ProvidedSecurityContext.class); SecurityContext securityContext = oldContext != null ? oldContext.get(SecurityContext.class) : (providedSecurityContext != null ? providedSecurityContext.context : null); if (providedSecurityContext == null && (securityContext == null || securityContext == defaultContext)) { final Identity identity = clientIdentity.get(); if (identity != null) { securityContext = new SecurityContext(identity.subject); } else { securityContext = defaultContext; } } newContext.set(SecurityContext.class, securityContext); } public UUID overrideWithRunAsContext(final ThreadContext ctx, final BeanContext newContext, final BeanContext oldContext) { Subject runAsSubject = getRunAsSubject(newContext); if (oldContext != null && runAsSubject == null) { runAsSubject = getRunAsSubject(oldContext); } ctx.set(SecurityContext.class, new SecurityContext(runAsSubject)); return disassociate(); } public Subject getRunAsSubject(final BeanContext callingBeanContext) { if (callingBeanContext == null) { return null; } return createRunAsSubject(callingBeanContext.getRunAsUser(), callingBeanContext.getRunAs()); } protected Subject createRunAsSubject(final String runAsUser, final String runAsRole) { return createSubject(runAsUser, runAsRole); } @Override public void contextExited(final ThreadContext exitedContext, final ThreadContext reenteredContext) { if (reenteredContext == null) { JavaSecurityManagers.setContextID(null); } else { JavaSecurityManagers.setContextID(reenteredContext.getBeanContext().getModuleID()); } } protected UUID registerSubject(final Subject subject) { final Identity identity = new Identity(subject); final UUID token = identity.getToken(); identities.put(token, identity); return token; } @Override public void logout(final UUID securityIdentity) throws LoginException { final Identity identity = identities.get(securityIdentity); if (identity == null) { throw new LoginException("Identity is not currently logged in: " + securityIdentity); } identities.remove(securityIdentity); } protected void unregisterSubject(final Object securityIdentity) { identities.remove(securityIdentity); } @Override public void associate(final UUID securityIdentity) throws LoginException { final Identity existing = clientIdentity.get(); if (existing != null && existing.getToken() != null /*can be null if enterWebApp is called, this is not a login without a token*/) { throw new LoginException("Thread already associated with a client identity. Refusing to overwrite. " + "(current=" + existing.getToken() + "/" + existing.getSubject() + ", refused=" + securityIdentity + ")"); } if (securityIdentity == null) { throw new NullPointerException("The security token passed in is null"); } // The securityIdentity token must associated with a logged in Identity final Identity identity = identities.get(securityIdentity); if (identity == null) { throw new LoginException("Identity is not currently logged in: " + securityIdentity); } clientIdentity.set(identity); } @Override public UUID disassociate() { try { final Identity identity = clientIdentity.get(); return identity == null ? null : identity.getToken(); } finally { clientIdentity.remove(); } } @Override public boolean isCallerInRole(final String role) { if (role == null) { throw new IllegalArgumentException("Role must not be null"); } final ThreadContext threadContext = ThreadContext.getThreadContext(); if (threadContext == null) { return false; } final SecurityContext securityContext = threadContext.get(SecurityContext.class); if ("**".equals(role)) { return securityContext != defaultContext; // ie logged in } final Set<Group> grps = securityContext.subject.getPrincipals(Group.class); for (final Group grp : grps) { if (grp.getName().equals(role)) { return true; } } final Set<GroupPrincipal> grpsp = securityContext.subject.getPrincipals(GroupPrincipal.class); for (final GroupPrincipal grp : grpsp) { if (grp.getName().equals(role)) { return true; } } return false; } @Override public Principal getCallerPrincipal() { final ThreadContext threadContext = ThreadContext.getThreadContext(); if (threadContext == null) { final Identity id = clientIdentity.get(); if (id != null) { return getCallerPrincipal(id.getSubject().getPrincipals()); } return null; } final SecurityContext securityContext = threadContext.get(SecurityContext.class); final Set<Principal> principals = securityContext.subject.getPrincipals(); return getCallerPrincipal(principals); } private Principal getCallerPrincipal(final Set<Principal> principals) { if (!principals.isEmpty()) { for (final Principal principal : principals) { if (principal.getClass().isAnnotationPresent(CallerPrincipal.class)) { return principal; } } return principals.iterator().next(); } return null; } @Override public boolean isCallerAuthorized(final Method method, final InterfaceType type) { final ThreadContext threadContext = ThreadContext.getThreadContext(); final BeanContext beanContext = threadContext.getBeanContext(); try { final String ejbName = beanContext.getEjbName(); String name = type == null ? null : type.getSpecName(); if ("LocalBean".equals(name) || "LocalBeanHome".equals(name)) { name = null; } final Identity currentIdentity = clientIdentity.get(); final SecurityContext securityContext; if (currentIdentity == null) { securityContext = threadContext.get(SecurityContext.class); } else { securityContext = new SecurityContext(currentIdentity.getSubject()); } securityContext.acc.checkPermission(new EJBMethodPermission(ejbName, name, method)); } catch (final AccessControlException e) { return false; } return true; } protected static String autoJaccProvider() { return SystemInstance.isInitialized() ? SystemInstance.get().getProperty(JaccProvider.class.getName(), BasicJaccProvider.class.getName()) : BasicJaccProvider.class.getName(); } protected static void installJacc() { final ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); final String providerKey = "javax.security.jacc.PolicyConfigurationFactory.provider"; try { if (JavaSecurityManagers.getSystemProperty(providerKey) == null) { JavaSecurityManagers.setSystemProperty(providerKey, JaccProvider.Factory.class.getName()); final ClassLoader cl = JaccProvider.Factory.class.getClassLoader(); Thread.currentThread().setContextClassLoader(cl); } // Force the loading of the javax.security.jacc.PolicyConfigurationFactory.provider // Hopefully it will be cached thereafter and ClassNotFoundExceptions thrown // from the equivalent call in JaccPermissionsBuilder can be avoided. PolicyConfigurationFactory.getPolicyConfigurationFactory(); } catch (final Exception e) { throw new IllegalStateException("Could not install JACC Policy Configuration Factory: " + JavaSecurityManagers.getSystemProperty(providerKey), e); } finally { Thread.currentThread().setContextClassLoader(contextClassLoader); } final String policyProvider = SystemInstance.get().getOptions().get("javax.security.jacc.policy.provider", JaccProvider.Policy.class.getName()); try { final ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); final Class policyClass = Class.forName(policyProvider, true, classLoader); final Policy policy = (Policy) policyClass.newInstance(); policy.refresh(); Policy.setPolicy(policy); } catch (final Exception e) { throw new IllegalStateException("Could not install JACC Policy Provider: " + policyProvider, e); } } protected Subject createSubject(final String name, final String groupName) { if (name == null) { return null; } final User user = new User(name); final Group group = new Group(groupName); group.addMember(user); final HashSet<Principal> principals = new HashSet<Principal>(); principals.add(user); principals.add(group); return new Subject(true, principals, new HashSet(), new HashSet()); } @Override public Object currentState() { return clientIdentity.get(); } @Override public void setState(final Object o) { if (Identity.class.isInstance(o)) { clientIdentity.set(Identity.class.cast(o)); } else if (o == null) { clientIdentity.remove(); } } public static final class ProvidedSecurityContext { public final SecurityContext context; public ProvidedSecurityContext(final SecurityContext context) { this.context = context; } } public static final class SecurityContext { public final Subject subject; public final AccessControlContext acc; @SuppressWarnings("unchecked") public SecurityContext(final Subject subject) { this.subject = subject; this.acc = (AccessControlContext) Subject.doAsPrivileged(subject, new PrivilegedAction() { @Override public Object run() { return AccessController.getContext(); } }, null); } } protected static class Identity implements Serializable { private final Subject subject; private final UUID token; public Identity(final Subject subject) { this.subject = subject; this.token = UUID.randomUUID(); } public Identity(final Subject subject, final UUID token) { this.subject = subject; this.token = token; } public Subject getSubject() { return subject; } public UUID getToken() { return token; } } public static class Group implements java.security.acl.Group { private final List<Principal> members = new ArrayList<Principal>(); private final String name; public Group(final String name) { this.name = name; } @Override public boolean addMember(final Principal user) { return members.add(user); } @Override public boolean removeMember(final Principal user) { return members.remove(user); } @Override public boolean isMember(final Principal member) { return members.contains(member); } @Override public Enumeration<? extends Principal> members() { return Collections.enumeration(members); } @Override public String getName() { return name; } } @CallerPrincipal // to force it to be before group in getCallerPrincipal, otherwise we aren't deterministic public static class User implements Principal { private final String name; public User(final String name) { this.name = name; } @Override public String getName() { return name; } @Override public boolean equals(final Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } final User user = User.class.cast(o); return !(name != null ? !name.equals(user.name) : user.name != null); } @Override public int hashCode() { return name != null ? name.hashCode() : 0; } } }