com.hortonworks.streamline.streams.security.service.SecurityCatalogService.java Source code

Java tutorial

Introduction

Here is the source code for com.hortonworks.streamline.streams.security.service.SecurityCatalogService.java

Source

/**
 * Copyright 2017 Hortonworks.
 *
 * Licensed 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 com.hortonworks.streamline.streams.security.service;

import com.google.common.collect.Sets;
import com.hortonworks.streamline.common.QueryParam;
import com.hortonworks.streamline.common.util.Utils;
import com.hortonworks.streamline.storage.StorableKey;
import com.hortonworks.streamline.storage.StorageManager;
import com.hortonworks.streamline.storage.util.StorageUtils;
import com.hortonworks.streamline.streams.security.Permission;
import com.hortonworks.streamline.streams.security.catalog.AclEntry;
import com.hortonworks.streamline.streams.security.catalog.Role;
import com.hortonworks.streamline.streams.security.catalog.RoleHierarchy;
import com.hortonworks.streamline.streams.security.catalog.User;
import com.hortonworks.streamline.streams.security.catalog.UserRole;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static com.hortonworks.streamline.streams.security.catalog.AclEntry.SidType.ROLE;
import static com.hortonworks.streamline.streams.security.catalog.AclEntry.SidType.USER;

public class SecurityCatalogService {
    private static final Logger LOG = LoggerFactory.getLogger(SecurityCatalogService.class);

    private final StorageManager dao;

    public SecurityCatalogService(StorageManager storageManager) {
        this.dao = storageManager;
    }

    public Collection<Role> listRoles() {
        return this.dao.list(Role.NAMESPACE);
    }

    public Collection<Role> listRoles(List<QueryParam> params) {
        return dao.find(Role.NAMESPACE, params);
    }

    public Optional<Role> getRole(String roleName) {
        List<QueryParam> qps = QueryParam.params(Role.NAME, String.valueOf(roleName));
        Collection<Role> roles = listRoles(qps);
        return roles.isEmpty() ? Optional.empty() : Optional.of(roles.iterator().next());
    }

    public Role getRole(Long roleId) {
        Role role = new Role();
        role.setId(roleId);
        return this.dao.get(new StorableKey(Role.NAMESPACE, role.getPrimaryKey()));
    }

    public Role addRole(Role role) {
        if (role.getId() == null) {
            role.setId(this.dao.nextId(Role.NAMESPACE));
        }
        if (role.getTimestamp() == null) {
            role.setTimestamp(System.currentTimeMillis());
        }
        validateRole(role);
        this.dao.add(role);
        return role;
    }

    public Role addOrUpdateRole(Long id, Role role) {
        role.setId(id);
        role.setTimestamp(System.currentTimeMillis());
        this.dao.addOrUpdate(role);
        return role;
    }

    public Role removeRole(Long roleId) {
        // check if role is part of any parent roles, if so parent role should be deleted first.
        Set<Role> parentRoles = getParentRoles(roleId);
        if (!parentRoles.isEmpty()) {
            throw new IllegalStateException("Role is a child role of the following parent role(s): " + parentRoles
                    + ". Parent roles must be deleted first.");
        }

        // check if role has any users
        List<QueryParam> qps = QueryParam.params(UserRole.ROLE_ID, String.valueOf(roleId));
        Collection<UserRole> userRoles = listUserRoles(qps);
        if (!userRoles.isEmpty()) {
            throw new IllegalStateException("Role has users");
        }

        // remove child role associations
        qps = QueryParam.params(RoleHierarchy.PARENT_ID, String.valueOf(roleId));
        Collection<RoleHierarchy> roleHierarchies = dao.find(RoleHierarchy.NAMESPACE, qps);
        LOG.info("Removing child role association for role id {}", roleId);
        roleHierarchies.forEach(rh -> removeChildRole(roleId, rh.getChildId()));

        // remove permissions assigned to role
        qps = QueryParam.params(AclEntry.SID_ID, String.valueOf(roleId), AclEntry.SID_TYPE,
                AclEntry.SidType.ROLE.toString());
        LOG.info("Removing ACL entries for role id {}", roleId);
        listAcls(qps).forEach(aclEntry -> removeAcl(aclEntry.getId()));
        Role role = new Role();
        role.setId(roleId);
        return dao.remove(new StorableKey(Role.NAMESPACE, role.getPrimaryKey()));
    }

    public Collection<User> listUsers() {
        return fillRoles(this.dao.list(User.NAMESPACE));
    }

    public Collection<User> listUsers(List<QueryParam> params) {
        return fillRoles(dao.find(User.NAMESPACE, params));
    }

    // list of users that have the given role
    public Collection<User> listUsers(Role role) {
        List<QueryParam> qps = QueryParam.params(UserRole.ROLE_ID, role.getId().toString());
        return listUserRoles(qps).stream().map(ur -> getUser(ur.getUserId())).collect(Collectors.toSet());
    }

    public User getUser(String name) {
        List<QueryParam> qps = QueryParam.params(User.NAME, name);
        Collection<User> users = listUsers(qps);
        if (users.size() == 1) {
            return fillRoles(users.iterator().next());
        }
        return null;
    }

    public User getUser(Long userId) {
        User user = new User();
        user.setId(userId);
        return fillRoles(this.dao.<User>get(new StorableKey(User.NAMESPACE, user.getPrimaryKey())));
    }

    public User addUser(User user) {
        if (user.getId() == null) {
            user.setId(this.dao.nextId(User.NAMESPACE));
        }
        if (user.getTimestamp() == null) {
            user.setTimestamp(System.currentTimeMillis());
        }
        validateUser(user);
        this.dao.add(user);
        // create user - role association
        if (user.getRoles() != null) {
            user.getRoles().forEach(roleName -> {
                Optional<Role> role = getRole(roleName);
                if (!role.isPresent()) {
                    removeUser(user.getId());
                    throw new IllegalArgumentException("No such role: " + roleName);
                }
                addUserRole(user.getId(), role.get().getId());
            });
        }
        return user;
    }

    public User addOrUpdateUser(Long id, User user) {
        user.setId(id);
        user.setTimestamp(System.currentTimeMillis());
        validateUser(user);
        this.dao.addOrUpdate(user);
        // update user - role association
        if (user.getRoles() != null) {
            List<QueryParam> qps = QueryParam.params(UserRole.USER_ID, String.valueOf(user.getId()));
            Set<Long> existing = listUserRoles(qps).stream().map(UserRole::getRoleId).collect(Collectors.toSet());
            Set<Long> newRoles = user.getRoles().stream().map(this::getRole).filter(Optional::isPresent)
                    .map(role -> role.get().getId()).collect(Collectors.toSet());
            Sets.difference(existing, newRoles).forEach(roleId -> removeUserRole(id, roleId));
            Sets.difference(newRoles, existing).forEach(roleId -> {
                if (getRole(roleId) == null) {
                    throw new IllegalArgumentException("No role with id: " + roleId);
                }
                addUserRole(id, roleId);
            });
        }
        return user;
    }

    public User removeUser(Long userId) {
        User userToRemove = getUser(userId);
        if (userToRemove != null) {
            if (userToRemove.getRoles() != null) {
                userToRemove.getRoles().forEach(roleName -> {
                    Optional<Role> r = getRole(roleName);
                    if (r.isPresent()) {
                        removeUserRole(userId, r.get().getId());
                    }
                });
            }
            // remove permissions assigned to user
            LOG.debug("Removing ACL entries for user {}", userToRemove);
            List<QueryParam> qps = QueryParam.params(AclEntry.SID_ID, String.valueOf(userId), AclEntry.SID_TYPE,
                    AclEntry.SidType.USER.toString());
            listAcls(qps).forEach(aclEntry -> removeAcl(aclEntry.getId()));
            return dao.remove(new StorableKey(User.NAMESPACE, userToRemove.getPrimaryKey()));
        }
        throw new IllegalArgumentException("No user with id: " + userId);
    }

    private Set<Role> getParentRoles(Long childRoleId) {
        List<QueryParam> qps = QueryParam.params(RoleHierarchy.CHILD_ID, String.valueOf(childRoleId));
        Collection<RoleHierarchy> roleHierarchies = dao.find(RoleHierarchy.NAMESPACE, qps);
        Set<Role> res = new HashSet<>();
        roleHierarchies.forEach(rh -> {
            res.add(getRole(rh.getParentId()));
        });
        return res;
    }

    public Set<Role> getChildRoles(Long parentRoleId) {
        return doGetChildRoles(parentRoleId, new HashMap<>());
    }

    public RoleHierarchy addChildRole(Long parentRoleId, Long childRoleId) {
        validateRoleIds(parentRoleId);
        RoleHierarchy roleHierarchy = new RoleHierarchy();
        roleHierarchy.setParentId(parentRoleId);
        roleHierarchy.setChildId(childRoleId);
        this.dao.add(roleHierarchy);
        return roleHierarchy;
    }

    public RoleHierarchy removeChildRole(Long parentRoleId, Long childRoleId) {
        validateRoleIds(parentRoleId);
        RoleHierarchy roleHierarchy = new RoleHierarchy();
        roleHierarchy.setParentId(parentRoleId);
        roleHierarchy.setChildId(childRoleId);
        return this.dao.remove(new StorableKey(RoleHierarchy.NAMESPACE, roleHierarchy.getPrimaryKey()));
    }

    public Collection<UserRole> listUserRoles(List<QueryParam> qps) {
        return dao.find(UserRole.NAMESPACE, qps);
    }

    public UserRole addUserRole(Long userId, Long roleId) {
        UserRole userRole = new UserRole();
        userRole.setUserId(userId);
        userRole.setRoleId(roleId);
        dao.add(userRole);
        return userRole;
    }

    public UserRole removeUserRole(Long userId, Long roleId) {
        UserRole userRole = new UserRole();
        userRole.setUserId(userId);
        userRole.setRoleId(roleId);
        return dao.remove(new StorableKey(UserRole.NAMESPACE, userRole.getPrimaryKey()));
    }

    public Collection<AclEntry> listAcls() {
        return this.dao.list(AclEntry.NAMESPACE);
    }

    public Collection<AclEntry> listAcls(List<QueryParam> params) {
        return dao.find(AclEntry.NAMESPACE, params);
    }

    public Collection<AclEntry> listUserAcls(Long userId, String targetEntityNamespace, Long targetEntityId) {
        List<QueryParam> qps = QueryParam.params(AclEntry.SID_ID, userId.toString(), AclEntry.SID_TYPE,
                AclEntry.SidType.USER.toString(), AclEntry.OBJECT_NAMESPACE, targetEntityNamespace,
                AclEntry.OBJECT_ID, targetEntityId.toString());
        return listAcls(qps);
    }

    public Collection<AclEntry> listRoleAcls(Long roleId, String targetEntityNamespace, Long targetEntityId) {
        List<QueryParam> qps = QueryParam.params(AclEntry.SID_ID, roleId.toString(), AclEntry.SID_TYPE,
                AclEntry.SidType.ROLE.toString(), AclEntry.OBJECT_NAMESPACE, targetEntityNamespace,
                AclEntry.OBJECT_ID, targetEntityId.toString());
        return listAcls(qps);
    }

    public AclEntry getAcl(Long aclEntryId) {
        AclEntry aclEntry = new AclEntry();
        aclEntry.setId(aclEntryId);
        return this.dao.get(new StorableKey(AclEntry.NAMESPACE, aclEntry.getPrimaryKey()));
    }

    public AclEntry addAcl(AclEntry aclEntry) {
        if (aclEntry.getId() == null) {
            aclEntry.setId(this.dao.nextId(AclEntry.NAMESPACE));
        }
        if (aclEntry.getTimestamp() == null) {
            aclEntry.setTimestamp(System.currentTimeMillis());
        }
        validateAcl(aclEntry);
        this.dao.add(aclEntry);
        return aclEntry;
    }

    public AclEntry addOrUpdateAcl(Long id, AclEntry aclEntry) {
        validateAcl(aclEntry);
        aclEntry.setId(id);
        aclEntry.setTimestamp(System.currentTimeMillis());
        this.dao.addOrUpdate(aclEntry);
        return aclEntry;
    }

    public AclEntry removeAcl(Long id) {
        AclEntry aclEntry = new AclEntry();
        aclEntry.setId(id);
        return dao.remove(new StorableKey(AclEntry.NAMESPACE, aclEntry.getPrimaryKey()));
    }

    public boolean checkUserPermissions(String objectNamespace, Long objectId, Long userId,
            EnumSet<Permission> required) {
        User user = getUser(userId);
        if (user == null) {
            return false;
        }
        EnumSet<Permission> remaining = EnumSet.copyOf(required);
        // try direct user acl entry first
        List<QueryParam> qps = QueryParam.params(AclEntry.OBJECT_NAMESPACE, objectNamespace, AclEntry.OBJECT_ID,
                String.valueOf(objectId), AclEntry.SID_TYPE, USER.toString(), AclEntry.SID_ID,
                String.valueOf(userId));
        Collection<AclEntry> acls = listAcls(qps);
        if (acls.size() > 1) {
            throw new IllegalStateException("More than one ACL entry for " + qps);
        } else if (acls.size() == 1) {
            AclEntry aclEntry = acls.iterator().next();
            remaining.removeAll(aclEntry.getPermissions());
        }
        // try role based permissions next
        if (!remaining.isEmpty() && user.getRoles() != null) {
            qps = QueryParam.params(AclEntry.OBJECT_NAMESPACE, objectNamespace, AclEntry.OBJECT_ID,
                    String.valueOf(objectId), AclEntry.SID_TYPE, AclEntry.SidType.ROLE.toString());
            acls = listAcls(qps);
            Set<Role> userRoles = getAllUserRoles(user);
            Iterator<AclEntry> it = acls.iterator();
            while (!remaining.isEmpty() && it.hasNext()) {
                AclEntry roleEntry = it.next();
                if (userRoles.contains(getRole(roleEntry.getSidId()))) {
                    remaining.removeAll(roleEntry.getPermissions());
                }
            }
        }
        return remaining.isEmpty();
    }

    Set<Role> getAllUserRoles(User user) {
        Set<Role> userRoles = user.getRoles().stream().map(this::getRole).filter(Optional::isPresent)
                .map(Optional::get).collect(Collectors.toSet());
        Set<Role> childRoles = userRoles.stream().flatMap(role -> getChildRoles(role.getId()).stream())
                .collect(Collectors.toSet());
        return Sets.union(userRoles, childRoles);
    }

    private void validateAcl(AclEntry aclEntry) {
        Long sidId = aclEntry.getSidId();
        if (aclEntry.getSidType() == USER) {
            if (getUser(sidId) == null) {
                throw new IllegalArgumentException("No user with id: " + sidId);
            }
        } else if (aclEntry.getSidType() == ROLE) {
            if (getRole(sidId) == null) {
                throw new IllegalArgumentException("No role with id: " + sidId);
            }
        }
    }

    private Collection<User> fillRoles(Collection<User> users) {
        return users.stream().map(this::fillRoles).collect(Collectors.toList());
    }

    private User fillRoles(User user) {
        User res = null;
        if (user != null) {
            User userWithRole = new User(user);
            userWithRole.setRoles(Collections.emptySet());
            List<QueryParam> qps = QueryParam.params(UserRole.USER_ID, String.valueOf(user.getId()));
            listUserRoles(qps).forEach(userRole -> {
                userWithRole.addRole(getRole(userRole.getRoleId()).getName());
            });
            res = userWithRole;
        }
        return res;
    }

    private void validateRole(Role role) {
        Utils.requireNonEmpty(role.getName(), "Role name");
        if (StringUtils.isNumeric(role.getName())) {
            throw new IllegalArgumentException("Role name cannot be numeric");
        }
        StorageUtils.ensureUnique(role, this::listRoles, QueryParam.params(User.NAME, role.getName()));
    }

    private void validateUser(User user) {
        Utils.requireNonEmpty(user.getName(), "User name");
        if (StringUtils.isNumeric(user.getName())) {
            throw new IllegalArgumentException("User name cannot be numeric");
        }
        StorageUtils.ensureUnique(user, this::listUsers, QueryParam.params(User.NAME, user.getName()));
    }

    private void validateRoleIds(Long... ids) {
        for (Long id : ids) {
            if (getRole(id) == null) {
                throw new IllegalArgumentException("No role with id " + id);
            }
        }
    }

    private enum State {
        VISITING, VISITED
    }

    private Set<Role> doGetChildRoles(Long parentRoleId, Map<Long, State> state) {
        State curState = state.get(parentRoleId);
        Set<Role> childRoles = new HashSet<>();
        if (curState == State.VISITING) {
            throw new IllegalStateException("Cycle");
        } else if (curState != State.VISITED) {
            state.put(parentRoleId, State.VISITING);
            List<QueryParam> qps = QueryParam.params(RoleHierarchy.PARENT_ID, String.valueOf(parentRoleId));
            Collection<RoleHierarchy> res = dao.find(RoleHierarchy.NAMESPACE, qps);
            res.forEach(rh -> {
                childRoles.add(getRole(rh.getChildId()));
                childRoles.addAll(doGetChildRoles(rh.getChildId(), state));
            });
            state.put(parentRoleId, State.VISITED);
        }
        return childRoles;
    }
}