Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[CoE] Security Roles - drift report & config #348

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions changelog/348.added
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Added module for vCenter Security Roles with functionality - list/find/save roles.
Added state for Security Roles - drift report and remediation.
joechainz marked this conversation as resolved.
Show resolved Hide resolved
1 change: 1 addition & 0 deletions docs/ref/modules/all.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Execution Modules
saltext.vmware.modules.nsxt_transport_node_profiles
saltext.vmware.modules.nsxt_transport_zone
saltext.vmware.modules.nsxt_uplink_profiles
saltext.vmware.modules.roles
saltext.vmware.modules.ssl_adapter
saltext.vmware.modules.storage_policies
saltext.vmware.modules.tag
Expand Down
6 changes: 6 additions & 0 deletions docs/ref/modules/saltext.vmware.modules.roles.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

saltext.vmware.modules.roles
============================

.. automodule:: saltext.vmware.modules.roles
:members:
1 change: 1 addition & 0 deletions docs/ref/states/all.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ State Modules
saltext.vmware.states.nsxt_transport_node_profiles
saltext.vmware.states.nsxt_transport_zone
saltext.vmware.states.nsxt_uplink_profiles
saltext.vmware.states.roles
saltext.vmware.states.storage_policies
saltext.vmware.states.tag
saltext.vmware.states.vm
Expand Down
6 changes: 6 additions & 0 deletions docs/ref/states/saltext.vmware.states.roles.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

saltext.vmware.states.roles
===========================

.. automodule:: saltext.vmware.states.roles
:members:
337 changes: 337 additions & 0 deletions src/saltext/vmware/modules/roles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,337 @@
# Copyright 2021 VMware, Inc.
# SPDX-License: Apache-2.0
import json
import logging

import salt.exceptions
import saltext.vmware.utils.connect as connect

log = logging.getLogger(__name__)

try:
from pyVmomi import pbm, VmomiSupport, SoapStubAdapter, vim

HAS_PYVMOMI = True
except ImportError:
HAS_PYVMOMI = False


__virtualname__ = "vcenter_roles"


def __virtual__():
if not HAS_PYVMOMI:
return False, "Unable to import pyVmomi module."
return __virtualname__


def _get_privilege_descriptions(authorizationManager):
privileges_desc = {}
for desciption in authorizationManager.description.privilege:
privileges_desc[desciption.key] = {"label": desciption.label, "summary": desciption.summary}
return privileges_desc


def _get_privilege_group_descriptions(authorizationManager):
privilege_groups_desc = {}
for desciption in authorizationManager.description.privilegeGroup:
privilege_groups_desc[desciption.key] = {
"label": desciption.label,
"summary": desciption.summary,
}
return privilege_groups_desc


def get_privilege_descriptions(service_instance=None, profile=None):
"""
Returns descriptions of all privileges.

service_instance
Use this vCenter service connection instance instead of creating a new one. (optional).

profile
Profile to use (optional)
"""
service_instance = service_instance or connect.get_service_instance(
config=__opts__, profile=profile
)
authorizationManager = service_instance.RetrieveContent().authorizationManager
return _get_privilege_descriptions(authorizationManager)


def get_privilege_group_descriptions(service_instance=None, profile=None):
"""
Returns descriptions of all privilege groups.

service_instance
Use this vCenter service connection instance instead of creating a new one. (optional).

profile
Profile to use (optional)
"""
service_instance = service_instance or connect.get_service_instance(
config=__opts__, profile=profile
)
authorizationManager = service_instance.RetrieveContent().authorizationManager
return _get_privilege_group_descriptions(authorizationManager)


def find(role_name=None, service_instance=None, profile=None):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion This should either get or actually search. For instance, on my current test instance, vcenter_roles.find returns one called "Resource pool administrator (sample)".

However, vcenter_roles.find administrator doesn't return any results. I have to seek for vcenter_roles.find "Resource pool administrator (sample)" in order to get the role, which is get rather than find, since I need to know the name first.

If it's important to be able to allow exact matches, I'd do something like add exact=False to the arguments, and then add a bit of logic to catch all that.

"""
Gets vCenter roles. Returns list of roles filtered by role_name or all roles if rone_name is not provided.

role_name
Filter by role name, if None returns all policies

service_instance
Use this vCenter service connection instance instead of creating a new one. (optional).

profile
Profile to use (optional)

.. code-block:: json

{
"role": "SRM Administrator",
"privileges": {
"Protection Group": [
"Assign to plan",
"Create",
"Modify",
"Remove",
"Remove from plan"
],
"Recovery Plan": [
"Configure commands",
"Create",
"Remove",
"Modify",
"Recovery"
]
}
}
"""
service_instance = service_instance or connect.get_service_instance(
config=__opts__, profile=profile
)

authorizationManager = service_instance.RetrieveContent().authorizationManager

# Collect priviliges descriptions
privileges_desc = _get_privilege_descriptions(authorizationManager)
privilege_groups_desc = _get_privilege_group_descriptions(authorizationManager)

# Collect all privilages with their descriptions
joechainz marked this conversation as resolved.
Show resolved Hide resolved
privileges = {}
for privilege in authorizationManager.privilegeList:
desciption = privileges_desc[privilege.privId]
desciption_group = privilege_groups_desc[privilege.privGroupName]
privileges[privilege.privId] = {
"name": privilege.name,
"label": desciption["label"],
"summary": desciption["summary"],
"groupName": privilege.privGroupName,
"groupLabel": desciption_group["label"],
"onParent": privilege.onParent,
}

# make JSON representation of current policies
# old_configs holds only the rules that are in the scope of interest (provided in argument config_input)
joechainz marked this conversation as resolved.
Show resolved Hide resolved
result = []
for role in authorizationManager.roleList:
if role_name is not None and role_name != role.info.label:
continue
Comment on lines +142 to +143
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If changing this to actually search, it would be:

if role_name and role_name not in role.info.label:
    continue

This would treat an empty string as role_name as the same as None, which is probably expected behavior.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also for a case insensitive match: role_name.lower() not in role.info.label.lower() (or .upper())


role_json = {
"id": role.roleId,
"roleName": role.name,
"label": role.info.label,
"description": role.info.summary,
"system": role.system,
}
role_json["privileges"] = []
for privilage_id in role.privilege:
role_privilege = privileges[privilage_id]
role_json["privileges"].append(
{
"id": privilage_id,
"name": role_privilege["label"],
"description": role_privilege["summary"],
"groupName": role_privilege["groupName"],
"groupLabel": role_privilege["groupLabel"],
"onParent": role_privilege["onParent"],
}
)
result.append(role_json)

# make JSON representation of current policies
roles_config = []
for role in result:
role_json = {"role": role["label"], "privileges": {}}
for privilege in role["privileges"]:
priv_name = privilege["name"]
group_name = privilege["groupLabel"]
if group_name not in role_json["privileges"]:
role_json["privileges"][group_name] = []
role_json["privileges"][group_name].append(priv_name)
roles_config.append(role_json)

return roles_config


def save(role_config, service_instance=None, profile=None):
"""
Create new role with given configuration, if it doesn't exist.
Otherwise update existing role.
Apply changes only for particular group from configuration.
Roles outside the groups mentioned in configuration are kept unchanged.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion Also should note that permissions that do not exist will be ignored, Like I just tried saving a role with resource privileges of ["Create resource pool", "Query vMotion", "lulz", "not real"], and this worked just fine and ignored the non-existent lulz/not real bits.


role_config
Role name and configuration values.

service_instance
Use this vCenter service connection instance instead of creating a new one. (optional).

profile
Profile to use (optional)

.. code-block:: json

{
"role": "SRM Administrator",
"privileges": {
"Protection Group": [
"Assign to plan",
"Create",
"Modify",
"Remove",
"Remove from plan"
],
"Recovery Plan": [
"Configure commands",
"Create",
"Remove",
"Modify",
"Recovery"
]
}
}
"""
service_instance = service_instance or connect.get_service_instance(
config=__opts__, profile=profile
)

authorizationManager = service_instance.RetrieveContent().authorizationManager

privileges_desc = _get_privilege_descriptions(authorizationManager)
privilege_groups_desc = _get_privilege_group_descriptions(authorizationManager)

# Collect privilages by group label and privilege name
group_privileges = {}
privilege_group_map = {}
for privilege in authorizationManager.privilegeList:
desciption_group = privilege_groups_desc[privilege.privGroupName]
group_label = desciption_group["label"]
if group_label not in group_privileges:
group_privileges[group_label] = []
group_privileges[group_label].append(privilege)
privilege_group_map[privilege.privId] = group_label

role_name = role_config["role"]

# find role to store or update
role = None
for role_obj in authorizationManager.roleList:
if role_obj.info.label == role_name:
role = role_obj
break

# group new privileges
new_privileges_by_groups = {}
for group in role_config["privileges"]:
if group not in new_privileges_by_groups:
new_privileges_by_groups[group] = []
privileges_in_group = role_config["privileges"][group]
for priv in group_privileges[group]:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion try this with a non-existent group, e.g. salt-call vcenter_roles.save '{"role": "Blerps", "privileges": {"Fnord": ["Create resource pool", "something else", "cool guy"]}}'

You should see this raise a KeyError, which is not so good 😅

priv_label = privileges_desc[priv.privId]["label"]
if priv_label in privileges_in_group:
# collect new privileges
new_privileges_by_groups[group].append(priv.privId)

# Create if role doesn't exist
if role is None:
if not role_name:
raise salt.exceptions.CommandExecutionError(f"Role name is required!")

log.debug("")
log.debug("*********************************")
log.debug("Create Role: " + role_name)
log.debug("")

role_privileges = []
for group in new_privileges_by_groups:
role_privileges += new_privileges_by_groups[group]

log.debug("Privileges:")
log.debug(json.dumps(list(role_privileges), indent=2))

authorizationManager.AddAuthorizationRole(role_name, role_privileges)
log.debug("*********************************")

return {"status": "created"}
else:
# otherwise update existing role
# apply changes only for particular group from configuration
# roles outside the groups mentioned in configuration are kept unchanged

role_privileges = []
old_privileges_by_groups = {}
for priv_name in role.privilege:
role_privileges.append(priv_name)
if priv_name in privilege_group_map:
group = privilege_group_map[priv_name]
if group not in old_privileges_by_groups:
old_privileges_by_groups[group] = []
# collect current privileges
old_privileges_by_groups[group].append(priv_name)

log.debug("")
log.debug("*********************************")
log.debug("Update Role: " + role_name)
log.debug("")
add_privileges = []
remove_priviliges = []
for group in new_privileges_by_groups:
if group in old_privileges_by_groups:
# merge group privileges
add_privileges += set(new_privileges_by_groups[group]).difference(
old_privileges_by_groups[group]
)
remove_priviliges += set(old_privileges_by_groups[group]).difference(
new_privileges_by_groups[group]
)
else:
# add new group with privileges
add_privileges += new_privileges_by_groups[group]

log.debug("Add privileges:")
log.debug(json.dumps(list(add_privileges), indent=2))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion privileges are already lists (i.e. []). FWIW, this is also always going to do json.dumps which theoretically could be a performance hit, especially for large deployments.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If order isn't important, changing them to set() and using the correct methods would be 👍

log.debug("Remove privileges:")
log.debug(json.dumps(list(remove_priviliges), indent=2))
log.debug("---------------------------")

# remove privileges from role
for priv in remove_priviliges:
role_privileges.remove(priv)

# add privileges to role
for priv in add_privileges:
role_privileges.append(priv)

log.debug("Final privileges:")
log.debug(json.dumps(list(role_privileges), indent=2))

authorizationManager.UpdateAuthorizationRole(role.roleId, role.name, role_privileges)
log.debug("*********************************")

return {"status": "updated"}
Loading