security.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  1. # pylint: disable=C,R,W
  2. """A set of constants and methods to manage permissions and security"""
  3. import logging
  4. from flask import g
  5. from flask_appbuilder.security.sqla import models as ab_models
  6. from flask_appbuilder.security.sqla.manager import SecurityManager
  7. from sqlalchemy import or_
  8. from superset import sql_parse
  9. from superset.connectors.connector_registry import ConnectorRegistry
  10. READ_ONLY_MODEL_VIEWS = {
  11. 'DatabaseAsync',
  12. 'DatabaseView',
  13. 'DruidClusterModelView',
  14. }
  15. GAMMA_READ_ONLY_MODEL_VIEWS = {
  16. 'SqlMetricInlineView',
  17. 'TableColumnInlineView',
  18. 'TableModelView',
  19. 'DruidColumnInlineView',
  20. 'DruidDatasourceModelView',
  21. 'DruidMetricInlineView',
  22. } | READ_ONLY_MODEL_VIEWS
  23. ADMIN_ONLY_VIEW_MENUS = {
  24. 'AccessRequestsModelView',
  25. 'Manage',
  26. 'SQL Lab',
  27. 'Queries',
  28. 'Refresh Druid Metadata',
  29. 'ResetPasswordView',
  30. 'RoleModelView',
  31. 'Security',
  32. 'UserDBModelView',
  33. 'UserLDAPModelView',
  34. 'UserOAuthModelView',
  35. 'UserOIDModelView',
  36. 'UserRemoteUserModelView',
  37. }
  38. ALPHA_ONLY_VIEW_MENUS = {
  39. 'Upload a CSV',
  40. }
  41. ADMIN_ONLY_PERMISSIONS = {
  42. 'all_database_access',
  43. 'can_sql_json', # TODO: move can_sql_json to sql_lab role
  44. 'can_override_role_permissions',
  45. 'can_sync_druid_source',
  46. 'can_override_role_permissions',
  47. 'can_approve',
  48. 'can_update_role',
  49. }
  50. READ_ONLY_PERMISSION = {
  51. 'can_show',
  52. 'can_list',
  53. }
  54. ALPHA_ONLY_PERMISSIONS = set([
  55. 'muldelete',
  56. 'all_datasource_access',
  57. ])
  58. OBJECT_SPEC_PERMISSIONS = set([
  59. 'database_access',
  60. 'schema_access',
  61. 'datasource_access',
  62. 'metric_access',
  63. ])
  64. class SupersetSecurityManager(SecurityManager):
  65. def get_schema_perm(self, database, schema):
  66. if schema:
  67. return '[{}].[{}]'.format(database, schema)
  68. def can_access(self, permission_name, view_name, user=None):
  69. """Protecting from has_access failing from missing perms/view"""
  70. if not user:
  71. user = g.user
  72. if user.is_anonymous:
  73. return self.is_item_public(permission_name, view_name)
  74. return self._has_view_access(user, permission_name, view_name)
  75. def all_datasource_access(self, user=None):
  76. return self.can_access(
  77. 'all_datasource_access', 'all_datasource_access', user=user)
  78. def database_access(self, database, user=None):
  79. return (
  80. self.can_access(
  81. 'all_database_access', 'all_database_access', user=user) or
  82. self.can_access('database_access', database.perm, user=user)
  83. )
  84. def schema_access(self, datasource, user=None):
  85. return (
  86. self.database_access(datasource.database, user=user) or
  87. self.all_datasource_access(user=user) or
  88. self.can_access('schema_access', datasource.schema_perm, user=user)
  89. )
  90. def datasource_access(self, datasource, user=None):
  91. return (
  92. self.schema_access(datasource, user=user) or
  93. self.can_access('datasource_access', datasource.perm, user=user)
  94. )
  95. def get_datasource_access_error_msg(self, datasource):
  96. return """This endpoint requires the datasource {}, database or
  97. `all_datasource_access` permission""".format(datasource.name)
  98. def get_datasource_access_link(self, datasource):
  99. from superset import conf
  100. return conf.get('PERMISSION_INSTRUCTIONS_LINK')
  101. def get_table_access_error_msg(self, table_name):
  102. return """You need access to the following tables: {}, all database access or
  103. `all_datasource_access` permission""".format(table_name)
  104. def get_table_access_link(self, tables):
  105. from superset import conf
  106. return conf.get('PERMISSION_INSTRUCTIONS_LINK')
  107. def datasource_access_by_name(
  108. self, database, datasource_name, schema=None):
  109. from superset import db
  110. if self.database_access(database) or self.all_datasource_access():
  111. return True
  112. schema_perm = self.get_schema_perm(database, schema)
  113. if schema and self.can_access('schema_access', schema_perm):
  114. return True
  115. datasources = ConnectorRegistry.query_datasources_by_name(
  116. db.session, database, datasource_name, schema=schema)
  117. for datasource in datasources:
  118. if self.can_access('datasource_access', datasource.perm):
  119. return True
  120. return False
  121. def get_schema_and_table(self, table_in_query, schema):
  122. table_name_pieces = table_in_query.split('.')
  123. if len(table_name_pieces) == 2:
  124. table_schema = table_name_pieces[0]
  125. table_name = table_name_pieces[1]
  126. else:
  127. table_schema = schema
  128. table_name = table_name_pieces[0]
  129. return (table_schema, table_name)
  130. def datasource_access_by_fullname(
  131. self, database, table_in_query, schema):
  132. table_schema, table_name = self.get_schema_and_table(table_in_query, schema)
  133. return self.datasource_access_by_name(
  134. database, table_name, schema=table_schema)
  135. def rejected_datasources(self, sql, database, schema):
  136. superset_query = sql_parse.SupersetQuery(sql)
  137. return [
  138. t for t in superset_query.tables if not
  139. self.datasource_access_by_fullname(database, t, schema)]
  140. def user_datasource_perms(self):
  141. datasource_perms = set()
  142. for r in g.user.roles:
  143. for perm in r.permissions:
  144. if (
  145. perm.permission and
  146. 'datasource_access' == perm.permission.name):
  147. datasource_perms.add(perm.view_menu.name)
  148. return datasource_perms
  149. def schemas_accessible_by_user(self, database, schemas, hierarchical=True):
  150. from superset import db
  151. from superset.connectors.sqla.models import SqlaTable
  152. if (hierarchical and
  153. (self.database_access(database) or
  154. self.all_datasource_access())):
  155. return schemas
  156. subset = set()
  157. for schema in schemas:
  158. schema_perm = self.get_schema_perm(database, schema)
  159. if self.can_access('schema_access', schema_perm):
  160. subset.add(schema)
  161. perms = self.user_datasource_perms()
  162. if perms:
  163. tables = (
  164. db.session.query(SqlaTable)
  165. .filter(
  166. SqlaTable.perm.in_(perms),
  167. SqlaTable.database_id == database.id,
  168. )
  169. .all()
  170. )
  171. for t in tables:
  172. if t.schema:
  173. subset.add(t.schema)
  174. return sorted(list(subset))
  175. def accessible_by_user(self, database, datasource_names, schema=None):
  176. from superset import db
  177. if self.database_access(database) or self.all_datasource_access():
  178. return datasource_names
  179. if schema:
  180. schema_perm = self.get_schema_perm(database, schema)
  181. if self.can_access('schema_access', schema_perm):
  182. return datasource_names
  183. user_perms = self.user_datasource_perms()
  184. user_datasources = ConnectorRegistry.query_datasources_by_permissions(
  185. db.session, database, user_perms)
  186. if schema:
  187. names = {
  188. d.table_name
  189. for d in user_datasources if d.schema == schema}
  190. return [d for d in datasource_names if d in names]
  191. else:
  192. full_names = {d.full_name for d in user_datasources}
  193. return [d for d in datasource_names if d in full_names]
  194. def merge_perm(self, permission_name, view_menu_name):
  195. # Implementation copied from sm.find_permission_view_menu.
  196. # TODO: use sm.find_permission_view_menu once issue
  197. # https://github.com/airbnb/superset/issues/1944 is resolved.
  198. permission = self.find_permission(permission_name)
  199. view_menu = self.find_view_menu(view_menu_name)
  200. pv = None
  201. if permission and view_menu:
  202. pv = self.get_session.query(self.permissionview_model).filter_by(
  203. permission=permission, view_menu=view_menu).first()
  204. if not pv and permission_name and view_menu_name:
  205. self.add_permission_view_menu(permission_name, view_menu_name)
  206. def is_user_defined_permission(self, perm):
  207. return perm.permission.name in OBJECT_SPEC_PERMISSIONS
  208. def create_custom_permissions(self):
  209. # Global perms
  210. self.merge_perm('all_datasource_access', 'all_datasource_access')
  211. self.merge_perm('all_database_access', 'all_database_access')
  212. def create_missing_perms(self):
  213. """Creates missing perms for datasources, schemas and metrics"""
  214. from superset import db
  215. from superset.models import core as models
  216. logging.info(
  217. 'Fetching a set of all perms to lookup which ones are missing')
  218. all_pvs = set()
  219. for pv in self.get_session.query(self.permissionview_model).all():
  220. if pv.permission and pv.view_menu:
  221. all_pvs.add((pv.permission.name, pv.view_menu.name))
  222. def merge_pv(view_menu, perm):
  223. """Create permission view menu only if it doesn't exist"""
  224. if view_menu and perm and (view_menu, perm) not in all_pvs:
  225. self.merge_perm(view_menu, perm)
  226. logging.info('Creating missing datasource permissions.')
  227. datasources = ConnectorRegistry.get_all_datasources(db.session)
  228. for datasource in datasources:
  229. merge_pv('datasource_access', datasource.get_perm())
  230. merge_pv('schema_access', datasource.schema_perm)
  231. logging.info('Creating missing database permissions.')
  232. databases = db.session.query(models.Database).all()
  233. for database in databases:
  234. merge_pv('database_access', database.perm)
  235. logging.info('Creating missing metrics permissions')
  236. metrics = []
  237. for datasource_class in ConnectorRegistry.sources.values():
  238. metrics += list(db.session.query(datasource_class.metric_class).all())
  239. for metric in metrics:
  240. if metric.is_restricted:
  241. merge_pv('metric_access', metric.perm)
  242. def clean_perms(self):
  243. """FAB leaves faulty permissions that need to be cleaned up"""
  244. logging.info('Cleaning faulty perms')
  245. sesh = self.get_session
  246. pvms = (
  247. sesh.query(ab_models.PermissionView)
  248. .filter(or_(
  249. ab_models.PermissionView.permission == None, # NOQA
  250. ab_models.PermissionView.view_menu == None, # NOQA
  251. ))
  252. )
  253. deleted_count = pvms.delete()
  254. sesh.commit()
  255. if deleted_count:
  256. logging.info('Deleted {} faulty permissions'.format(deleted_count))
  257. def sync_role_definitions(self):
  258. """Inits the Superset application with security roles and such"""
  259. from superset import conf
  260. logging.info('Syncing role definition')
  261. self.create_custom_permissions()
  262. # Creating default roles
  263. self.set_role('Admin', self.is_admin_pvm)
  264. self.set_role('Alpha', self.is_alpha_pvm)
  265. self.set_role('Gamma', self.is_gamma_pvm)
  266. self.set_role('granter', self.is_granter_pvm)
  267. self.set_role('sql_lab', self.is_sql_lab_pvm)
  268. if conf.get('PUBLIC_ROLE_LIKE_GAMMA', False):
  269. self.set_role('Public', self.is_gamma_pvm)
  270. self.create_missing_perms()
  271. # commit role and view menu updates
  272. self.get_session.commit()
  273. self.clean_perms()
  274. def set_role(self, role_name, pvm_check):
  275. logging.info('Syncing {} perms'.format(role_name))
  276. sesh = self.get_session
  277. pvms = sesh.query(ab_models.PermissionView).all()
  278. pvms = [p for p in pvms if p.permission and p.view_menu]
  279. role = self.add_role(role_name)
  280. role_pvms = [p for p in pvms if pvm_check(p)]
  281. role.permissions = role_pvms
  282. sesh.merge(role)
  283. sesh.commit()
  284. def is_admin_only(self, pvm):
  285. # not readonly operations on read only model views allowed only for admins
  286. if (pvm.view_menu.name in READ_ONLY_MODEL_VIEWS and
  287. pvm.permission.name not in READ_ONLY_PERMISSION):
  288. return True
  289. return (
  290. pvm.view_menu.name in ADMIN_ONLY_VIEW_MENUS or
  291. pvm.permission.name in ADMIN_ONLY_PERMISSIONS
  292. )
  293. def is_alpha_only(self, pvm):
  294. if (pvm.view_menu.name in GAMMA_READ_ONLY_MODEL_VIEWS and
  295. pvm.permission.name not in READ_ONLY_PERMISSION):
  296. return True
  297. return (
  298. pvm.view_menu.name in ALPHA_ONLY_VIEW_MENUS or
  299. pvm.permission.name in ALPHA_ONLY_PERMISSIONS
  300. )
  301. def is_admin_pvm(self, pvm):
  302. return not self.is_user_defined_permission(pvm)
  303. def is_alpha_pvm(self, pvm):
  304. return not (self.is_user_defined_permission(pvm) or self.is_admin_only(pvm))
  305. def is_gamma_pvm(self, pvm):
  306. return not (self.is_user_defined_permission(pvm) or self.is_admin_only(pvm) or
  307. self.is_alpha_only(pvm))
  308. def is_sql_lab_pvm(self, pvm):
  309. return (
  310. pvm.view_menu.name in {
  311. 'SQL Lab', 'SQL Editor', 'Query Search', 'Saved Queries',
  312. } or
  313. pvm.permission.name in {
  314. 'can_sql_json', 'can_csv', 'can_search_queries', 'can_sqllab_viz',
  315. 'can_sqllab',
  316. } or
  317. (pvm.view_menu.name == 'UserDBModelView' and
  318. pvm.permission.name == 'can_list'))
  319. def is_granter_pvm(self, pvm):
  320. return pvm.permission.name in {
  321. 'can_override_role_permissions', 'can_approve',
  322. }
  323. def set_perm(self, mapper, connection, target): # noqa
  324. if target.perm != target.get_perm():
  325. link_table = target.__table__
  326. connection.execute(
  327. link_table.update()
  328. .where(link_table.c.id == target.id)
  329. .values(perm=target.get_perm()),
  330. )
  331. # add to view menu if not already exists
  332. permission_name = 'datasource_access'
  333. view_menu_name = target.get_perm()
  334. permission = self.find_permission(permission_name)
  335. view_menu = self.find_view_menu(view_menu_name)
  336. pv = None
  337. if not permission:
  338. permission_table = self.permission_model.__table__ # noqa: E501 pylint: disable=no-member
  339. connection.execute(
  340. permission_table.insert()
  341. .values(name=permission_name),
  342. )
  343. permission = self.find_permission(permission_name)
  344. if not view_menu:
  345. view_menu_table = self.viewmenu_model.__table__ # pylint: disable=no-member
  346. connection.execute(
  347. view_menu_table.insert()
  348. .values(name=view_menu_name),
  349. )
  350. view_menu = self.find_view_menu(view_menu_name)
  351. if permission and view_menu:
  352. pv = self.get_session.query(self.permissionview_model).filter_by(
  353. permission=permission, view_menu=view_menu).first()
  354. if not pv and permission and view_menu:
  355. permission_view_table = self.permissionview_model.__table__ # noqa: E501 pylint: disable=no-member
  356. connection.execute(
  357. permission_view_table.insert()
  358. .values(
  359. permission_id=permission.id,
  360. view_menu_id=view_menu.id,
  361. ),
  362. )