33from collections .abc import Callable , Iterable
44from dataclasses import dataclass , replace
55from functools import partial , cached_property
6+ from typing import Protocol
67
78from databricks .labs .blueprint .installation import Installation
89from databricks .labs .blueprint .parallel import ManyError , Threads
@@ -109,6 +110,15 @@ def object_key(self) -> str:
109110 _ , key = self .this_type_and_key ()
110111 return key .lower ()
111112
113+ @property
114+ def order (self ) -> int :
115+ """Order of the grants to be upheld when applying."""
116+ match self .action_type :
117+ case "OWN" : # Apply ownership as last to avoid losing permissions for applying grants
118+ return 1
119+ case _:
120+ return 0
121+
112122 def this_type_and_key (self ):
113123 return self .type_and_key (
114124 catalog = self .catalog ,
@@ -598,17 +608,22 @@ def __init__(
598608 self ._external_locations = external_locations
599609 self ._compute_locations = cluster_locations
600610
601- def get_interactive_cluster_grants (self ) -> list [Grant ]:
611+ def get_interactive_cluster_grants (self ) -> set [Grant ]:
602612 tables = list (self ._tables_crawler .snapshot ())
603- grants : set [Grant ] = set ()
613+ grants = set [Grant ]()
604614
605- for compute_location in self ._compute_locations ():
615+ try :
616+ compute_locations = self ._compute_locations ()
617+ except DatabricksError as e :
618+ logger .warning ("No compute locations found." , exc_info = e )
619+ return grants
620+ for compute_location in compute_locations :
606621 principals = self ._get_cluster_principal_mapping (compute_location .compute_id , compute_location .compute_type )
607622 if len (principals ) == 0 :
608623 continue
609624 cluster_usage = self ._get_grants (compute_location .locations , principals , tables )
610625 grants .update (cluster_usage )
611- return list ( grants )
626+ return grants
612627
613628 def _get_privilege (self , table : Table , locations : dict [str , str ]) -> str | None :
614629 if table .view_text is not None :
@@ -726,6 +741,27 @@ def _get_location_name(self, location_url: str):
726741 return None
727742
728743
744+ class SecurableObject (Protocol ):
745+ """A protocol for a securable object.
746+
747+ Docs:
748+ https://docs.databricks.com/en/data-governance/table-acls/object-privileges.html#securable-objects-in-the-hive-metastore
749+ """
750+
751+ @property
752+ def kind (self ) -> str :
753+ """The type of securable objects, see doc referenced above."""
754+
755+ @property
756+ def full_name (self ) -> str :
757+ """The object name often a synonym for `key`"""
758+
759+ @property
760+ def key (self ) -> str :
761+ """The object identifier often a synonym for `full_name`"""
762+ return self .full_name
763+
764+
729765class MigrateGrants :
730766 def __init__ (
731767 self ,
@@ -737,20 +773,22 @@ def __init__(
737773 self ._group_manager = group_manager
738774 self ._grant_loaders = grant_loaders
739775
740- def apply (self , src : Table , uc_table_key : str ) -> bool :
776+ def apply (self , src : SecurableObject , dst : SecurableObject ) -> bool :
741777 for grant in self ._match_grants (src ):
742- acl_migrate_sql = grant .uc_grant_sql (src .kind , uc_table_key )
778+ acl_migrate_sql = grant .uc_grant_sql (dst .kind , dst . full_name )
743779 if acl_migrate_sql is None :
744780 logger .warning (
745781 f"failed-to-migrate: Hive metastore grant '{ grant .action_type } ' cannot be mapped to UC grant for "
746- f"{ src .kind } '{ uc_table_key } '. Skipping."
782+ f"{ dst .kind } '{ dst . full_name } '. Skipping."
747783 )
748784 continue
749- logger .debug (f"Migrating acls on { uc_table_key } using SQL query: { acl_migrate_sql } " )
785+ logger .debug (f"Migrating acls on { dst . full_name } using SQL query: { acl_migrate_sql } " )
750786 try :
751787 self ._sql_backend .execute (acl_migrate_sql )
752788 except DatabricksError as e :
753- logger .warning (f"failed-to-migrate: Failed to migrate ACL for { src .key } to { uc_table_key } : { e } " )
789+ logger .warning (
790+ f"failed-to-migrate: Failed to migrate ACL for { src .full_name } to { dst .full_name } " , exc_info = e
791+ )
754792 return True
755793
756794 @cached_property
@@ -765,16 +803,14 @@ def _grants(self) -> list[Grant]:
765803 grants .append (grant )
766804 return grants
767805
768- def _match_grants (self , table : Table ) -> list [Grant ]:
806+ def _match_grants (self , src : SecurableObject ) -> list [Grant ]:
769807 matched_grants = []
770808 for grant in self ._grants :
771- if grant .database != table .database :
772- continue
773- if table .name not in (grant .table , grant .view ):
809+ if grant .object_key != src .key :
774810 continue
775811 grant = self ._replace_account_group (grant )
776812 matched_grants .append (grant )
777- return matched_grants
813+ return sorted ( matched_grants , key = lambda g : g . order )
778814
779815 def _replace_account_group (self , grant : Grant ) -> Grant :
780816 target_principal = self ._workspace_to_account_group_names .get (grant .principal )
0 commit comments