mirror of https://github.com/apache/superset.git
Add access control over metrics (#584)
* Add the new field "is_restricted" to SqlMetric and DruidMetric * Add the access control on metrics * Add the more descriptions on is_restricted * Update docs/security.rst * Update docs/security.rst
This commit is contained in:
parent
55baab413a
commit
4c6026fdda
|
@ -0,0 +1,47 @@
|
|||
"""Add new field 'is_restricted' to SqlMetric and DruidMetric
|
||||
|
||||
Revision ID: d8bc074f7aad
|
||||
Revises: 1226819ee0e3
|
||||
Create Date: 2016-06-07 12:33:25.756640
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'd8bc074f7aad'
|
||||
down_revision = '1226819ee0e3'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from caravel import db
|
||||
from caravel import models
|
||||
|
||||
|
||||
def upgrade():
|
||||
with op.batch_alter_table('metrics', schema=None) as batch_op:
|
||||
batch_op.add_column(
|
||||
sa.Column('is_restricted', sa.Boolean(), nullable=True))
|
||||
|
||||
with op.batch_alter_table('sql_metrics', schema=None) as batch_op:
|
||||
batch_op.add_column(
|
||||
sa.Column('is_restricted', sa.Boolean(), nullable=True))
|
||||
|
||||
bind = op.get_bind()
|
||||
session = db.Session(bind=bind)
|
||||
|
||||
session.query(models.DruidMetric).update({
|
||||
'is_restricted': False
|
||||
})
|
||||
session.query(models.SqlMetric).update({
|
||||
'is_restricted': False
|
||||
})
|
||||
|
||||
session.commit()
|
||||
session.close()
|
||||
|
||||
|
||||
def downgrade():
|
||||
with op.batch_alter_table('sql_metrics', schema=None) as batch_op:
|
||||
batch_op.drop_column('is_restricted')
|
||||
|
||||
with op.batch_alter_table('metrics', schema=None) as batch_op:
|
||||
batch_op.drop_column('is_restricted')
|
|
@ -38,9 +38,10 @@ from sqlalchemy.orm import relationship
|
|||
from sqlalchemy.sql import table, literal_column, text, column
|
||||
from sqlalchemy_utils import EncryptedType
|
||||
|
||||
from caravel import app, db, get_session, utils
|
||||
import caravel
|
||||
from caravel import app, db, get_session, utils, sm
|
||||
from caravel.viz import viz_types
|
||||
from caravel.utils import flasher
|
||||
from caravel.utils import flasher, MetricPermException
|
||||
|
||||
config = app.config
|
||||
|
||||
|
@ -858,12 +859,20 @@ class SqlMetric(Model, AuditMixinNullable):
|
|||
'SqlaTable', backref='metrics', foreign_keys=[table_id])
|
||||
expression = Column(Text)
|
||||
description = Column(Text)
|
||||
is_restricted = Column(Boolean, default=False, nullable=True)
|
||||
|
||||
@property
|
||||
def sqla_col(self):
|
||||
name = self.metric_name
|
||||
return literal_column(self.expression).label(name)
|
||||
|
||||
@property
|
||||
def perm(self):
|
||||
return (
|
||||
"{parent_name}.[{obj.metric_name}](id:{obj.id})"
|
||||
).format(obj=self,
|
||||
parent_name=self.table.full_name)
|
||||
|
||||
|
||||
class TableColumn(Model, AuditMixinNullable):
|
||||
|
||||
|
@ -1135,11 +1144,25 @@ class DruidDatasource(Model, AuditMixinNullable, Queryable):
|
|||
conf.get('fn', "/"),
|
||||
conf.get('fields', []),
|
||||
conf.get('name', ''))
|
||||
|
||||
aggregations = {
|
||||
m.metric_name: m.json_obj
|
||||
for m in self.metrics
|
||||
if m.metric_name in all_metrics
|
||||
}
|
||||
}
|
||||
|
||||
rejected_metrics = [
|
||||
m.metric_name for m in self.metrics
|
||||
if m.is_restricted and
|
||||
m.metric_name in aggregations.keys() and
|
||||
not sm.has_access('metric_access', m.perm)
|
||||
]
|
||||
|
||||
if rejected_metrics:
|
||||
raise MetricPermException(
|
||||
"Access to the metrics denied: " + ', '.join(rejected_metrics)
|
||||
)
|
||||
|
||||
granularity = granularity or "all"
|
||||
if granularity != "all":
|
||||
granularity = utils.parse_human_timedelta(
|
||||
|
@ -1329,6 +1352,7 @@ class DruidMetric(Model, AuditMixinNullable):
|
|||
enable_typechecks=False)
|
||||
json = Column(Text)
|
||||
description = Column(Text)
|
||||
is_restricted = Column(Boolean, default=False, nullable=True)
|
||||
|
||||
@property
|
||||
def json_obj(self):
|
||||
|
@ -1338,6 +1362,12 @@ class DruidMetric(Model, AuditMixinNullable):
|
|||
obj = {}
|
||||
return obj
|
||||
|
||||
@property
|
||||
def perm(self):
|
||||
return (
|
||||
"{parent_name}.[{obj.metric_name}](id:{obj.id})"
|
||||
).format(obj=self,
|
||||
parent_name=self.datasource.full_name)
|
||||
|
||||
class DruidColumn(Model, AuditMixinNullable):
|
||||
|
||||
|
@ -1428,6 +1458,7 @@ class DruidColumn(Model, AuditMixinNullable):
|
|||
'fieldNames': [self.column_name]})
|
||||
))
|
||||
session = get_session()
|
||||
new_metrics = []
|
||||
for metric in metrics:
|
||||
m = (
|
||||
session.query(M)
|
||||
|
@ -1438,9 +1469,12 @@ class DruidColumn(Model, AuditMixinNullable):
|
|||
)
|
||||
metric.datasource_name = self.datasource_name
|
||||
if not m:
|
||||
new_metrics.append(metric)
|
||||
session.add(metric)
|
||||
session.flush()
|
||||
|
||||
utils.init_metrics_perm(caravel, new_metrics)
|
||||
|
||||
|
||||
class FavStar(Model):
|
||||
__tablename__ = 'favstar'
|
||||
|
|
|
@ -29,6 +29,10 @@ class CaravelSecurityException(CaravelException):
|
|||
pass
|
||||
|
||||
|
||||
class MetricPermException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def flasher(msg, severity=None):
|
||||
"""Flask's flash if available, logging call if not"""
|
||||
try:
|
||||
|
@ -211,6 +215,23 @@ def init(caravel):
|
|||
for table_perm in table_perms:
|
||||
merge_perm(sm, 'datasource_access', table_perm)
|
||||
|
||||
init_metrics_perm(caravel)
|
||||
|
||||
|
||||
def init_metrics_perm(caravel, metrics=None):
|
||||
db = caravel.db
|
||||
models = caravel.models
|
||||
sm = caravel.appbuilder.sm
|
||||
|
||||
if metrics is None:
|
||||
metrics = []
|
||||
for model in [models.SqlMetric, models.DruidMetric]:
|
||||
metrics += list(db.session.query(model).all())
|
||||
|
||||
metric_perms = [metric.perm for metric in metrics]
|
||||
for metric_perm in metric_perms:
|
||||
merge_perm(sm, 'metric_access', metric_perm)
|
||||
|
||||
|
||||
def datetime_f(dttm):
|
||||
"""Formats datetime to take less room when it is recent"""
|
||||
|
|
|
@ -7,6 +7,7 @@ from __future__ import unicode_literals
|
|||
import json
|
||||
import logging
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
|
@ -29,6 +30,7 @@ from sqlalchemy.sql.expression import TextAsFrom
|
|||
from werkzeug.routing import BaseConverter
|
||||
from wtforms.validators import ValidationError
|
||||
|
||||
import caravel
|
||||
from caravel import appbuilder, db, models, viz, utils, app, sm, ascii_art
|
||||
|
||||
config = app.config
|
||||
|
@ -209,14 +211,19 @@ appbuilder.add_view_no_menu(DruidColumnInlineView)
|
|||
|
||||
class SqlMetricInlineView(CompactCRUDMixin, CaravelModelView): # noqa
|
||||
datamodel = SQLAInterface(models.SqlMetric)
|
||||
list_columns = ['metric_name', 'verbose_name', 'metric_type']
|
||||
list_columns = ['metric_name', 'verbose_name', 'metric_type',
|
||||
'is_restricted']
|
||||
edit_columns = [
|
||||
'metric_name', 'description', 'verbose_name', 'metric_type',
|
||||
'expression', 'table']
|
||||
'expression', 'table', 'is_restricted']
|
||||
description_columns = {
|
||||
'expression': utils.markdown(
|
||||
"a valid SQL expression as supported by the underlying backend. "
|
||||
"Example: `count(DISTINCT userid)`", True),
|
||||
'is_restricted': _("Whether the access to this metric is restricted "
|
||||
"to certain roles. Only roles with the permission "
|
||||
"'metric access on XXX (the name of this metric)' "
|
||||
"are allowed to access this metric"),
|
||||
}
|
||||
add_columns = edit_columns
|
||||
page_size = 500
|
||||
|
@ -228,15 +235,20 @@ class SqlMetricInlineView(CompactCRUDMixin, CaravelModelView): # noqa
|
|||
'expression': _("SQL Expression"),
|
||||
'table': _("Table"),
|
||||
}
|
||||
|
||||
def post_add(self, new_item):
|
||||
utils.init_metrics_perm(caravel, [new_item])
|
||||
|
||||
appbuilder.add_view_no_menu(SqlMetricInlineView)
|
||||
|
||||
|
||||
class DruidMetricInlineView(CompactCRUDMixin, CaravelModelView): # noqa
|
||||
datamodel = SQLAInterface(models.DruidMetric)
|
||||
list_columns = ['metric_name', 'verbose_name', 'metric_type']
|
||||
list_columns = ['metric_name', 'verbose_name', 'metric_type',
|
||||
'is_restricted']
|
||||
edit_columns = [
|
||||
'metric_name', 'description', 'verbose_name', 'metric_type', 'json',
|
||||
'datasource']
|
||||
'datasource', 'is_restricted']
|
||||
add_columns = edit_columns
|
||||
page_size = 500
|
||||
validators_columns = {
|
||||
|
@ -248,6 +260,10 @@ class DruidMetricInlineView(CompactCRUDMixin, CaravelModelView): # noqa
|
|||
"[Druid Post Aggregation]"
|
||||
"(http://druid.io/docs/latest/querying/post-aggregations.html)",
|
||||
True),
|
||||
'is_restricted': _("Whether the access to this metric is restricted "
|
||||
"to certain roles. Only roles with the permission "
|
||||
"'metric access on XXX (the name of this metric)' "
|
||||
"are allowed to access this metric"),
|
||||
}
|
||||
label_columns = {
|
||||
'metric_name': _("Metric"),
|
||||
|
@ -257,6 +273,11 @@ class DruidMetricInlineView(CompactCRUDMixin, CaravelModelView): # noqa
|
|||
'json': _("JSON"),
|
||||
'datasource': _("Druid Datasource"),
|
||||
}
|
||||
|
||||
def post_add(self, new_item):
|
||||
utils.init_metrics_perm(caravel, [new_item])
|
||||
|
||||
|
||||
appbuilder.add_view_no_menu(DruidMetricInlineView)
|
||||
|
||||
|
||||
|
@ -819,11 +840,9 @@ class Caravel(BaseView):
|
|||
@expose("/checkbox/<model_view>/<id_>/<attr>/<value>", methods=['GET'])
|
||||
def checkbox(self, model_view, id_, attr, value):
|
||||
"""endpoint for checking/unchecking any boolean in a sqla model"""
|
||||
model = None
|
||||
if model_view == 'TableColumnInlineView':
|
||||
model = models.TableColumn
|
||||
elif model_view == 'DruidColumnInlineView':
|
||||
model = models.DruidColumn
|
||||
views = sys.modules[__name__]
|
||||
model_view_cls = getattr(views, model_view)
|
||||
model = model_view_cls.datamodel.obj
|
||||
|
||||
obj = db.session.query(model).filter_by(id=id_).first()
|
||||
if obj:
|
||||
|
|
|
@ -68,3 +68,25 @@ you to create your own roles, and union them to existing ones.
|
|||
|
||||
The best way to go is probably to give user ``Gamma`` plus another role
|
||||
that would add specific permissions needed by this type of users.
|
||||
|
||||
|
||||
Restricting the access to the metrics
|
||||
-------------------------------------
|
||||
Sometimes some metrics are relatively sensitive (e.g. revenue).
|
||||
We may want to restrict those metrics to only a few roles.
|
||||
For example, assumed there is a metric ``[cluster1].[datasource1].[revenue]``
|
||||
and only Admin users are allowed to see it. Here’s how to restrict the access.
|
||||
|
||||
1. Edit the datasource (``Menu -> Source -> Druid datasources -> edit the
|
||||
record "datasource1"``) and go to the tab ``List Druid Metric``. Check
|
||||
the checkbox ``Is Restricted`` in the row of the metric ``revenue``.
|
||||
|
||||
2. Edit the role (``Menu -> Security -> List Roles -> edit the record
|
||||
“Admin”``), in the permissions field, type-and-search the permission
|
||||
``metric access on [cluster1].[datasource1].[revenue] (id: 1)``, then
|
||||
click the Save button on the bottom of the page.
|
||||
|
||||
Any users without the permission will see the error message
|
||||
*Access to the metrics denied: revenue (Status: 500)* in the slices.
|
||||
It also happens when the user wants to access a post-aggregation metric that
|
||||
is dependent on revenue.
|
||||
|
|
Loading…
Reference in New Issue