Skip to content

Commit

Permalink
add new permissions support for volumes
Browse files Browse the repository at this point in the history
  • Loading branch information
DaanRademaker committed Nov 27, 2023
1 parent b4591ef commit f6c3862
Show file tree
Hide file tree
Showing 10 changed files with 748 additions and 390 deletions.
736 changes: 348 additions & 388 deletions aws-lambda/poetry.lock

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions aws-lambda/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@ requests = ">=2.22"
boto3 = "^1.21.10"
cfnresponse = "^1.1.2"
tenacity = "^8.2.2"
databricks-sdk = "^0.11.0"
databricks-sdk = "^0.13.0"

[tool.poetry.dev-dependencies]
coverage = { version = "^6.1.1", extras = ["toml"] }
pytest = "7.0.1"
pytest-mock = "3.6.1"
pyproject-flake8 = "^5.0.4"
isort = "^5.10.0"
black = "20.8b1"
black = "^22.3.0"
pytest-cov = "^4.0.0"
mypy = "^0.971"

Expand Down
12 changes: 12 additions & 0 deletions aws-lambda/src/databricks_cdk/resources/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@
create_or_update_warehouse_permissions,
delete_warehouse_permissions,
)
from databricks_cdk.resources.permissions.volume_permissions import (
VolumePermissionsProperties,
create_or_update_volume_permissions,
delete_volume_permissions,
)
from databricks_cdk.resources.scim.user import UserProperties, create_or_update_user, delete_user
from databricks_cdk.resources.secrets.secret import SecretProperties, create_or_update_secret, delete_secret
from databricks_cdk.resources.secrets.secret_scope import (
Expand Down Expand Up @@ -197,6 +202,8 @@ def create_or_update_resource(event: DatabricksEvent) -> CnfResponse:
return create_or_update_registered_model_permissions(
RegisteredModelPermissionPermissionProperties(**event.ResourceProperties)
)
elif action == "volume-permissions":
return create_or_update_volume_permissions(VolumePermissionsProperties(**event.ResourceProperties))
elif action == "experiment-permission":
return create_or_update_experiment_permissions(ExperimentPermissionProperties(**event.ResourceProperties))
elif action == "token":
Expand Down Expand Up @@ -292,6 +299,11 @@ def delete_resource(event: DatabricksEvent) -> CnfResponse:
RegisteredModelPermissionPermissionProperties(**event.ResourceProperties),
event.PhysicalResourceId,
)
elif action == "volume-permissions":
return delete_volume_permissions(
VolumePermissionsProperties(**event.ResourceProperties),
event.PhysicalResourceId,
)
elif action == "unity-storage-credentials":
return delete_storage_credential(
StorageCredentialsProperties(**event.ResourceProperties),
Expand Down
87 changes: 87 additions & 0 deletions aws-lambda/src/databricks_cdk/resources/permissions/changes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
from databricks.sdk.service.catalog import PermissionsChange, PermissionsList, Privilege, PrivilegeAssignment


def get_assignment_dict_from_permissions_list(permissions_list: PermissionsList) -> dict[str, list[Privilege]]:
"""Converts PermissionsList to dict with key of principal and list of associated privileges as value"""
privilige_assignments = permissions_list.privilege_assignments or {}

if privilige_assignments is None:
return {}

return {
x.principal: [Privilege(y) for y in x.privileges]
for x in privilige_assignments
if permissions_list.privilege_assignments is not None and x.principal is not None and x.privileges is not None
}


def get_assignment_dict_from_privilege_assignments(
privilege_assignments: list[PrivilegeAssignment],
) -> dict[str, list[Privilege]]:
"""Converts list of PrivilegeAssignment to dict with key of principal and list of associated privileges as value"""
return {
x.principal: x.privileges for x in privilege_assignments if x.principal is not None and x.privileges is not None
}


def get_permission_changes_principals(
assignments_on_databricks_dict: dict[str, list[Privilege]],
assignments_from_properties_dict: dict[str, list[Privilege]],
) -> list[PermissionsChange]:
"""See if there are new principals that need to be added"""
permission_changes = []

principals_on_databricks = set(assignments_on_databricks_dict.keys())
principals_from_properties = set(assignments_from_properties_dict.keys())

principals_to_add = principals_from_properties.difference(principals_on_databricks)
principals_to_remove = principals_on_databricks.difference(principals_from_properties)

for principal in principals_to_add:
permission_changes.append(
PermissionsChange(principal=principal, add=assignments_from_properties_dict[principal])
)

for principal in principals_to_remove:
permission_changes.append(
PermissionsChange(principal=principal, remove=assignments_on_databricks_dict[principal])
)

return permission_changes


def get_permission_changes_assignments_changed(
assignments_on_databricks_dict: dict[str, list[Privilege]],
assignments_from_properties_dict: dict[str, list[Privilege]],
) -> list[PermissionsChange]:
"""See if there are principal assignemnts that need to be updated"""
permission_changes = []
for principal, privileges in assignments_from_properties_dict.items():
privileges_databricks = set(assignments_on_databricks_dict.get(principal, []))
privileges_properties = set(privileges)

if privileges_databricks != privileges_properties and len(privileges_databricks) > 0:
to_remove = privileges_databricks.difference(privileges_properties)
to_add = privileges_properties.difference(privileges_databricks)
permission_changes.append(PermissionsChange(principal=principal, add=list(to_add), remove=list(to_remove)))

return permission_changes


def get_permission_changes(
assignments_on_databricks: PermissionsList, assignments_from_properties: list[PrivilegeAssignment]
) -> list[PermissionsChange]:
"""Get the changes between the existing grants and the new grants and create PermissionsChange object"""
# Convert to dict for easier lookup
assignments_on_databricks_dict = get_assignment_dict_from_permissions_list(assignments_on_databricks)
assignments_from_properties_dict = get_assignment_dict_from_privilege_assignments(assignments_from_properties)

permission_changes: list[PermissionsChange] = []
permission_changes += get_permission_changes_principals(
assignments_on_databricks_dict, assignments_from_properties_dict
)
permission_changes += get_permission_changes_assignments_changed(
assignments_on_databricks_dict, assignments_from_properties_dict
)

return permission_changes
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from databricks.sdk.service.catalog import PermissionsList, PrivilegeAssignment, SecurableType
from pydantic import BaseModel

from databricks_cdk.resources.permissions.changes import get_permission_changes
from databricks_cdk.utils import CnfResponse, get_workspace_client


class VolumePermissionsProperties(BaseModel):
workspace_url: str
volume_name: str
privilege_assignments: list[PrivilegeAssignment] = []


def create_or_update_volume_permissions(
properties: VolumePermissionsProperties,
) -> CnfResponse:
"""Create volume permissions on volume at databricks"""

workspace_client = get_workspace_client(properties.workspace_url)
existing_grants: PermissionsList = workspace_client.grants.get(
securable_type=SecurableType.VOLUME, full_name=properties.volume_name
)

permission_changes = get_permission_changes(existing_grants, properties.privilege_assignments)

workspace_client.grants.update(
securable_type=SecurableType.VOLUME, full_name=properties.volume_name, changes=permission_changes
)

return CnfResponse(
physical_resource_id=f"{properties.volume_name}/permissions",
)


def delete_volume_permissions(properties: VolumePermissionsProperties, physical_resource_id: str) -> CnfResponse:
"""Deletes all volume permissions on volume at databricks"""
workspace_client = get_workspace_client(properties.workspace_url)
existing_grants: PermissionsList = workspace_client.grants.get(
securable_type=SecurableType.VOLUME, full_name=properties.volume_name
)

permission_changes = get_permission_changes(existing_grants, [])

workspace_client.grants.update(
securable_type=SecurableType.VOLUME, full_name=properties.volume_name, changes=permission_changes
)

return CnfResponse(
physical_resource_id=physical_resource_id,
)
139 changes: 139 additions & 0 deletions aws-lambda/tests/resources/permissions/test_changes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
from databricks.sdk.service.catalog import PermissionsChange, PermissionsList, Privilege, PrivilegeAssignment

from databricks_cdk.resources.permissions.changes import (
get_assignment_dict_from_permissions_list,
get_assignment_dict_from_privilege_assignments,
get_permission_changes,
get_permission_changes_assignments_changed,
get_permission_changes_principals,
)


def test_get_assignment_dict_from_permissions_list():
permissions_list = PermissionsList(
**{
"privilege_assignments": [
PrivilegeAssignment(**{"principal": "principal1", "privileges": ["APPLY_TAG", "WRITE_FILES"]}),
PrivilegeAssignment(**{"principal": "principal2", "privileges": ["READ_FILES"]}),
]
}
)

result = get_assignment_dict_from_permissions_list(permissions_list)

assert result == {
"principal1": [Privilege.APPLY_TAG, Privilege.WRITE_FILES],
"principal2": [Privilege.READ_FILES],
}


def test_get_assignment_dict_from_permissions_list_empty():
permissions_list = PermissionsList(**{"privilege_assignments": None})

result = get_assignment_dict_from_permissions_list(permissions_list)

assert result == {}


def test_get_assignment_dict_from_privilege_assignments():
privilege_assignments = [
PrivilegeAssignment(**{"principal": "principal1", "privileges": [Privilege.APPLY_TAG, Privilege.WRITE_FILES]}),
PrivilegeAssignment(**{"principal": "principal2", "privileges": [Privilege.READ_FILES]}),
]

result = get_assignment_dict_from_privilege_assignments(privilege_assignments)

assert result == {
"principal1": [Privilege.APPLY_TAG, Privilege.WRITE_FILES],
"principal2": [Privilege.READ_FILES],
}


def test_get_assignment_dict_from_privilege_assignments_empty():
privilege_assignments = [
PrivilegeAssignment(**{"principal": "principal1", "privileges": None}),
PrivilegeAssignment(**{"principal": None, "privileges": [Privilege.READ_FILES]}),
]
result = get_assignment_dict_from_privilege_assignments(privilege_assignments)

assert result == {}


def test_get_permission_changes_assignments_changed():
assignments_on_databricks_dict = {"principal": list(tuple([Privilege.APPLY_TAG, Privilege.WRITE_FILES]))}
assignments_from_properties_dict = {"principal": list(tuple([Privilege.APPLY_TAG, Privilege.READ_FILES]))}

result = get_permission_changes_assignments_changed(
assignments_on_databricks_dict, assignments_from_properties_dict
)

# READ_FILES should be added WRITE_FILES should be removed
assert result == [
PermissionsChange(add=[Privilege.READ_FILES], principal="principal", remove=[Privilege.WRITE_FILES])
]


def test_get_permission_changes_assignments_changed_no_changes():
assignments_on_databricks_dict = {"principal": list(tuple([Privilege.APPLY_TAG, Privilege.WRITE_FILES]))}
assignments_from_properties_dict = {"principal": list(tuple([Privilege.APPLY_TAG, Privilege.WRITE_FILES]))}

result = get_permission_changes_assignments_changed(
assignments_on_databricks_dict, assignments_from_properties_dict
)

# No changes
assert result == []


def test_get_permission_changes_new_principals():
assignments_on_databricks_dict = {
"principal_to_remove": list(tuple([Privilege.APPLY_TAG])),
"principal_to_stay": list(tuple([Privilege.APPLY_TAG])),
}
assignments_from_properties_dict = {
"principal_to_add": list(tuple([Privilege.APPLY_TAG])),
"principal_to_stay": list(tuple([Privilege.APPLY_TAG])),
}

result = get_permission_changes_principals(assignments_on_databricks_dict, assignments_from_properties_dict)

# No new principals
assert result == [
PermissionsChange(add=[Privilege.APPLY_TAG], principal="principal_to_add", remove=None),
PermissionsChange(add=None, principal="principal_to_remove", remove=[Privilege.APPLY_TAG]),
]


def test_get_permission_changes_no_principals():
assignments_on_databricks_dict = {}
assignments_from_properties_dict = {}

result = get_permission_changes_principals(assignments_on_databricks_dict, assignments_from_properties_dict)

# No new principals
assert result == []


def test_get_permission_changes():
sample_permissions_list = PermissionsList(
privilege_assignments=[
PrivilegeAssignment(principal="user1", privileges=[Privilege.APPLY_TAG]),
PrivilegeAssignment(principal="user2", privileges=[Privilege.CREATE]),
PrivilegeAssignment(principal="userToRemove", privileges=[Privilege.CREATE]),
]
)
sample_privilege_assignments = [
PrivilegeAssignment(principal="user1", privileges=[Privilege.APPLY_TAG]),
PrivilegeAssignment(principal="user2", privileges=[Privilege.APPLY_TAG]),
PrivilegeAssignment(principal="userToAdd", privileges=[Privilege.CREATE]),
]

result = get_permission_changes(
assignments_on_databricks=sample_permissions_list, assignments_from_properties=sample_privilege_assignments
)

assert result == [
PermissionsChange(add=[Privilege.CREATE], principal="userToAdd", remove=None),
PermissionsChange(add=None, principal="userToRemove", remove=[Privilege.CREATE]),
PermissionsChange(add=[Privilege.APPLY_TAG], principal="user2", remove=[Privilege.CREATE]),
]
8 changes: 8 additions & 0 deletions typescript/src/resources/deploy-lambda.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {Workspace, WorkspaceProperties} from "./account/workspace";
import {InstanceProfile, InstanceProfileProperties} from "./instance-profiles/instance-profile";
import {Cluster, ClusterProperties} from "./clusters/cluster";
import {ClusterPermissions, ClusterPermissionsProperties} from "./permissions/cluster-permissions";
import {VolumePermissions, VolumePermissionsProperties} from "./permissions/volumePermissions";
import {DbfsFile, DbfsFileProperties} from "./dbfs/dbfs-file";
import {SecretScope, SecretScopeProperties} from "./secrets/secret-scope";
import {Job, JobProperties} from "./jobs/job";
Expand Down Expand Up @@ -178,6 +179,13 @@ export abstract class IDatabricksDeployLambda extends Construct {
});
}

public createVolumePermissions(scope: Construct, id: string, props: VolumePermissionsProperties): VolumePermissions {
return new VolumePermissions(scope, id, {
...props,
serviceToken: this.serviceToken
});
}

public createInstancePool(scope: Construct, id: string, props: InstancePoolProperties): InstancePool {
return new InstancePool(scope, id, {
...props,
Expand Down
1 change: 1 addition & 0 deletions typescript/src/resources/permissions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from "./job-permissions";
export * from "./cluster-policy-permissions";
export * from "./registered-model-permissions";
export * from "./experiment-permissions";
export * from "./volumePermissions";
36 changes: 36 additions & 0 deletions typescript/src/resources/permissions/volumePermissions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {CustomResource} from "aws-cdk-lib";
import {Construct} from "constructs";

export enum PrivilegeVolume {
APPLY_TAG = "APPLY_TAG",
READ_VOLUME = "READ_VOLUME",
WRITE_VOLUME = "WRITE_VOLUME",
ALL_PRIVILEGES = "ALL_PRIVILEGES"
}

export interface PrivilegeAssignmentVolume {
principal: string
priviliges: Array<PrivilegeVolume>
}

export interface VolumePermissionsProperties {
workspaceUrl: string
privilege_assignments: Array<PrivilegeAssignmentVolume>
}

export interface VolumePermissionsProps extends VolumePermissionsProperties {
readonly serviceToken: string
}

export class VolumePermissions extends CustomResource {
constructor(scope: Construct, id: string, props: VolumePermissionsProps) {
super(scope, id, {
serviceToken: props.serviceToken,
properties: {
action: "volume-permissions",
workspace_url: props.workspaceUrl,
privilege_assignments: props.privilege_assignments,
}
});
}
}
Loading

0 comments on commit f6c3862

Please sign in to comment.