/*
* 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.activemq.artemis.core.security.impl;
import javax.security.cert.X509Certificate;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.apache.activemq.artemis.api.core.SimpleString;
import org.apache.activemq.artemis.api.core.management.CoreNotificationType;
import org.apache.activemq.artemis.api.core.management.ManagementHelper;
import org.apache.activemq.artemis.core.security.CheckType;
import org.apache.activemq.artemis.core.security.Role;
import org.apache.activemq.artemis.core.security.SecurityAuth;
import org.apache.activemq.artemis.core.security.SecurityStore;
import org.apache.activemq.artemis.core.server.ActiveMQMessageBundle;
import org.apache.activemq.artemis.core.server.management.Notification;
import org.apache.activemq.artemis.core.server.management.NotificationService;
import org.apache.activemq.artemis.core.settings.HierarchicalRepository;
import org.apache.activemq.artemis.core.settings.HierarchicalRepositoryChangeListener;
import org.apache.activemq.artemis.spi.core.security.ActiveMQSecurityManager;
import org.apache.activemq.artemis.spi.core.security.ActiveMQSecurityManager2;
import org.apache.activemq.artemis.spi.core.security.ActiveMQSecurityManager3;
import org.apache.activemq.artemis.utils.collections.ConcurrentHashSet;
import org.apache.activemq.artemis.utils.collections.TypedProperties;
import org.jboss.logging.Logger;
/**
* The ActiveMQ Artemis SecurityStore implementation
*/
public class SecurityStoreImpl implements SecurityStore, HierarchicalRepositoryChangeListener {
private static final Logger logger = Logger.getLogger(SecurityStoreImpl.class);
private final HierarchicalRepository<Set<Role>> securityRepository;
private final ActiveMQSecurityManager securityManager;
private final ConcurrentMap<String, ConcurrentHashSet<SimpleString>> cache = new ConcurrentHashMap<>();
private final long invalidationInterval;
private volatile long lastCheck;
private final boolean securityEnabled;
private final String managementClusterUser;
private final String managementClusterPassword;
private final NotificationService notificationService;
// Constructors --------------------------------------------------
/**
* @param notificationService can be <code>null</code>
*/
public SecurityStoreImpl(final HierarchicalRepository<Set<Role>> securityRepository,
final ActiveMQSecurityManager securityManager,
final long invalidationInterval,
final boolean securityEnabled,
final String managementClusterUser,
final String managementClusterPassword,
final NotificationService notificationService) {
this.securityRepository = securityRepository;
this.securityManager = securityManager;
this.invalidationInterval = invalidationInterval;
this.securityEnabled = securityEnabled;
this.managementClusterUser = managementClusterUser;
this.managementClusterPassword = managementClusterPassword;
this.notificationService = notificationService;
this.securityRepository.registerListener(this);
}
// SecurityManager implementation --------------------------------
@Override
public boolean isSecurityEnabled() {
return securityEnabled;
}
@Override
public void stop() {
securityRepository.unRegisterListener(this);
}
@Override
public String authenticate(final String user,
final String password,
X509Certificate[] certificates) throws Exception {
if (securityEnabled) {
if (managementClusterUser.equals(user)) {
if (logger.isTraceEnabled()) {
logger.trace("Authenticating cluster admin user");
}
/*
* The special user cluster user is used for creating sessions that replicate management
* operation between nodes
*/
if (!managementClusterPassword.equals(password)) {
throw ActiveMQMessageBundle.BUNDLE.unableToValidateClusterUser(user);
} else {
return managementClusterUser;
}
}
String validatedUser = null;
boolean userIsValid = false;
if (securityManager instanceof ActiveMQSecurityManager3) {
validatedUser = ((ActiveMQSecurityManager3) securityManager).validateUser(user, password, certificates);
} else if (securityManager instanceof ActiveMQSecurityManager2) {
userIsValid = ((ActiveMQSecurityManager2) securityManager).validateUser(user, password, certificates);
} else {
userIsValid = securityManager.validateUser(user, password);
}
if (!userIsValid && validatedUser == null) {
if (notificationService != null) {
TypedProperties props = new TypedProperties();
Notification notification = new Notification(null, CoreNotificationType.SECURITY_AUTHENTICATION_VIOLATION, props);
notificationService.sendNotification(notification);
}
throw ActiveMQMessageBundle.BUNDLE.unableToValidateUser();
}
return validatedUser;
}
return null;
}
@Override
public void check(final SimpleString address,
final CheckType checkType,
final SecurityAuth session) throws Exception {
if (securityEnabled) {
if (logger.isTraceEnabled()) {
logger.trace("checking access permissions to " + address);
}
String user = session.getUsername();
if (checkCached(address, user, checkType)) {
// OK
return;
}
String saddress = address.toString();
Set<Role> roles = securityRepository.getMatch(saddress);
// bypass permission checks for management cluster user
if (managementClusterUser.equals(user) && session.getPassword().equals(managementClusterPassword)) {
return;
}
final boolean validated;
if (securityManager instanceof ActiveMQSecurityManager3) {
final ActiveMQSecurityManager3 securityManager3 = (ActiveMQSecurityManager3) securityManager;
validated = securityManager3.validateUserAndRole(user, session.getPassword(), roles, checkType, saddress, session.getRemotingConnection()) != null;
} else if (securityManager instanceof ActiveMQSecurityManager2) {
final ActiveMQSecurityManager2 securityManager2 = (ActiveMQSecurityManager2) securityManager;
validated = securityManager2.validateUserAndRole(user, session.getPassword(), roles, checkType, saddress, session.getRemotingConnection());
} else {
validated = securityManager.validateUserAndRole(user, session.getPassword(), roles, checkType);
}
if (!validated) {
if (notificationService != null) {
TypedProperties props = new TypedProperties();
props.putSimpleStringProperty(ManagementHelper.HDR_ADDRESS, address);
props.putSimpleStringProperty(ManagementHelper.HDR_CHECK_TYPE, new SimpleString(checkType.toString()));
props.putSimpleStringProperty(ManagementHelper.HDR_USER, SimpleString.toSimpleString(user));
Notification notification = new Notification(null, CoreNotificationType.SECURITY_PERMISSION_VIOLATION, props);
notificationService.sendNotification(notification);
}
throw ActiveMQMessageBundle.BUNDLE.userNoPermissions(session.getUsername(), checkType, saddress);
}
// if we get here we're granted, add to the cache
ConcurrentHashSet<SimpleString> set = new ConcurrentHashSet<>();
ConcurrentHashSet<SimpleString> act = cache.putIfAbsent(user + "." + checkType.name(), set);
if (act != null) {
set = act;
}
set.add(address);
}
}
@Override
public void onChange() {
invalidateCache();
}
// Public --------------------------------------------------------
// Protected -----------------------------------------------------
// Package Private -----------------------------------------------
// Private -------------------------------------------------------
private void invalidateCache() {
cache.clear();
}
private boolean checkCached(final SimpleString dest, final String user, final CheckType checkType) {
long now = System.currentTimeMillis();
boolean granted = false;
if (now - lastCheck > invalidationInterval) {
invalidateCache();
lastCheck = now;
} else {
ConcurrentHashSet<SimpleString> act = cache.get(user + "." + checkType.name());
if (act != null) {
granted = act.contains(dest);
}
}
return granted;
}
// Inner class ---------------------------------------------------
}