/*
 * 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.cassandra.auth;

import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.exceptions.RequestExecutionException;
import org.apache.cassandra.exceptions.UnauthorizedException;

public class Roles
{
    private static final Logger logger = LoggerFactory.getLogger(Roles.class);

    private static final Role NO_ROLE = new Role("", false, false, Collections.emptyMap(), Collections.emptySet());

    public static final RolesCache cache = new RolesCache(DatabaseDescriptor.getRoleManager(), DatabaseDescriptor::isAuthenticationRequired);

    /** Use {@link AuthCacheService#initializeAndRegisterCaches} rather than calling this directly */
    public static void init()
    {
        AuthCacheService.instance.register(cache);
    }

    /**
     * Identify all roles granted to the supplied Role, including both directly granted
     * and inherited roles.
     * This method is used where we mainly just care about *which* roles are granted to a given role,
     * including when looking up or listing permissions for a role on a given resource.
     *
     * @param primaryRole the Role
     * @return set of all granted Roles for the primary Role
     */
    public static Set<RoleResource> getRoles(RoleResource primaryRole)
    {
        return cache.getRoleResources(primaryRole);
    }

    /**
     * Get detailed info on all the roles granted to the role identified by the supplied RoleResource.
     * This includes superuser status and login privileges for the primary role and all roles granted directly
     * to it or inherited.
     * The returned roles may be cached if roles_validity > 0
     * This method is used where we need to know specific attributes of the collection of granted roles, i.e.
     * when checking for superuser status which may be inherited from *any* granted role.
     *
     * @param primaryRole identifies the role
     * @return set of detailed info for all of the roles granted to the primary
     */
    public static Set<Role> getRoleDetails(RoleResource primaryRole)
    {
        return cache.getRoles(primaryRole);
    }

    /**
     * Enumerate all the roles in the system, preferably these will be fetched from the cache, which in turn
     * may have been warmed during startup.
     */
    public static Set<RoleResource> getAllRoles()
    {
        return cache.getAllRoles();
    }

    /**
     * Gets all roles which pass the predicate.
     *
     * @param predicate a predicate to filter roles with
     * @return unmodifiable set of role resources passing the predicate
     */
    public static Set<RoleResource> getAllRoles(Predicate<RoleResource> predicate)
    {
        return getAllRoles().stream()
                            .filter(predicate)
                            .collect(Collectors.toUnmodifiableSet());
    }

    /**
     * Returns true if the supplied role or any other role granted to it
     * (directly or indirectly) has superuser status.
     *
     * @param role the primary role
     * @return true if the role has superuser status, false otherwise
     */
    public static boolean hasSuperuserStatus(RoleResource role)
    {
        try
        {
            for (Role r : getRoleDetails(role))
                if (r.isSuper)
                    return true;

            return false;
        }
        catch (RequestExecutionException e)
        {
            logger.debug("Failed to authorize {} for super-user permission", role.getRoleName());
            throw new UnauthorizedException("Unable to perform authorization of super-user permission: " + e.getMessage(), e);
        }
    }

    /**
     * Returns true if the supplied role has the login privilege. This cannot be inherited, so
     * returns true iff the named role has that bit set.
     * @param role the role identifier
     * @return true if the role has the canLogin privilege, false otherwise
     */
    public static boolean canLogin(final RoleResource role)
    {
        try
        {
            for (Role r : getRoleDetails(role))
                if (r.resource.equals(role))
                    return r.canLogin;

            return false;
        }
        catch (RequestExecutionException e)
        {
            logger.debug("Failed to authorize {} for login permission", role.getRoleName());
            throw new UnauthorizedException("Unable to perform authorization of login permission: " + e.getMessage(), e);
        }
    }

    /**
     * Returns the map of custom options for the named role. These options are not inherited from granted roles, but
     * are set directly.
     * @param role the role identifier
     * @return map of option_name -> value. If no options are set for the named role, the map will be empty
     * but never null.
     */
    public static Map<String, String> getOptions(RoleResource role)
    {
        for (Role r : getRoleDetails(role))
            if (r.resource.equals(role))
                return r.options;

        return NO_ROLE.options;
    }

   /**
    * Return the NullObject Role instance which can be safely used to indicate no information is available
    * when querying for a specific named role.
    * @return singleton null role object
    */
   public static Role nullRole()
   {
       return NO_ROLE;
   }

   /**
    * Just a convenience method which compares a role instance with the null object version, indicating if the
    * return from some query/lookup method was a valid Role or indicates that the role does not exist.
    * @param role
    * @return true if the supplied role is the null role instance, false otherwise.
    */
   public static boolean isNullRole(Role role)
   {
       return NO_ROLE.equals(role);
   }


   /**
    * Constructs a Role object from a RoleResource, using the methods of the supplied IRoleManager.
    * This is used by the default implementation of IRoleManager#getRoleDetails so that IRoleManager impls
    * which don't implement an optimized getRoleDetails remain compatible. Depending on the IRoleManager
    * implementation this could be quite heavyweight, so should not be used on any hot path.
    *
    * @param resource identifies the role
    * @param roleManager provides lookup functions to retrieve role info
    * @return Role object including superuser status, login privilege, custom options and the set of roles
    * granted to identified role.
    */
   public static Role fromRoleResource(RoleResource resource, IRoleManager roleManager)
   {
       return new Role(resource.getName(),
                       roleManager.isSuper(resource),
                       roleManager.canLogin(resource),
                       roleManager.getCustomOptions(resource),
                       roleManager.getRoles(resource, false)
                                  .stream()
                                  .map(RoleResource::getRoleName)
                                  .collect(Collectors.toSet()));
   }
}
