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 3 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,9 @@
# 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, RoleAssignment, RoleDefinition


__all__ = ["ApiVersion", "KeyVaultAccessControlClient", "KeyVaultPermission", "RoleAssignment", "RoleDefinition"]
Copy link
Contributor

Choose a reason for hiding this comment

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

Regrading ApiVersion, is this the standard in Python? In JavaScript we are standardizing on serviceVersion.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, it's consistent with the Python SDK.

Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# ------------------------------------
# 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 RoleAssignment, RoleDefinition
from ._internal import KeyVaultClientBase

if TYPE_CHECKING:
from typing import Any, Iterable


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, str, str, str, **Any) -> RoleAssignment
"""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: ID of the principal which will be assigned the role. This maps to the ID inside the
chlowell marked this conversation as resolved.
Show resolved Hide resolved
Active Directory. It can point to a user, service principal, or security group.
:rtype: RoleAssignment
Copy link
Contributor

Choose a reason for hiding this comment

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

should be ~azure.keyvault.administration.RoleAssignment

Copy link
Member Author

Choose a reason for hiding this comment

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

Sphinx links this correctly, the class is imported in this module.

Copy link
Contributor

Choose a reason for hiding this comment

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

oh interesting, do you know for sure if microsoft docs will?

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't. Trial balloon! 🎈

"""
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 RoleAssignment._from_generated(assignment)

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

:param str role_scope: the assignment's scope
: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: RoleAssignment
"""
assignment = self._client.role_assignments.delete(
vault_base_url=self._vault_url, scope=role_scope, role_assignment_name=str(role_assignment_name), **kwargs
)
return RoleAssignment._from_generated(assignment)

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

:param str role_scope: the assignment's scope
:param role_assignment_name: the assignment's name. Must be a UUID.
:type role_assignment_name: str or uuid.UUID
:rtype: RoleAssignment
"""
assignment = self._client.role_assignments.get(
vault_base_url=self._vault_url, scope=role_scope, role_assignment_name=str(role_assignment_name), **kwargs
)
return RoleAssignment._from_generated(assignment)

@distributed_trace
def list_role_assignments(self, role_scope, **kwargs):
# type: (str, **Any) -> Iterable[RoleAssignment]
chlowell marked this conversation as resolved.
Show resolved Hide resolved
"""List all role assignments for a scope.

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

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

:param str role_scope: scope of the role definitions
:rtype: ~azure.core.paging.ItemPaged[RoleDefinition]
"""
return self._client.role_definitions.list(
self._vault_url,
role_scope,
cls=lambda result: [RoleDefinition._from_generated(d) for d in result],
**kwargs
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
# ------------------------------------
# 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 RoleAssignment(object):
chlowell marked this conversation as resolved.
Show resolved Hide resolved
"""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 "RoleAssignment<{}>".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=RoleAssignmentProperties._from_generated(role_assignment.properties),
)


class RoleAssignmentProperties(object):
chlowell marked this conversation as resolved.
Show resolved Hide resolved
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 "RoleAssignmentProperties(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 RoleDefinition(object):
chlowell marked this conversation as resolved.
Show resolved Hide resolved
"""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 "<RoleDefinition {}>".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"]