def _sync_dag_view_permissions(self, dag_id, access_control): """ Set the access policy on the given DAG's ViewModel. :param dag_id: the ID of the DAG whose permissions should be updated :type dag_id: str :param access_control: a dict where each key is a rolename and each value is a set() of permission names (e.g., {'can_read'} :type access_control: dict """ dag_resource_name = permissions.resource_name_for_dag(dag_id) def _get_or_create_dag_permission(perm_name): dag_perm = self.find_permission_view_menu(perm_name, dag_resource_name) if not dag_perm: self.log.info("Creating new permission '%s' on view '%s'", perm_name, dag_resource_name) dag_perm = self.add_permission_view_menu(perm_name, dag_resource_name) return dag_perm def _revoke_stale_permissions(dag_view): existing_dag_perms = self.find_permissions_view_menu(dag_view) for perm in existing_dag_perms: non_admin_roles = [role for role in perm.role if role.name != 'Admin'] for role in non_admin_roles: target_perms_for_role = access_control.get(role.name, {}) if perm.permission.name not in target_perms_for_role: self.log.info( "Revoking '%s' on DAG '%s' for role '%s'", perm.permission, dag_resource_name, role.name, ) self.del_permission_role(role, perm) dag_view = self.find_view_menu(dag_resource_name) if dag_view: _revoke_stale_permissions(dag_view) for rolename, perms in access_control.items(): role = self.find_role(rolename) if not role: raise AirflowException( "The access_control mapping for DAG '{}' includes a role " "named '{}', but that role does not exist".format(dag_id, rolename) ) perms = set(perms) invalid_perms = perms - self.DAG_PERMS if invalid_perms: raise AirflowException( "The access_control map for DAG '{}' includes the following " "invalid permissions: {}; The set of valid permissions " "is: {}".format(dag_resource_name, invalid_perms, self.DAG_PERMS) ) for perm_name in perms: dag_perm = _get_or_create_dag_permission(perm_name) self.add_permission_role(role, dag_perm)
def _sync_dag_view_permissions(self, dag_id, access_control): """ Set the access policy on the given DAG's ViewModel. :param dag_id: the ID of the DAG whose permissions should be updated :param access_control: a dict where each key is a rolename and each value is a set() of action names (e.g. {'can_read'}) """ dag_resource_name = permissions.resource_name_for_dag(dag_id) def _get_or_create_dag_permission( action_name: str) -> Optional[Permission]: perm = self.get_permission(action_name, dag_resource_name) if not perm: self.log.info("Creating new action '%s' on resource '%s'", action_name, dag_resource_name) perm = self.create_permission(action_name, dag_resource_name) return perm def _revoke_stale_permissions(resource: Resource): existing_dag_perms = self.get_resource_permissions(resource) for perm in existing_dag_perms: non_admin_roles = [ role for role in perm.role if role.name != 'Admin' ] for role in non_admin_roles: target_perms_for_role = access_control.get(role.name, {}) if perm.action.name not in target_perms_for_role: self.log.info( "Revoking '%s' on DAG '%s' for role '%s'", perm.action, dag_resource_name, role.name, ) self.remove_permission_from_role(role, perm) resource = self.get_resource(dag_resource_name) if resource: _revoke_stale_permissions(resource) for rolename, action_names in access_control.items(): role = self.find_role(rolename) if not role: raise AirflowException( f"The access_control mapping for DAG '{dag_id}' includes a role named " f"'{rolename}', but that role does not exist") action_names = set(action_names) invalid_action_names = action_names - self.DAG_ACTIONS if invalid_action_names: raise AirflowException( f"The access_control map for DAG '{dag_resource_name}' includes " f"the following invalid permissions: {invalid_action_names}; " f"The set of valid permissions is: {self.DAG_ACTIONS}") for action_name in action_names: dag_perm = _get_or_create_dag_permission(action_name) if dag_perm: self.add_permission_to_role(role, dag_perm)
def can_delete_dag(self, dag_id, user=None) -> bool: """Determines whether a user has DAG delete access.""" root_dag_id = self._get_root_dag_id(dag_id) dag_resource_name = permissions.resource_name_for_dag(root_dag_id) return self.has_access(permissions.ACTION_CAN_DELETE, dag_resource_name, user=user)
def can_read_dag(self, dag_id, user=None) -> bool: """Determines whether a user has DAG read access.""" if not user: user = g.user dag_resource_name = permissions.resource_name_for_dag(dag_id) return self._has_view_access( user, permissions.ACTION_CAN_READ, permissions.RESOURCE_DAG ) or self._has_view_access(user, permissions.ACTION_CAN_READ, dag_resource_name)
def needs_perms(dag_id: str) -> bool: dag_resource_name = resource_name_for_dag(dag_id) for permission_name in DAG_ACTIONS: if not (session.query(Permission).join(Action).join(Resource). filter(Action.name == permission_name).filter( Resource.name == dag_resource_name).one_or_none()): return True return False
def prefixed_dag_id(self, dag_id): """Returns the permission name for a DAG id.""" warnings.warn( "`prefixed_dag_id` has been deprecated. " "Please use `airflow.security.permissions.resource_name_for_dag` instead.", DeprecationWarning, stacklevel=2, ) return permissions.resource_name_for_dag(dag_id)
def can_access_some_dags(self, action: str, dag_id: Optional[str] = None) -> bool: """Checks if user has read or write access to some dags.""" if dag_id and dag_id != '~': return self.has_access(action, permissions.resource_name_for_dag(dag_id)) user = g.user if action == permissions.ACTION_CAN_READ: return any(self.get_readable_dags(user)) return any(self.get_editable_dags(user))
def can_read_dag(self, dag_id, user=None) -> bool: """Determines whether a user has DAG read access.""" if not user: user = g.user # To account for SubDags root_dag_id = dag_id.split(".")[0] dag_resource_name = permissions.resource_name_for_dag(root_dag_id) return self._has_access( user, permissions.ACTION_CAN_READ, permissions.RESOURCE_DAG ) or self._has_access(user, permissions.ACTION_CAN_READ, dag_resource_name)
def needs_perm_views(dag_id: str) -> bool: dag_resource_name = resource_name_for_dag(dag_id) for permission_name in DAG_PERMS: if not (session.query(sqla_models.PermissionView).join( sqla_models.Permission).join( sqla_models.ViewMenu).filter( sqla_models.Permission.name == permission_name ).filter(sqla_models.ViewMenu.name == dag_resource_name).one_or_none()): return True return False
def test_create_dag_specific_permissions(self, dagbag_mock): access_control = {'Public': {permissions.ACTION_CAN_READ}} dags = [ DAG('has_access_control', access_control=access_control), DAG('no_access_control'), ] collect_dags_from_db_mock = mock.Mock() dagbag = mock.Mock() dagbag.dags = {dag.dag_id: dag for dag in dags} dagbag.collect_dags_from_db = collect_dags_from_db_mock dagbag_mock.return_value = dagbag self.security_manager._sync_dag_view_permissions = mock.Mock() for dag in dags: dag_resource_name = permissions.resource_name_for_dag(dag.dag_id) all_perms = self.security_manager.get_all_permissions() assert ('can_read', dag_resource_name) not in all_perms assert ('can_edit', dag_resource_name) not in all_perms self.security_manager.create_dag_specific_permissions() dagbag_mock.assert_called_once_with(read_dags_from_db=True) collect_dags_from_db_mock.assert_called_once_with() for dag in dags: dag_resource_name = permissions.resource_name_for_dag(dag.dag_id) all_perms = self.security_manager.get_all_permissions() assert ('can_read', dag_resource_name) in all_perms assert ('can_edit', dag_resource_name) in all_perms self.security_manager._sync_dag_view_permissions.assert_called_once_with( permissions.resource_name_for_dag('has_access_control'), access_control) del dagbag.dags["has_access_control"] with assert_queries_count( 1): # one query to get all perms; dagbag is mocked self.security_manager.create_dag_specific_permissions()
def sync_perm_for_dag(self, dag_id, access_control=None): """ Sync permissions for given dag id. The dag id surely exists in our dag bag as only / refresh button or DagBag will call this function :param dag_id: the ID of the DAG whose permissions should be updated :param access_control: a dict where each key is a rolename and each value is a set() of action names (e.g., {'can_read'} :return: """ dag_resource_name = permissions.resource_name_for_dag(dag_id) for dag_action_name in self.DAG_ACTIONS: self.create_permission(dag_action_name, dag_resource_name) if access_control: self._sync_dag_view_permissions(dag_resource_name, access_control)
def create_dag_specific_permissions(self) -> None: """ Creates 'can_read' and 'can_edit' permissions for all DAGs, along with any `access_control` permissions provided in them. This does iterate through ALL the DAGs, which can be slow. See `sync_perm_for_dag` if you only need to sync a single DAG. :return: None. """ perms = self.get_all_permissions() dagbag = DagBag(read_dags_from_db=True) dagbag.collect_dags_from_db() dags = dagbag.dags.values() for dag in dags: dag_resource_name = permissions.resource_name_for_dag(dag.dag_id) for perm_name in self.DAG_PERMS: if (perm_name, dag_resource_name) not in perms: self._merge_perm(perm_name, dag_resource_name) if dag.access_control: self._sync_dag_view_permissions(dag_resource_name, dag.access_control)
def _has_dag_perm(self, perm, dag_id, user): # if not user: # user = self.user return self.security_manager.has_access( perm, permissions.resource_name_for_dag(dag_id), user)
def can_edit_dag(self, dag_id, user=None) -> bool: """Determines whether a user has DAG edit access.""" dag_resource_name = permissions.resource_name_for_dag(dag_id) return self.has_access(permissions.ACTION_CAN_EDIT, dag_resource_name, user=user)