/* * 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.geode.internal.security; import static org.apache.geode.distributed.ConfigurationProperties.SECURITY_CLIENT_AUTHENTICATOR; import static org.apache.geode.distributed.ConfigurationProperties.SECURITY_MANAGER; import static org.apache.geode.distributed.ConfigurationProperties.SECURITY_PEER_AUTHENTICATOR; import static org.apache.geode.distributed.ConfigurationProperties.SECURITY_POST_PROCESSOR; import static org.apache.geode.distributed.ConfigurationProperties.SECURITY_SHIRO_INIT; import org.apache.commons.lang.SerializationException; import org.apache.commons.lang.StringUtils; import org.apache.geode.GemFireIOException; import org.apache.geode.internal.cache.EntryEventImpl; import org.apache.geode.internal.logging.LogService; import org.apache.geode.internal.logging.log4j.LogMarker; import org.apache.geode.internal.security.shiro.CustomAuthRealm; import org.apache.geode.internal.security.shiro.GeodeAuthenticationToken; import org.apache.geode.internal.security.shiro.ShiroPrincipal; import org.apache.geode.internal.util.BlobHelper; import org.apache.geode.management.internal.security.ResourceConstants; import org.apache.geode.management.internal.security.ResourceOperation; import org.apache.geode.security.AuthenticationFailedException; import org.apache.geode.security.GemFireSecurityException; import org.apache.geode.security.NotAuthorizedException; import org.apache.geode.security.PostProcessor; import org.apache.geode.security.ResourcePermission; import org.apache.geode.security.ResourcePermission.Operation; import org.apache.geode.security.ResourcePermission.Resource; import org.apache.geode.security.SecurityManager; import org.apache.logging.log4j.Logger; import org.apache.shiro.SecurityUtils; import org.apache.shiro.ShiroException; import org.apache.shiro.UnavailableSecurityManagerException; import org.apache.shiro.config.Ini.Section; import org.apache.shiro.config.IniSecurityManagerFactory; import org.apache.shiro.mgt.DefaultSecurityManager; import org.apache.shiro.realm.Realm; import org.apache.shiro.session.mgt.DefaultSessionManager; import org.apache.shiro.session.mgt.SessionManager; import org.apache.shiro.subject.Subject; import org.apache.shiro.subject.support.SubjectThreadState; import org.apache.shiro.util.ThreadContext; import org.apache.shiro.util.ThreadState; import java.io.IOException; import java.io.Serializable; import java.security.AccessController; import java.util.Properties; import java.util.Set; import java.util.concurrent.Callable; public class IntegratedSecurityService implements SecurityService { private static Logger logger = LogService.getLogger(LogService.SECURITY_LOGGER_NAME); private static SecurityService defaultInstance = new IntegratedSecurityService(); public static SecurityService getSecurityService() { return defaultInstance; } private IntegratedSecurityService() {} private PostProcessor postProcessor; private SecurityManager securityManager; private Boolean isIntegratedSecurity; private boolean isClientAuthenticator; // is there a SECURITY_CLIENT_AUTHENTICATOR private boolean isPeerAuthenticator; // is there a SECURITY_PEER_AUTHENTICATOR /** * It first looks the shiro subject in AccessControlContext since JMX will use multiple threads to * process operations from the same client, then it looks into Shiro's thead context. * * @return the shiro subject, null if security is not enabled */ public Subject getSubject() { if (!isIntegratedSecurity()) { return null; } Subject currentUser = null; // First try get the principal out of AccessControlContext instead of Shiro's Thread context // since threads can be shared between JMX clients. javax.security.auth.Subject jmxSubject = javax.security.auth.Subject.getSubject(AccessController.getContext()); if (jmxSubject != null) { Set<ShiroPrincipal> principals = jmxSubject.getPrincipals(ShiroPrincipal.class); if (principals.size() > 0) { ShiroPrincipal principal = principals.iterator().next(); currentUser = principal.getSubject(); ThreadContext.bind(currentUser); return currentUser; } } // in other cases like rest call, client operations, we get it from the current thread currentUser = SecurityUtils.getSubject(); if (currentUser == null || currentUser.getPrincipal() == null) { throw new GemFireSecurityException("Error: Anonymous User"); } return currentUser; } /** * convenient method for testing */ public Subject login(String username, String password) { if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) return null; Properties credentials = new Properties(); credentials.setProperty(ResourceConstants.USER_NAME, username); credentials.setProperty(ResourceConstants.PASSWORD, password); return login(credentials); } /** * @return null if security is not enabled, otherwise return a shiro subject */ public Subject login(Properties credentials) { if (!isIntegratedSecurity()) { return null; } if (credentials == null) return null; // this makes sure it starts with a clean user object ThreadContext.remove(); Subject currentUser = SecurityUtils.getSubject(); GeodeAuthenticationToken token = new GeodeAuthenticationToken(credentials); try { logger.info("Logging in " + token.getPrincipal()); currentUser.login(token); } catch (ShiroException e) { logger.info(e.getMessage(), e); throw new AuthenticationFailedException( "Authentication error. Please check your credentials.", e); } return currentUser; } public void logout() { Subject currentUser = getSubject(); if (currentUser == null) { return; } try { logger.info("Logging out " + currentUser.getPrincipal()); currentUser.logout(); } catch (ShiroException e) { logger.info(e.getMessage(), e); throw new GemFireSecurityException(e.getMessage(), e); } // clean out Shiro's thread local content ThreadContext.remove(); } public Callable associateWith(Callable callable) { Subject currentUser = getSubject(); if (currentUser == null) { return callable; } return currentUser.associateWith(callable); } /** * this binds the passed-in subject to the executing thread, normally, you would do this: * * ThreadState state = null; try{ state = IntegratedSecurityService.bindSubject(subject); //do the * rest of the work as this subject } finally{ if(state!=null) state.clear(); } */ public ThreadState bindSubject(Subject subject) { if (subject == null) { return null; } ThreadState threadState = new SubjectThreadState(subject); threadState.bind(); return threadState; } public void authorize(ResourceOperation resourceOperation) { if (resourceOperation == null) { return; } authorize(resourceOperation.resource().name(), resourceOperation.operation().name(), null); } public void authorizeClusterManage() { authorize("CLUSTER", "MANAGE"); } public void authorizeClusterWrite() { authorize("CLUSTER", "WRITE"); } public void authorizeClusterRead() { authorize("CLUSTER", "READ"); } public void authorizeDataManage() { authorize("DATA", "MANAGE"); } public void authorizeDataWrite() { authorize("DATA", "WRITE"); } public void authorizeDataRead() { authorize("DATA", "READ"); } public void authorizeRegionManage(String regionName) { authorize("DATA", "MANAGE", regionName); } public void authorizeRegionManage(String regionName, String key) { authorize("DATA", "MANAGE", regionName, key); } public void authorizeRegionWrite(String regionName) { authorize("DATA", "WRITE", regionName); } public void authorizeRegionWrite(String regionName, String key) { authorize("DATA", "WRITE", regionName, key); } public void authorizeRegionRead(String regionName) { authorize("DATA", "READ", regionName); } public void authorizeRegionRead(String regionName, String key) { authorize("DATA", "READ", regionName, key); } public void authorize(String resource, String operation) { authorize(resource, operation, null); } public void authorize(String resource, String operation, String regionName) { authorize(resource, operation, regionName, null); } public void authorize(String resource, String operation, String regionName, String key) { regionName = StringUtils.stripStart(regionName, "/"); authorize(new ResourcePermission(resource, operation, regionName, key)); } public void authorize(ResourcePermission context) { Subject currentUser = getSubject(); if (currentUser == null) { return; } if (context == null) { return; } if (context.getResource() == Resource.NULL && context.getOperation() == Operation.NULL) { return; } try { currentUser.checkPermission(context); } catch (ShiroException e) { String msg = currentUser.getPrincipal() + " not authorized for " + context; logger.info(msg); throw new NotAuthorizedException(msg, e); } } /** * initialize Shiro's Security Manager and Security Utilities */ public void initSecurity(Properties securityProps) { if (securityProps == null) { return; } String shiroConfig = securityProps.getProperty(SECURITY_SHIRO_INIT); String securityManagerConfig = securityProps.getProperty(SECURITY_MANAGER); String clientAuthenticatorConfig = securityProps.getProperty(SECURITY_CLIENT_AUTHENTICATOR); String peerAuthenticatorConfig = securityProps.getProperty(SECURITY_PEER_AUTHENTICATOR); if (!StringUtils.isBlank(shiroConfig)) { IniSecurityManagerFactory factory = new IniSecurityManagerFactory("classpath:" + shiroConfig); // we will need to make sure that shiro uses a case sensitive permission resolver Section main = factory.getIni().addSection("main"); main.put("geodePermissionResolver", "org.apache.geode.internal.security.shiro.GeodePermissionResolver"); if (!main.containsKey("iniRealm.permissionResolver")) { main.put("iniRealm.permissionResolver", "$geodePermissionResolver"); } org.apache.shiro.mgt.SecurityManager securityManager = factory.getInstance(); SecurityUtils.setSecurityManager(securityManager); isIntegratedSecurity = true; isClientAuthenticator = false; isPeerAuthenticator = false; } // only set up shiro realm if user has implemented SecurityManager else if (!StringUtils.isBlank(securityManagerConfig)) { SecurityManager securityManager = SecurityService .getObjectOfTypeFromClassName(securityManagerConfig, SecurityManager.class); securityManager.init(securityProps); this.setSecurityManager(securityManager); } else { isIntegratedSecurity = null; isClientAuthenticator = !StringUtils.isBlank(clientAuthenticatorConfig); isPeerAuthenticator = !StringUtils.isBlank(peerAuthenticatorConfig); } // this initializes the post processor String customPostProcessor = securityProps.getProperty(SECURITY_POST_PROCESSOR); if (!StringUtils.isBlank(customPostProcessor)) { postProcessor = SecurityService.getObjectOfTypeFromClassName(customPostProcessor, PostProcessor.class); postProcessor.init(securityProps); } else { postProcessor = null; } } public void close() { if (securityManager != null) { securityManager.close(); securityManager = null; } if (postProcessor != null) { postProcessor.close(); postProcessor = null; } ThreadContext.remove(); SecurityUtils.setSecurityManager(null); isIntegratedSecurity = null; isClientAuthenticator = false; isPeerAuthenticator = false; } /** * postProcess call already has this logic built in, you don't need to call this everytime you * call postProcess. But if your postProcess is pretty involved with preparations and you need to * bypass it entirely, call this first. */ public boolean needPostProcess() { return (isIntegratedSecurity() && postProcessor != null); } public Object postProcess(String regionPath, Object key, Object value, boolean valueIsSerialized) { return postProcess(null, regionPath, key, value, valueIsSerialized); } public Object postProcess(Object principal, String regionPath, Object key, Object value, boolean valueIsSerialized) { if (!needPostProcess()) return value; if (principal == null) { Subject subject = getSubject(); if (subject == null) return value; principal = (Serializable) subject.getPrincipal(); } String regionName = StringUtils.stripStart(regionPath, "/"); Object newValue = null; // if the data is a byte array, but the data itself is supposed to be an object, we need to // desearized it before we pass // it to the callback. if (valueIsSerialized && value instanceof byte[]) { try { Object oldObj = EntryEventImpl.deserialize((byte[]) value); Object newObj = postProcessor.processRegionValue(principal, regionName, key, oldObj); newValue = BlobHelper.serializeToBlob(newObj); } catch (IOException | SerializationException e) { throw new GemFireIOException("Exception de/serializing entry value", e); } } else { newValue = postProcessor.processRegionValue(principal, regionName, key, value); } return newValue; } public SecurityManager getSecurityManager() { return securityManager; } public void setSecurityManager(SecurityManager securityManager) { if (securityManager == null) { return; } this.securityManager = securityManager; Realm realm = new CustomAuthRealm(securityManager); DefaultSecurityManager shiroManager = new DefaultSecurityManager(realm); SecurityUtils.setSecurityManager(shiroManager); increaseShiroGlobalSessionTimeout(shiroManager); isIntegratedSecurity = true; isClientAuthenticator = false; isPeerAuthenticator = false; } private void increaseShiroGlobalSessionTimeout(final DefaultSecurityManager shiroManager) { SessionManager sessionManager = shiroManager.getSessionManager(); if (DefaultSessionManager.class.isInstance(sessionManager)) { DefaultSessionManager defaultSessionManager = (DefaultSessionManager) sessionManager; defaultSessionManager.setGlobalSessionTimeout(Long.MAX_VALUE); long value = defaultSessionManager.getGlobalSessionTimeout(); if (value != Long.MAX_VALUE) { logger.error("Unable to set Shiro Global Session Timeout. Current value is '{}'.", value); } } else { logger.error("Unable to set Shiro Global Session Timeout. Current SessionManager is '{}'.", sessionManager == null ? "null" : sessionManager.getClass()); } } public PostProcessor getPostProcessor() { return postProcessor; } public void setPostProcessor(PostProcessor postProcessor) { if (postProcessor == null) { return; } this.postProcessor = postProcessor; } /** * check if Shiro's security manager is configured * * @return true if configured, false if not */ public boolean isIntegratedSecurity() { if (isIntegratedSecurity != null) { return isIntegratedSecurity; } try { isIntegratedSecurity = (SecurityUtils.getSecurityManager() != null); } catch (UnavailableSecurityManagerException e) { isIntegratedSecurity = false; } return isIntegratedSecurity; } public boolean isClientSecurityRequired() { return isClientAuthenticator || isIntegratedSecurity(); } public boolean isPeerSecurityRequired() { return isPeerAuthenticator || isIntegratedSecurity(); } }