/*
* 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.examples.security;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.io.IOUtils;
import org.apache.geode.management.internal.security.ResourceConstants;
import org.apache.geode.security.AuthenticationFailedException;
import org.apache.geode.security.NotAuthorizedException;
import org.apache.geode.security.ResourcePermission;
import org.apache.geode.security.SecurityManager;
import org.apache.shiro.authz.Permission;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
/**
* This class provides a sample implementation of {@link SecurityManager} for authentication and
* authorization initialized from data provided as JSON.
*
* <p>
* A Geode member must be configured with the following:
*
* <p>
* {@code security-manager = org.apache.geode.security.examples.ExampleSecurityManager}
*
* <p>
* The class can be initialized with from a JSON resource called {@code security.json}. This file
* must exist on the classpath, so members should be started with an appropriate {@code --classpath}
* option.
*
* <p>
* The format of the JSON for configuration is as follows:
*
* <pre>
* <code>
* {
* "roles": [
* {
* "name": "admin",
* "operationsAllowed": [
* "CLUSTER:MANAGE",
* "DATA:MANAGE"
* ]
* },
* {
* "name": "readRegionA",
* "operationsAllowed": [
* "DATA:READ"
* ],
* "regions": ["RegionA", "RegionB"]
* }
* ],
* "users": [
* {
* "name": "admin",
* "password": "secret",
* "roles": ["admin"]
* },
* {
* "name": "guest",
* "password": "guest",
* "roles": ["readRegionA"]
* }
* ]
* }
* </code>
* </pre>
*/
public class ExampleSecurityManager implements SecurityManager {
public static final String SECURITY_JSON = "security-json";
protected static final String DEFAULT_JSON_FILE_NAME = "security.json";
private Map<String, User> userNameToUser;
@Override
public boolean authorize(final Object principal, final ResourcePermission context) {
if (principal == null)
return false;
User user = this.userNameToUser.get(principal.toString());
if (user == null)
return false; // this user is not authorized to do anything
// check if the user has this permission defined in the context
for (Role role : this.userNameToUser.get(user.name).roles) {
for (Permission permitted : role.permissions) {
if (permitted.implies(context)) {
return true;
}
}
}
return false;
}
@Override
public void init(final Properties securityProperties) throws NotAuthorizedException {
String jsonPropertyValue =
securityProperties != null ? securityProperties.getProperty(SECURITY_JSON) : null;
if (jsonPropertyValue == null) {
jsonPropertyValue = DEFAULT_JSON_FILE_NAME;
}
if (!initializeFromJsonResource(jsonPropertyValue)) {
throw new AuthenticationFailedException(
"ExampleSecurityManager: unable to find json resource \"" + jsonPropertyValue
+ "\" as specified by [" + SECURITY_JSON + "].");
}
}
@Override
public Object authenticate(final Properties credentials) throws AuthenticationFailedException {
String user = credentials.getProperty(ResourceConstants.USER_NAME);
String password = credentials.getProperty(ResourceConstants.PASSWORD);
User userObj = this.userNameToUser.get(user);
if (userObj == null) {
throw new AuthenticationFailedException("ExampleSecurityManager: wrong username/password");
}
if (user != null && !userObj.password.equals(password) && !"".equals(user)) {
throw new AuthenticationFailedException("ExampleSecurityManager: wrong username/password");
}
return user;
}
boolean initializeFromJson(final String json) {
try {
ObjectMapper mapper = new ObjectMapper();
JsonNode jsonNode = mapper.readTree(json);
this.userNameToUser = new HashMap<>();
Map<String, Role> roleMap = readRoles(jsonNode);
readUsers(this.userNameToUser, jsonNode, roleMap);
return true;
} catch (IOException ex) {
return false;
}
}
public boolean initializeFromJsonResource(final String jsonResource) {
try {
InputStream input = ClassLoader.getSystemResourceAsStream(jsonResource);
if (input != null) {
initializeFromJson(readJsonFromInputStream(input));
return true;
}
} catch (IOException ex) {
}
return false;
}
public User getUser(final String user) {
return this.userNameToUser.get(user);
}
private String readJsonFromInputStream(final InputStream input) throws IOException {
StringWriter writer = new StringWriter();
IOUtils.copy(input, writer, "UTF-8");
return writer.toString();
}
private void readUsers(final Map<String, User> rolesToUsers, final JsonNode node,
final Map<String, Role> roleMap) {
for (JsonNode usersNode : node.get("users")) {
User user = new User();
user.name = usersNode.get("name").asText();
if (usersNode.has("password")) {
user.password = usersNode.get("password").asText();
} else {
user.password = user.name;
}
for (JsonNode rolesNode : usersNode.get("roles")) {
user.roles.add(roleMap.get(rolesNode.asText()));
}
rolesToUsers.put(user.name, user);
}
}
private Map<String, Role> readRoles(final JsonNode jsonNode) {
if (jsonNode.get("roles") == null) {
return Collections.EMPTY_MAP;
}
Map<String, Role> roleMap = new HashMap<>();
for (JsonNode rolesNode : jsonNode.get("roles")) {
Role role = new Role();
role.name = rolesNode.get("name").asText();
String regionNames = null;
String keys = null;
JsonNode regionsNode = rolesNode.get("regions");
if (regionsNode != null) {
if (regionsNode.isArray()) {
regionNames = StreamSupport.stream(regionsNode.spliterator(), false).map(JsonNode::asText)
.collect(Collectors.joining(","));
} else {
regionNames = regionsNode.asText();
}
}
for (JsonNode operationsAllowedNode : rolesNode.get("operationsAllowed")) {
String[] parts = operationsAllowedNode.asText().split(":");
String resourcePart = (parts.length > 0) ? parts[0] : null;
String operationPart = (parts.length > 1) ? parts[1] : null;
if (parts.length > 2) {
regionNames = parts[2];
}
if (parts.length > 3) {
keys = parts[3];
}
String regionPart = (regionNames != null) ? regionNames : "*";
String keyPart = (keys != null) ? keys : "*";
role.permissions
.add(new ResourcePermission(resourcePart, operationPart, regionPart, keyPart));
}
roleMap.put(role.name, role);
if (rolesNode.has("serverGroup")) {
role.serverGroup = rolesNode.get("serverGroup").asText();
}
}
return roleMap;
}
public static class Role {
List<ResourcePermission> permissions = new ArrayList<>();
public List<ResourcePermission> getPermissions() {
return permissions;
}
public void setPermissions(final List<ResourcePermission> permissions) {
this.permissions = permissions;
}
public String getName() {
return name;
}
public void setName(final String name) {
this.name = name;
}
public String getServerGroup() {
return serverGroup;
}
public void setServerGroup(final String serverGroup) {
this.serverGroup = serverGroup;
}
String name;
String serverGroup;
}
public static class User {
String name;
Set<Role> roles = new HashSet<>();
public String getName() {
return name;
}
public void setName(final String name) {
this.name = name;
}
public Set<Role> getRoles() {
return roles;
}
public void setRoles(final Set<Role> roles) {
this.roles = roles;
}
public String getPassword() {
return password;
}
public void setPassword(final String password) {
this.password = password;
}
String password;
}
}