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

WIP: Add databricks permissions command #314

Open
wants to merge 28 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
2088c40
Add databricks permissions command
Aug 17, 2020
407abe7
Remove extra file
Aug 17, 2020
631f20b
Fix acidental removal of tests/__init__.py
Aug 17, 2020
c5075e1
tests/permissions/__init__.py was missing the license
Aug 17, 2020
18daab4
Merge branch 'master' into add-permissions
Sep 18, 2020
6f08afd
WIP: Add missing bits in the workspace API
Sep 18, 2020
4e17f06
Fix an issue where the if statements were checking type but raising a…
Sep 18, 2020
123a805
Remove WorkspaceApi import
Sep 18, 2020
c00458b
Fix a typo in WorkspaceFileInfo. We cannot change the order of argum…
Sep 18, 2020
4b7302c
get_id_for_directory was moved from the permissions api to the worksp…
Sep 18, 2020
e10f6f6
Fix lint errors about un-grouped imports
Sep 22, 2020
e8d55c0
Add some documentation showing all of the various urls that the permi…
Sep 22, 2020
849944c
databricks permissions:
Sep 22, 2020
5b59855
databricks cli permissions
Sep 22, 2020
12da975
Fix a lint error in databricks_cli/sdk/permissions.py caused by the t…
Sep 22, 2020
2c9af5e
databricks permissions:
Sep 22, 2020
eef1210
databricks permssions
Sep 23, 2020
0d9e5eb
Fix an issue with OneOfOption where the error would say Missing None.
Sep 23, 2020
d49cd78
databricks permssions
Sep 23, 2020
cf37d75
databricks permissions
Sep 23, 2020
cccf174
databricks permissions
Sep 23, 2020
25e710f
Add more tests of the permissions cli, still need to test the permiss…
Sep 23, 2020
6ca0a00
Fix lint errors
Sep 23, 2020
cfe232e
Merge remote-tracking branch 'upstream/master' into add-permissions
Sep 23, 2020
d9ba966
databricks permissions
Sep 23, 2020
fb5f354
databricks permissions:
Sep 23, 2020
83c6688
Checkpoint trying to cleanup how the permissions are managed to make …
Sep 24, 2020
b275af6
Merge branch 'master' into add-permissions
areese Oct 2, 2020
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 databricks_cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

from databricks_cli.configure.config import profile_option, debug_option
from databricks_cli.libraries.cli import libraries_group
from databricks_cli.permissions.cli import permissions_group
from databricks_cli.version import print_version_callback, version
from databricks_cli.utils import CONTEXT_SETTINGS
from databricks_cli.configure.cli import configure_cli
Expand Down Expand Up @@ -65,6 +66,7 @@ def cli():
cli.add_command(tokens_group, name='tokens')
cli.add_command(instance_pools_group, name="instance-pools")
cli.add_command(pipelines_group, name='pipelines')
cli.add_command(permissions_group, name='permissions')

if __name__ == "__main__":
cli()
3 changes: 2 additions & 1 deletion databricks_cli/click_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ def __init__(self, *args, **kwargs):
def handle_parse_result(self, ctx, opts, args):
cleaned_opts = set([o.replace('_', '-') for o in opts.keys()])
if len(cleaned_opts.intersection(set(self.one_of))) == 0:
raise MissingParameter('One of {} must be provided.'.format(self.one_of))
raise MissingParameter('One of {} must be provided.'.format(self.one_of), param=self)
if len(cleaned_opts.intersection(set(self.one_of))) > 1:
raise UsageError('Only one of {} should be provided.'.format(self.one_of))
return super(OneOfOption, self).handle_parse_result(ctx, opts, args)
Expand All @@ -124,6 +124,7 @@ class OptionalOneOfOption(Option):
def __init__(self, *args, **kwargs):
self.one_of = kwargs.pop('one_of')
super(OptionalOneOfOption, self).__init__(*args, **kwargs)
self.param_hint = self.one_of

def handle_parse_result(self, ctx, opts, args):
cleaned_opts = set([o.replace('_', '-') for o in opts.keys()])
Expand Down
Empty file.
343 changes: 343 additions & 0 deletions databricks_cli/permissions/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,343 @@
# Databricks CLI
# Copyright 2017 Databricks, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"), except
# that the use of services to which certain application programming
# interfaces (each, an "API") connect requires that the user first obtain
# a license for the use of the APIs from Databricks, Inc. ("Databricks"),
# by creating an account at www.databricks.com and agreeing to either (a)
# the Community Edition Terms of Service, (b) the Databricks Terms of
# Service, or (c) another written agreement between Licensee and Databricks
# for the use of the APIs.
#
# You may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from enum import Enum

from databricks_cli.sdk.permissions_service import PermissionsService
from .exceptions import PermissionsError


class PermissionTargets(Enum):
clusters = 'clusters'
Copy link
Contributor

Choose a reason for hiding this comment

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

logically this should rather be a dictionary or list of tuples, because it's mixing two different things under same enumeration.

e.g. cluster is the object type and clusters is the prefix for object it,

cluster = clusters
directories = 'directories'
directory = directories
instance_pools = 'instance-pools'
instance_pool = instance_pools
jobs = 'jobs'
job = jobs
notebooks = 'notebooks'
notebook = notebooks
registered_models = 'registered-models'
registered_model = registered_models
model = registered_models
models = registered_models

@classmethod
def values(cls):
return [e.value for e in PermissionTargets]

@classmethod
def help_values(cls):
return ', '.join([e.value for e in PermissionTargets])

@classmethod
def get(cls, item):
if '-' in item:
item = item.replace('-', '_')
return PermissionTargets[item]


class PermissionLevel(Enum):
no_permissions = 'NONE'
manage = 'CAN_MANAGE'
manage_staging_versions = 'CAN_MANAGE_STAGING_VERSIONS'
manage_production_versions = 'CAN_MANAGE_PRODUCTION_VERSIONS'
restart = 'CAN_RESTART'
attach = 'CAN_ATTACH_TO'
manage_run = 'CAN_MANAGE_RUN'
owner = 'IS_OWNER'
view = 'CAN_VIEW'
read = 'CAN_READ'
run = 'CAN_RUN'
edit = 'CAN_EDIT'
use = 'CAN_USE'

@classmethod
def names(cls):
return [e.name for e in PermissionLevel]

@classmethod
def values(cls):
return [e.value for e in PermissionLevel]

@classmethod
def help_values(cls):
return ', '.join([e.value for e in PermissionLevel])


class BasicPermissions(object):
Copy link
Contributor

Choose a reason for hiding this comment

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

it looks over-engineered. please simplify.

technically, it all can be replaced by single simple lookup structure.

def __init__(self, object_type, valid_permissions):
self.object_type = object_type
self.valid_permissions = valid_permissions

def is_valid_target(self, permission):
# type: (str) -> bool
return permission in self.valid_permissions

def valid_targets(self):
return [s.name for s in self.valid_permissions]


class TokenPermissions(BasicPermissions):
def __init__(self):
super().__init__(PermissionTargets.token, {
PermissionLevel.no_permissions,
PermissionLevel.use,
PermissionLevel.manage,
})


class PasswordPermissions(BasicPermissions):
def __init__(self):
super().__init__(PermissionTargets.password, {
PermissionLevel.no_permissions,
PermissionLevel.use,
})


class ClusterPermissions(BasicPermissions):
def __init__(self):
super().__init__(PermissionTargets.clusters, {
PermissionLevel.no_permissions,
PermissionLevel.attach,
PermissionLevel.restart,
PermissionLevel.manage,
})


class InstancePoolPermissions(BasicPermissions):
def __init__(self):
super().__init__(PermissionTargets.instance_pools, {
PermissionLevel.no_permissions,
PermissionLevel.attach,
PermissionLevel.manage,
})


class JobPermissions(BasicPermissions):
def __init__(self):
super().__init__(PermissionTargets.jobs, {
PermissionLevel.no_permissions,
PermissionLevel.view,
PermissionLevel.manage_run,
PermissionLevel.owner,
PermissionLevel.manage,
})


class NotebookPermissions(BasicPermissions):
def __init__(self):
super().__init__(PermissionTargets.notebook, {
PermissionLevel.no_permissions,
PermissionLevel.read,
PermissionLevel.run,
PermissionLevel.edit,
PermissionLevel.manage,
})


class DirectoryPermissions(BasicPermissions):
def __init__(self):
super().__init__(PermissionTargets.directory, {
PermissionLevel.no_permissions,
PermissionLevel.read,
PermissionLevel.run,
PermissionLevel.edit,
PermissionLevel.manage,
})


class MlFlowPermissions(BasicPermissions):
def __init__(self):
super().__init__(PermissionTargets.models, {
PermissionLevel.no_permissions,
PermissionLevel.read,
PermissionLevel.edit,
PermissionLevel.manage_staging_versions,
PermissionLevel.manage_production_versions,
PermissionLevel.manage,
})


class PermissionType(Enum):
user = 'user_name'
group = 'group_name'
service = 'service_principal_name'

@classmethod
def values(cls):
return [e.value for e in PermissionType]


class PermissionsLookup(object):
"""
static lookup table for permissions
"""

items = {
'CAN_MANAGE': PermissionLevel.manage,
'CAN_RESTART': PermissionLevel.restart,
'CAN_ATTACH_TO': PermissionLevel.attach,
'CAN_MANAGE_RUN': PermissionLevel.manage_run,
'IS_OWNER': PermissionLevel.owner,
'CAN_VIEW': PermissionLevel.view,
'CAN_READ': PermissionLevel.read,
'CAN_RUN': PermissionLevel.run,
'CAN_EDIT': PermissionLevel.edit,
'user_name': PermissionType.user,
'group_name': PermissionType.group,
'service_principal_name': PermissionType.service,

'clusters': ClusterPermissions(),
'cluster': ClusterPermissions(),
'directories': DirectoryPermissions(),
'directory': DirectoryPermissions(),
'instance-pools': InstancePoolPermissions(),
'instance_pools': InstancePoolPermissions(),
'jobs': JobPermissions(),
'job': JobPermissions(),
'notebooks': NotebookPermissions(),
'notebook': NotebookPermissions(),
'registered-models': MlFlowPermissions(),
'registered_models': MlFlowPermissions(),
'model': MlFlowPermissions(),
'models': MlFlowPermissions(),
}


class Permission(object):
def __init__(self, object_type, permission_type, permission_level, permission_value):
# type: (str, PermissionType, str, str) -> None
self.validator = PermissionsLookup.items[object_type]

if not self.validator.is_valid_target(permission_level):
raise PermissionsError(
'{} is not a valid target for {}\n'.format(permission_level,
self.validator.object_type) +

'Valid values are {}'.format(self.validator.valid_targets()))

self.permission_type = permission_type
self.permission_level = PermissionLevel[permission_level]
self.value = permission_value

def to_dict(self):
# type: () -> dict
if not self.permission_type or not self.permission_level:
return {}

return {
self.permission_type.value: self.value,
'permission_level': self.permission_level.value
}


class PermissionsObject(object):
def __init__(self, permissions=None):
if not permissions:
permissions = []
self.permissions = permissions

def add(self, permission):
# type: (Permission) -> None
self.permissions.append(permission)

def user(self, name, level):
# type: (str, PermissionLevel) -> None
self.add(Permission(PermissionType.user, value=name, permission_level=level))

def group(self, name, level):
# type: (str, PermissionLevel) -> None
self.add(Permission(PermissionType.group, value=name, permission_level=level))

def service(self, name, level):
# type: (str, PermissionLevel) -> None
self.add(Permission(PermissionType.service, value=name, permission_level=level))

def to_dict(self):
# type: () -> dict
if not self.permissions:
return {}

return {
'access_control_list': [entry.to_dict() for entry in self.permissions]
}

def check_if_valid_for(self, object_type):
"""
Check if the permissions are valid for this object type.
"""
pass


# FIXME: add set/update permissions, right now this is read only.
class PermissionsApi(object):

Choose a reason for hiding this comment

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

This class doesn't expose a way to set / update permissions.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you, that's a good point I'll fix it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll add update, but looking at https://docs.databricks.com/dev-tools/api/latest/permissions.html#operation/update-all-notebook-permissions

I'm kinda skeptical of set_permissions. The issue I have is it's dangerous, you could wipe all permission out everywhere if you are not careful.

I'm going to leave that out of this pr, and start a thread with engineering to determine if that's something that should be exposed.

def __init__(self, api_client):
self.api_client = api_client
self.client = PermissionsService(api_client)

def get_permissions(self, object_type, object_id):
# type: (str, str) -> dict
if not object_type:
raise PermissionsError('object_type is invalid')

if not object_id:
object_id = ''
# raise PermissionsError('object_id is invalid')

return self.client.get_permissions(object_type=PermissionTargets.get(object_type).value,
object_id=object_id)

def get_possible_permissions(self, object_type, object_id):
# type: (str, str) -> dict
if not object_type:
raise PermissionsError('object_type is invalid')

if not object_id:
raise PermissionsError('object_id is invalid')

return self.client.get_possible_permissions(
object_type=PermissionTargets.get(object_type).value,
object_id=object_id)

def add_permissions(self, object_type, object_id, permissions):
# type: (str, str, PermissionsObject) -> dict
if not object_type:
raise PermissionsError('object_type is invalid')

if not object_id:
raise PermissionsError('object_id is invalid')

return self.client.add_permissions(object_type=PermissionTargets.get(object_type).value,
object_id=object_id, data=permissions.to_dict())

def update_permissions(self, object_type, object_id, permissions):
# type: (str, str, PermissionsObject) -> dict
if not object_type:
raise PermissionsError('object_type is invalid')

if not object_id:
raise PermissionsError('object_id is invalid')

return self.client.update_permissions(object_type=PermissionTargets.get(object_type).value,
object_id=object_id, data=permissions.to_dict())
Loading