| /* |
| * 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.qpid.server.security.group; |
| |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileNotFoundException; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.util.Collections; |
| import java.util.Date; |
| import java.util.Map; |
| import java.util.Properties; |
| import java.util.Set; |
| import java.util.concurrent.ConcurrentHashMap; |
| import java.util.concurrent.ConcurrentSkipListSet; |
| |
| import com.google.common.base.Joiner; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import org.apache.qpid.server.util.BaseAction; |
| import org.apache.qpid.server.util.FileHelper; |
| import org.apache.qpid.server.util.ServerScopedRuntimeException; |
| |
| /** |
| * A group database that reads/writes the following file format: |
| * <p> |
| * group1.users=user1,user2 |
| * group2.users=user2,user3 |
| */ |
| public class FileGroupDatabase implements GroupDatabase |
| { |
| private static final Logger LOGGER = LoggerFactory.getLogger(FileGroupDatabase.class); |
| |
| private final Map<String, Set<String>> _groupToUserMap; |
| private final Map<String, Set<String>> _userToGroupMap; |
| private String _groupFile; |
| private final boolean _caseSensitive; |
| |
| /** |
| * @param caseSensitive provides information if search of Users and Groups is CaseSensitive or CaseInsensitive; |
| */ |
| public FileGroupDatabase(boolean caseSensitive) |
| { |
| this._caseSensitive = caseSensitive; |
| _groupToUserMap = new ConcurrentHashMap<>(); |
| _userToGroupMap = new ConcurrentHashMap<>(); |
| } |
| |
| @Override |
| public Set<String> getAllGroups() |
| { |
| return Collections.unmodifiableSet(_groupToUserMap.keySet()); |
| } |
| |
| public synchronized void setGroupFile(String groupFile) throws IOException |
| { |
| File file = new File(groupFile); |
| |
| if (!file.canRead()) |
| { |
| throw new FileNotFoundException(groupFile + " cannot be found or is not readable"); |
| } |
| |
| readGroupFile(groupFile); |
| } |
| |
| @Override |
| public Set<String> getUsersInGroup(String group) |
| { |
| if (group == null) |
| { |
| LOGGER.warn("Requested user set for null group. Returning empty set."); |
| return Collections.emptySet(); |
| } |
| |
| Set<String> set = _groupToUserMap.get(keySearch(_groupToUserMap.keySet(), group)); |
| if (set == null) |
| { |
| return Collections.emptySet(); |
| } |
| else |
| { |
| return Collections.unmodifiableSet(set); |
| } |
| } |
| |
| @Override |
| public synchronized void addUserToGroup(String user, String group) |
| { |
| Set<String> users = _groupToUserMap.get(keySearch(_groupToUserMap.keySet(), group)); |
| if (users == null) |
| { |
| throw new IllegalArgumentException("Group " |
| + group |
| + " does not exist so could not add " |
| + user |
| + " to it"); |
| } |
| |
| users.add(keySearch(users, user)); |
| |
| Set<String> groups = _userToGroupMap.get(keySearch(_userToGroupMap.keySet(), user)); |
| if (groups == null) |
| { |
| groups = new ConcurrentSkipListSet<String>(); |
| _userToGroupMap.put(user, groups); |
| } |
| groups.add(keySearch(_groupToUserMap.keySet(), group)); |
| |
| update(); |
| } |
| |
| @Override |
| public synchronized void removeUserFromGroup(String user, String group) |
| { |
| Set<String> users = _groupToUserMap.get(keySearch(_groupToUserMap.keySet(), group)); |
| if (users == null) |
| { |
| throw new IllegalArgumentException("Group " |
| + group |
| + " does not exist so could not remove " |
| + user |
| + " from it"); |
| } |
| |
| users.remove(keySearch(users, user)); |
| |
| Set<String> groups = _userToGroupMap.get(keySearch(_userToGroupMap.keySet(), user)); |
| if (groups != null) |
| { |
| groups.remove(keySearch(groups, group)); |
| } |
| |
| update(); |
| } |
| |
| @Override |
| public Set<String> getGroupsForUser(String user) |
| { |
| if (user == null) |
| { |
| LOGGER.warn("Requested group set for null user. Returning empty set."); |
| return Collections.emptySet(); |
| } |
| |
| Set<String> groups = _userToGroupMap.get(keySearch(_userToGroupMap.keySet(), user)); |
| if (groups == null) |
| { |
| return Collections.emptySet(); |
| } |
| else |
| { |
| return Collections.unmodifiableSet(groups); |
| } |
| } |
| |
| @Override |
| public synchronized void createGroup(String group) |
| { |
| Set<String> users = new ConcurrentSkipListSet<String>(); |
| _groupToUserMap.put(group, users); |
| |
| update(); |
| } |
| |
| @Override |
| public synchronized void removeGroup(String group) |
| { |
| _groupToUserMap.remove(keySearch(_groupToUserMap.keySet(), group)); |
| for (Set<String> groupsForUser : _userToGroupMap.values()) |
| { |
| groupsForUser.remove(keySearch(groupsForUser, group)); |
| } |
| |
| update(); |
| } |
| |
| private synchronized void update() |
| { |
| if (_groupFile != null) |
| { |
| try |
| { |
| writeGroupFile(_groupFile); |
| } |
| catch (IOException e) |
| { |
| throw new ServerScopedRuntimeException("Unable to persist change to file " + _groupFile, e); |
| } |
| } |
| } |
| |
| private synchronized void readGroupFile(String groupFile) throws IOException |
| { |
| _groupFile = groupFile; |
| _groupToUserMap.clear(); |
| _userToGroupMap.clear(); |
| Properties propertiesFile = new Properties(); |
| try (FileInputStream fileInputStream = new FileInputStream(groupFile)) |
| { |
| propertiesFile.load(fileInputStream); |
| } |
| |
| for (String propertyName : propertiesFile.stringPropertyNames()) |
| { |
| validatePropertyNameIsGroupName(propertyName); |
| |
| String groupName = propertyName.replaceAll("\\.users$", ""); |
| String userString = propertiesFile.getProperty(propertyName); |
| |
| final Set<String> userSet = buildUserSetFromCommaSeparateValue(userString); |
| |
| _groupToUserMap.put(groupName, userSet); |
| |
| for (String userName : userSet) |
| { |
| Set<String> groupsForThisUser = _userToGroupMap.get(keySearch(_userToGroupMap.keySet(), userName)); |
| |
| if (groupsForThisUser == null) |
| { |
| groupsForThisUser = new ConcurrentSkipListSet<String>(); |
| _userToGroupMap.put(userName, groupsForThisUser); |
| } |
| |
| groupsForThisUser.add(groupName); |
| } |
| } |
| } |
| |
| private synchronized void writeGroupFile(final String groupFile) throws IOException |
| { |
| final Properties propertiesFile = new Properties(); |
| |
| for (String group : _groupToUserMap.keySet()) |
| { |
| Set<String> users = _groupToUserMap.get(keySearch(_groupToUserMap.keySet(), group)); |
| final String userList = Joiner.on(",").useForNull("").join(users); |
| |
| propertiesFile.setProperty(group + ".users", userList); |
| } |
| |
| new FileHelper().writeFileSafely(new File(groupFile).toPath(), new BaseAction<File, IOException>() |
| { |
| @Override |
| public void performAction(File file) throws IOException |
| { |
| String comment = "Written " + new Date(); |
| try (FileOutputStream fileOutputStream = new FileOutputStream(file)) |
| { |
| propertiesFile.store(fileOutputStream, comment); |
| } |
| } |
| }); |
| } |
| |
| private void validatePropertyNameIsGroupName(String propertyName) |
| { |
| if (!propertyName.endsWith(".users")) |
| { |
| throw new IllegalArgumentException("Invalid definition with name '" |
| + propertyName |
| + "'. Group definitions must end with suffix '.users'"); |
| } |
| } |
| |
| private ConcurrentSkipListSet<String> buildUserSetFromCommaSeparateValue(String userString) |
| { |
| String[] users = userString.split(","); |
| final ConcurrentSkipListSet<String> userSet = new ConcurrentSkipListSet<String>(); |
| for (String user : users) |
| { |
| final String trimmed = user.trim(); |
| if (!trimmed.isEmpty()) |
| { |
| userSet.add(trimmed); |
| } |
| } |
| return userSet; |
| } |
| |
| private String keySearch(Set<String> set, String requiredKey) |
| { |
| if (!_caseSensitive) |
| { |
| for (String key : set) |
| { |
| if (key.equalsIgnoreCase(requiredKey)) |
| { |
| return key; |
| } |
| } |
| } |
| return requiredKey; |
| } |
| } |