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

Add KeyVaultAccessControlClient for data plane RBAC #13372

Merged
merged 7 commits into from
Sep 1, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,15 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# ------------------------------------
__path__ = __import__("pkgutil").extend_path(__path__, __name__) # type: ignore
from ._access_control_client import KeyVaultAccessControlClient
from ._internal.client_base import ApiVersion
from ._models import KeyVaultPermission, KeyVaultRoleAssignment, KeyVaultRoleDefinition


__all__ = [
"ApiVersion",
"KeyVaultAccessControlClient",
"KeyVaultPermission",
"KeyVaultRoleAssignment",
"KeyVaultRoleDefinition",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# ------------------------------------
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# ------------------------------------
from typing import TYPE_CHECKING

from azure.core.tracing.decorator import distributed_trace

from ._models import KeyVaultRoleAssignment, KeyVaultRoleDefinition
from ._internal import KeyVaultClientBase

if TYPE_CHECKING:
from typing import Any, Union
from uuid import UUID
from azure.core.paging import ItemPaged


class KeyVaultAccessControlClient(KeyVaultClientBase):
"""Manages role-based access to Azure Key Vault.

:param str vault_url: URL of the vault the client will manage. This is also called the vault's "DNS Name".
:param credential: an object which can provide an access token for the vault, such as a credential from
:mod:`azure.identity`
"""

# pylint:disable=protected-access

@distributed_trace
def create_role_assignment(self, role_scope, role_assignment_name, role_definition_id, principal_id, **kwargs):
Copy link
Contributor

Choose a reason for hiding this comment

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

We're using just name in JavaScript. Should I change it to something like roleAssignmentName? Or perhaps we could change it to name here? I'm tempted to lean towards shorter parameters if possible.

# type: (str, Union[str, UUID], str, str, **Any) -> KeyVaultRoleAssignment
"""Create a role assignment.

:param str role_scope: scope the role assignment will apply over
Copy link
Member

Choose a reason for hiding this comment

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

For the other languages we are using a custom type to provide some context for the valid values that can be passed here ("/", "/keys", "/keys/some specific Key resource Id"). Is there a pythonic way to accomplish the same thing here?

Copy link
Member Author

Choose a reason for hiding this comment

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

There's nothing I know of in the standard library quite like .NET's Uri. Typical Python just passes strings around. I think validating the argument with a regex is the best we could do here. I think it's unlikely but not inconceivable we'd do that; for today I'll punt 🏈

Copy link
Member

Choose a reason for hiding this comment

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

Makes sense - perhaps just some hints in the comment text would be enough.

:param role_assignment_name: a name for the role assignment. Must be a UUID.
chlowell marked this conversation as resolved.
Show resolved Hide resolved
:type role_assignment_name: str or uuid.UUID
:param str role_definition_id: ID of the role's definition
:param str principal_id: Azure Active Directory object ID of the principal which will be assigned the role. The
principal can be a user, service principal, or security group.
:rtype: KeyVaultRoleAssignment
"""
create_parameters = self._client.role_assignments.models.RoleAssignmentCreateParameters(
properties=self._client.role_assignments.models.RoleAssignmentProperties(
principal_id=principal_id, role_definition_id=str(role_definition_id)
)
)
assignment = self._client.role_assignments.create(
vault_base_url=self._vault_url,
scope=role_scope,
role_assignment_name=role_assignment_name,
parameters=create_parameters,
**kwargs
)
return KeyVaultRoleAssignment._from_generated(assignment)

@distributed_trace
def delete_role_assignment(self, role_scope, role_assignment_name, **kwargs):
# type: (str, Union[str, UUID], **Any) -> KeyVaultRoleAssignment
"""Delete a role assignment.

:param str role_scope: the assignment's scope, for example "/", "/keys", or "/keys/<specific key identifier>"
:param role_assignment_name: the assignment's name. Must be a UUID.
:type role_assignment_name: str or uuid.UUID
:returns: the deleted assignment
:rtype: KeyVaultRoleAssignment
"""
assignment = self._client.role_assignments.delete(
vault_base_url=self._vault_url, scope=role_scope, role_assignment_name=str(role_assignment_name), **kwargs
)
return KeyVaultRoleAssignment._from_generated(assignment)

@distributed_trace
def get_role_assignment(self, role_scope, role_assignment_name, **kwargs):
# type: (str, Union[str, UUID], **Any) -> KeyVaultRoleAssignment
"""Get a role assignment.

:param str role_scope: the assignment's scope, for example "/", "/keys", or "/keys/<specific key identifier>"
:param role_assignment_name: the assignment's name. Must be a UUID.
:type role_assignment_name: str or uuid.UUID
:rtype: KeyVaultRoleAssignment
"""
assignment = self._client.role_assignments.get(
vault_base_url=self._vault_url, scope=role_scope, role_assignment_name=str(role_assignment_name), **kwargs
)
return KeyVaultRoleAssignment._from_generated(assignment)

@distributed_trace
def list_role_assignments(self, role_scope, **kwargs):
# type: (str, **Any) -> ItemPaged[KeyVaultRoleAssignment]
"""List all role assignments for a scope.

:param str role_scope: scope of the role assignments
:rtype: ~azure.core.paging.ItemPaged[KeyVaultRoleAssignment]
"""
return self._client.role_assignments.list_for_scope(
self._vault_url,
role_scope,
cls=lambda result: [KeyVaultRoleAssignment._from_generated(a) for a in result],
**kwargs
)

@distributed_trace
def list_role_definitions(self, role_scope, **kwargs):
# type: (str, **Any) -> ItemPaged[KeyVaultRoleDefinition]
"""List all role definitions applicable at and above a scope.

:param str role_scope: scope of the role definitions
:rtype: ~azure.core.paging.ItemPaged[KeyVaultRoleDefinition]
"""
return self._client.role_definitions.list(
self._vault_url,
role_scope,
cls=lambda result: [KeyVaultRoleDefinition._from_generated(d) for d in result],
**kwargs
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# ------------------------------------
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# ------------------------------------
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from typing import Any


# pylint:disable=protected-access


class KeyVaultPermission(object):
"""Role definition permissions.

:ivar list[str] actions: allowed actions
:ivar list[str] not_actions: denied actions
Copy link
Contributor

Choose a reason for hiding this comment

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

side note: not sure if this has already been a design discussion, but I feel like denied_actions sounds better than not_actions

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure, that's reasonable. I don't know whether there's been a discussion either, I just copied this from @christothes 😊

Copy link
Member

Choose a reason for hiding this comment

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

I'm using denied actions as well.

:ivar list[str] data_actions: allowed data actions
:ivar list[str] not_data_actions: denied data actions
"""

def __init__(self, **kwargs):
# type: (**Any) -> None
self.actions = kwargs.get("actions")
self.not_actions = kwargs.get("not_actions")
self.data_actions = kwargs.get("data_actions")
self.not_data_actions = kwargs.get("not_data_actions")

@classmethod
def _from_generated(cls, permissions):
return cls(
actions=permissions.actions,
not_actions=permissions.not_actions,
data_actions=permissions.data_actions,
not_data_actions=permissions.not_data_actions,
)


class KeyVaultRoleAssignment(object):
"""Represents the assignment to a principal of a role over a scope"""

def __init__(self, **kwargs):
# type: (**Any) -> None
self._assignment_id = kwargs.get("assignment_id")
self._name = kwargs.get("name")
self._properties = kwargs.get("properties")
self._type = kwargs.get("assignment_type")

def __repr__(self):
# type: () -> str
return "KeyVaultRoleAssignment<{}>".format(self._assignment_id)

@property
def assignment_id(self):
# type: () -> str
"""unique identifier for this assignment"""
return self._assignment_id

@property
def name(self):
# type: () -> str
"""name of the assignment"""
return self._name

@property
def principal_id(self):
# type: () -> str
"""ID of the principal this assignment applies to.

This maps to the ID inside the Active Directory. It can point to a user, service principal, or security group.
"""
return self._properties.principal_id

@property
def role_definition_id(self):
# type: () -> str
"""ID of the role's definition"""
return self._properties.role_definition_id

@property
def scope(self):
# type: () -> str
"""scope of the assignment"""
return self._properties.scope

@property
def type(self):
# type: () -> str
"""the type of this assignment"""
return self._type

@classmethod
def _from_generated(cls, role_assignment):
return cls(
assignment_id=role_assignment.id,
name=role_assignment.name,
assignment_type=role_assignment.type,
properties=KeyVaultRoleAssignmentProperties._from_generated(role_assignment.properties),
)


class KeyVaultRoleAssignmentProperties(object):
def __init__(self, **kwargs):
# type: (**Any) -> None
self.principal_id = kwargs.get("principal_id")
self.role_definition_id = kwargs.get("role_definition_id")
self.scope = kwargs.get("scope")

def __repr__(self):
# type: () -> str
return "KeyVaultRoleAssignmentProperties(principal_id={}, role_definition_id={}, scope={})".format(
self.principal_id, self.role_definition_id, self.scope
)[:1024]

@classmethod
def _from_generated(cls, role_assignment_properties):
# the generated RoleAssignmentProperties and RoleAssignmentPropertiesWithScope
# models differ only in that the latter has a "scope" attribute
return cls(
principal_id=role_assignment_properties.principal_id,
role_definition_id=role_assignment_properties.role_definition_id,
scope=getattr(role_assignment_properties, "scope", None),
)


class KeyVaultRoleDefinition(object):
"""Role definition.

:ivar str id: The role definition ID.
:ivar str name: The role definition name.
:ivar str type: The role definition type.
:ivar str role_name: The role name.
:ivar str description: The role definition description.
:ivar str role_type: The role type.
:ivar permissions: Role definition permissions.
:vartype permissions: list[KeyVaultPermission]
:ivar list[str] assignable_scopes: Role definition assignable scopes.
"""

def __init__(self, **kwargs):
# type: (**Any) -> None
self.id = kwargs.get("id")
self.name = kwargs.get("name")
self.role_name = kwargs.get("role_name")
self.description = kwargs.get("description")
self.role_type = kwargs.get("role_type")
self.type = kwargs.get("type")
self.permissions = kwargs.get("permissions")
self.assignable_scopes = kwargs.get("assignable_scopes")

def __repr__(self):
# type: () -> str
return "<KeyVaultRoleDefinition {}>".format(self.role_name)[:1024]

@classmethod
def _from_generated(cls, definition):
return cls(
assignable_scopes=definition.assignable_scopes,
description=definition.description,
id=definition.id,
name=definition.name,
permissions=[KeyVaultPermission._from_generated(p) for p in definition.permissions],
role_name=definition.role_name,
role_type=definition.role_type,
type=definition.type,
)
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# ------------------------------------
from ._access_control_client import KeyVaultAccessControlClient

__all__ = ["KeyVaultAccessControlClient"]