# Copyright (c) 2019-2022 VMware, Inc.  All rights reserved.
# -- VMware Confidential
# coding: utf-8

import logging
import os

from cis.cisreglib import (
    _get_syscfg_info, LookupServiceClient, SsoClient, AuthzClient,
    CisregOptionParser)
from cis.vecs import SsoGroup, vmafd_machine_id
from wcpconfigure import (
    create_sol_user, configure_wcp_os_user, LocalOsUser, _WCPSVC_OS_USER, )
import vmafd
import featureState


logger = logging.getLogger(__name__)


# Add the description of your changes to CHANGELOG
CHANGELOG = ["Ensure that NsxAdministrators group is created and that wcp "
             "solution user is a member of that group."
             "Ensure that VM Services privileges and VM Services "
             "Administrator role are created."
             "Ensure that Namespace SelfService privileges and Namespace "
             "SelfService Operator Conntroller role are created."
             "Ensure that roles are updated with new privileges after "
             "patching."
             "Grant new Supervisor Upgrade privilege to roles created by "
             "customers that previously had equivalent access through "
             "Namespaces.Manage."
             "Grant wcpsvc local os user permissions to "
             "read from necessary vecs stores"
             "Remove wcpsvc local os user from group cis"]


# NSX Groups for SSO Authentication.
NSX_GROUP_ADMINS = 'NsxAdministrators'
NSX_GROUP_VI_ADMINS = 'NsxViAdministrators'
NSX_GROUP_AUDITORS = 'NsxAuditors'


# Desired SSO Groups with their descriptions.
DESIRED_SSO_GROUPS = {
    NSX_GROUP_ADMINS: "SSO group to view and modify NSX configuration.",
    NSX_GROUP_VI_ADMINS: "SSO group to manage NSX.",
    NSX_GROUP_AUDITORS: "SSO group to view NSX configuration."
}


GROUP_ROLE = {
    NSX_GROUP_AUDITORS: 'NsxAuditor',
    NSX_GROUP_VI_ADMINS: 'NsxViAdministrator',
    NSX_GROUP_ADMINS: 'NsxAdministrator'
}


SYSTEM_PRIVILEGES = {
    "System.Anonymous",
    "System.Read",
    "System.View"
}

PRIVILEGE_NAMESPACES_MANAGE = 'Namespaces.Manage'
PRIVILEGE_NAMESPACES_UPGRADE = 'Namespaces.Upgrade'

# IDs for built-in Roles, taken from com.vmware.cis.core.util.AclConstants.
BUILT_IN_ROLES = {
    "ROLE_ADMIN": -1,
    "ROLE_READ_ONLY": -2,
    "ROLE_VIEW": -3,
    "ROLE_ANONYMOUS": -4,
    "ROLE_NO_ACCESS": -5,
    "ROLE_NOCRYPTO_ADMIN": -6,
    "ROLE_TRUSTED_ADMIN": -7,
    "ROLE_NO_TRUSTED_ADMIN": -8
}

CIS_GROUP = "cis"


class SsoPatch:
    def __init__(self):
        vmafdClient = vmafd.client('localhost')
        username = vmafdClient.GetMachineName()
        password = vmafdClient.GetMachinePassword()
        self.sso_group = SsoGroup(username, password)

    def ensure_groups_exist(self, groups):
        """
        Tries to create all the groups specified if they do not exist.

        :param groups: list of groups that should exist.
        :type groups: list
        :rtype: list
        """
        created = []
        for name, description in groups.items():
            logger.debug('Attempting to create group "%s"', name)
            if self.sso_group.create(name, description):
                logger.info('Added "%s" group', name)
                created.append(name)
            else:
                # SsoGroup.create returns False when group already exists
                logger.debug('Group "%s" already exists', name)
        return created

    def assign_users_to_groups(self):
        """
        Ensures that users are assigned to given groups.

        :param user_groups: dictionary mapping user name to a list of groups
        that the user should be a member of.
        :type user_groups: dict
        :rtype: None
        """
        logger.debug('Creating solution user and assigning the user to SSO groups')
        create_sol_user()


class AuthzPatch:
    def __init__(self):
        """
        Initializes AuthzClient needed for patching roles and permissions
        """
        ls_url, self.domain_name = _get_syscfg_info()
        logger.debug("Connecting to Lookup Service, url=%s", ls_url)
        ls_obj = LookupServiceClient(ls_url, retry_count=1)
        sts_url, sts_cert_data = ls_obj.get_sts_endpoint_data()

        logger.debug("Logging into SSO AdminClient as machine solution user, "
                     "url=%s", sts_url)
        MACHINE_NAME = 'machine'
        self.sso_client = SsoClient.init_with_vecs(sts_url, sts_cert_data,
                                                   MACHINE_NAME, MACHINE_NAME)
        ls_obj.set_sso_client(self.sso_client)
        authz_url, _ = ls_obj.get_authz_endpoint_data()
        logger.debug("Connecting to Authz, url=%s", authz_url)
        self.authz_client = AuthzClient(authz_url, self.sso_client)
        logger.debug("Successfully initialized AuthzClient")

    def load_roles_from_file(self, role_def_file):
        """
        Returns a list roles based on the content of role_def_file.

        :param role_def_file: Path to an XML file containing role data.
        :type role_def_file: str
        :rtype: list[Role]
        """
        cisreg_options = {
            'permission.newRole': role_def_file
        }
        cisreg_optparser = CisregOptionParser(cisreg_options)
        return cisreg_optparser.get_roles()

    def setup_roles(self, role_def_file):
        """
        Creates all roles defined in role_def_file. Ensures roles have any all
        privileges defined in role_def_file.

        :param role_def_file: path to XML file containing role definitions.
        :type role_def_file: str
        :rtype: int
        """
        expected_roles = self.load_roles_from_file(role_def_file)
        logger.debug("Ensuring roles exist: %s", expected_roles)
        added = self.authz_client.load_roles(expected_roles)
        logger.info("Successfully loaded authz roles, added %d new roles",
                    added)

        all_vc_roles = {role.name: role for role in
                        self.authz_client._authz_service.GetRoles()}
        for expected_role in expected_roles:
            logger.info("Checking if privileges should be updated for role %s"
                        % (expected_role))
            vc_role = all_vc_roles.get(expected_role["name"])
            if not vc_role:
                raise Exception("Role %s not found in VC."
                                % (expected_role["name"]))
            self.update_role_with_new_privileges(vc_role, expected_role)

        return added

    def ensure_privileges_exist(self, privileges_def_file):
        """
        Creates all privileges defined in privileges_def_file.

        :param privileges_def_file: path to XML file containing role
        definitions.
        :type privileges_def_file: str
        """
        cisreg_options = {
            'permission.newPrivilege': privileges_def_file
        }
        cisreg_optparser = CisregOptionParser(cisreg_options)
        privsList = cisreg_optparser.get_privs()
        logger.debug("Ensuring privileges exist: %s", privsList)
        self.authz_client.load_privs(privsList)
        logger.info("Successfully updated wcp privileges: %s" % privsList)

    def assign_groups_to_roles(self, group_role_map):
        """
        Assign groups to roles as specified in group_role_map.

        :param group_role_map: map of global privileges linking group to role.
        :type group_role_map: dict
        :rtype: None
        """
        for group, role in group_role_map.items():
            logger.debug("Creating global permission for group %s and role %s",
                         group, role)
            # Last boolean argument in set_permission is "isGroup".
            self.authz_client.set_permission(
                self.domain_name, role, group, True)

    def update_role_with_new_privileges(self, vc_role, expected_role):
        """
        Update role with new privileges added in role_def_file.

        :param vc_role: vc role to be updated if required
        :type vc_role: dataservice.accesscontrol.Role
        :param expected_role: role info from role_def_file file
        :type expected_role: dict
        :rtype: None
        """
        all_privileges_for_role = set(expected_role["priv_ids"])
        existing_privileges_for_role = set(vc_role.privilegeId)

        logger.debug("Privileges to be assigned to role: [%s]. "
                     "Privileges currently assigned to role: [%s]"
                     % (all_privileges_for_role, existing_privileges_for_role))
        # This ensures an existing role is updated with new privileges added.
        # Removing privileges from an existing role is not supported.
        privileges_to_be_added \
            = all_privileges_for_role - existing_privileges_for_role
        privileges_to_be_removed \
            = (existing_privileges_for_role - all_privileges_for_role
                - SYSTEM_PRIVILEGES)

        if privileges_to_be_added:
            privileges_for_role \
                = list(privileges_to_be_added | existing_privileges_for_role)
            logger.info("Updating role %s with privileges [%s]"
                        % (vc_role.name, privileges_for_role))
            self.authz_client.update_role(vc_role.id, privileges_for_role)
        else:
            logger.info("No new privileges to grant to %s" % vc_role.name)
        if privileges_to_be_removed:
            logger.error("Removing privileges %s from %s role is not supported"
                         % (privileges_to_be_removed, vc_role.name))

    def does_privilege_exist(self, privilegeId):
        """
        Checks if the given privilege exists.

        :param privilegeId: ID of the VC privilege to check for
        :type privilegeId: str
        :rtype: bool
        """
        all_privileges = self.authz_client._authz_service.GetPrivileges()
        for privilege in all_privileges:
            if privilege.id == privilegeId:
                return True
        return False

    def get_external_roles_to_update(self, wcp_role_def_file):
        """
        Checks if existing roles need their privileges updated. Currently
        handles Namespaces.Upgrade by checking if the privilege doesn't exist
        yet. Roles with Namespace.Manage privilege should be updated to include
        Namespaces.Upgrade to retain the same level of API access.

        :param wcp_role_def_file: path to XML file containing role definitions.
        :type wcp_role_def_file: str
        :rtype: list[str]
        """
        if self.does_privilege_exist(PRIVILEGE_NAMESPACES_UPGRADE):
            logger.debug("%s already exists." % PRIVILEGE_NAMESPACES_UPGRADE)
            return []

        all_vc_roles = self.authz_client._authz_service.GetRoles()
        roles_with_manage = list(filter(lambda r: PRIVILEGE_NAMESPACES_MANAGE in
            r.privilegeId, all_vc_roles))

        exclude_roles = [r["name"] for r in self.load_roles_from_file(
            wcp_role_def_file)]

        # Admin roles automatically get new privileges and should be skipped.
        def should_update_role(role):
            if role.name in exclude_roles:
                return False
            if role.id in BUILT_IN_ROLES.values():
                return False
            return True

        return list(filter(should_update_role, roles_with_manage))

    def update_external_roles(self, roles_to_update):
        """
        Adds new privilege to preexisting Roles. Currently handles roles that
        need Namespaces.Upgrade.

        :param roles_to_update: List of roles to update privileges for.
        :type roles_to_update: list[dataservice.accesscontrol.Role]
        :rtype: None
        """
        for role in roles_to_update:
            logger.debug("Updaing privileges for role %s" % role.name)
            privileges_for_role = role.privilegeId.copy()
            privileges_for_role.append(PRIVILEGE_NAMESPACES_UPGRADE)
            expected_role = {
                "priv_ids": privileges_for_role
            }
            self.update_role_with_new_privileges(role, expected_role)


def patch_sso():
    # Verify that all the needed SSO groups exist.
    sso_patch = SsoPatch()
    sso_patch.ensure_groups_exist(DESIRED_SSO_GROUPS)

    # Verify that solution users are members of desired groups.
    sso_patch.assign_users_to_groups()


def patch_authz(feature_state):
    # Verify that all desired roles exist.
    authz_patch = AuthzPatch()
    roles_to_update = authz_patch.get_external_roles_to_update(
        '/usr/lib/vmware-wcp/roles.xml')
    authz_patch.ensure_privileges_exist('/usr/lib/vmware-wcp/privileges.xml')
    authz_patch.setup_roles('/usr/lib/vmware-wcp/roles.xml')
    authz_patch.update_external_roles(roles_to_update)

    # If Namespace SelfService FSS is enabled, create Namespace Operator Roles
    if hasattr(feature_state, 'getWCP_Namespace_SelfService') and \
            feature_state.getWCP_Namespace_SelfService():
        authz_patch.ensure_privileges_exist(
            '/usr/lib/vmware-wcp/nsservice-privileges.xml')
        authz_patch.setup_roles('/usr/lib/vmware-wcp/nsop-roles.xml')

    if (hasattr(feature_state, 'getPodVMOnVDS') and
            feature_state.getPodVMOnVDS()):
        authz_patch.setup_roles('/usr/lib/vmware-wcp/netoperator-roles.xml')
        authz_patch.setup_roles('/usr/lib/vmware-wcp/PodVmLifeCycleManager-roles.xml')

    # Verify that groups are mapped to roles.
    authz_patch.assign_groups_to_roles(GROUP_ROLE)


def doPatchingWithDependencies():
    # It is no necessary for wcp user to be part of cis group
    # to access any shared files, removing wcp user from cis here.
    wcp_user = LocalOsUser(_WCPSVC_OS_USER)
    wcp_user.remove_user_from_group(CIS_GROUP)

    featureState.init(enableLogging=True)
    # In order to make changes to solution users and/or roles, other services
    # must be running. So these patching steps
    # would need to happen here.
    # Verify that all the needed SSO groups exist and user membership is
    # updated.
    patch_sso()
    # Verify that roles are created and that global privileges exist.
    patch_authz(featureState)
    # Grant wcpsvc local os user necessary permissions.
    configure_wcp_os_user()


def doPatchingWithoutDependencies():
    # doPatchingWithoutDependencies is executed before services that wcpsvc
    # depends on are started. Any patching that depend on other services
    # may fail if added here. Good candidates to place here are things
    # that might not require some other services to be up & running -
    # for example change file system perms, ownership of files, etc.
    # This function is currently invoked during b2b/day0 upgrade,
    # post rpm install.
    pass


def getChanges():
    return '%s: Changelog:\n%s' % (
        os.path.basename(__file__),
        '\n'.join(['* ' + line for line in CHANGELOG]))
