Skip to content

Commit

Permalink
Merge pull request apache#11 from ASUS-AICS/aics_privacy_control_tabl…
Browse files Browse the repository at this point in the history
…e_perm_autorevoke

Aics privacy control table perm autorevoke
  • Loading branch information
ArcherTsai authored May 8, 2020
2 parents 46fcd18 + 55727dd commit 8d2aaa9
Show file tree
Hide file tree
Showing 9 changed files with 474 additions and 51 deletions.
2 changes: 1 addition & 1 deletion superset/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ def _try_json_readfile(filepath):
WTF_CSRF_ENABLED = True

# Add endpoints that need to be exempt from CSRF protection
WTF_CSRF_EXEMPT_LIST = ["superset.views.core.log", "superset.views.core.sql_csv_api"]
WTF_CSRF_EXEMPT_LIST = ["superset.views.core.log", "superset.views.core.sql_csv_api", "superset.views.core.revoke_expired_perm"]

# Whether to run the web server in debug mode or not
DEBUG = os.environ.get("FLASK_ENV") == "development"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); 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.
"""Add AICS table permission control model
Revision ID: 9847d62ead51
Revises: 5ad4d4f30245
Create Date: 2020-05-06 15:23:35.200160
"""

# revision identifiers, used by Alembic.
revision = '9847d62ead51'
down_revision = '5ad4d4f30245'

from alembic import op
import sqlalchemy as sa


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('aics_table_permission',
sa.Column('created_on', sa.DateTime(), nullable=True),
sa.Column('changed_on', sa.DateTime(), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.Column('apply_date', sa.Date(), nullable=False),
sa.Column('expire_date', sa.Date(), nullable=False),
sa.Column('force_terminate_date', sa.DateTime(), nullable=True),
sa.Column('is_active', sa.Boolean()),
sa.Column('created_by_fk', sa.Integer(), nullable=True),
sa.Column('changed_by_fk', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['changed_by_fk'], ['ab_user.id'], ),
sa.ForeignKeyConstraint(['created_by_fk'], ['ab_user.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['ab_user.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('aics_tableperm_permissionview',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('tableperm_id', sa.Integer(), nullable=True),
sa.Column('permissionview_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['permissionview_id'], ['ab_permission_view.id'], ),
sa.ForeignKeyConstraint(['tableperm_id'], ['aics_table_permission.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('tableperm_id', 'permissionview_id')
)
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('aics_tableperm_permissionview')
op.drop_table('aics_table_permission')
# ### end Alembic commands ###
2 changes: 1 addition & 1 deletion superset/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from . import core, schedules, sql_lab, user_attributes
from . import core, schedules, sql_lab, user_attributes, table_permission
112 changes: 112 additions & 0 deletions superset/models/table_permission.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); 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.
import re
from datetime import datetime, timedelta

from flask_appbuilder import Model
from sqlalchemy import (
Table,
Column,
ForeignKey,
Integer,
Date,
DateTime,
Boolean,
Sequence,
UniqueConstraint,
)
from sqlalchemy.orm import relationship

from superset import security_manager
from superset.models.helpers import AuditMixinNullable

# Table for many-to-many relation between users and tables
# Ref: https://docs.sqlalchemy.org/en/13/orm/basic_relationships.html#relationships-many-to-many
assoc_tableperm_permissionview = Table(
"aics_tableperm_permissionview",
Model.metadata,
Column('id', Integer, Sequence("aics_tableperm_permissionview_id_seq"), primary_key=True) ,
Column('tableperm_id', Integer, ForeignKey("aics_table_permission.id")),
Column('permissionview_id', Integer, ForeignKey("ab_permission_view.id")),
UniqueConstraint("tableperm_id", "permissionview_id"),
)

class TablePermission(Model, AuditMixinNullable):
"""
Customized table permission control table
Superset 0.35 leverage Flask AppBuilder Role/Permission for permission control
However, it makes permission auto-revocation impossible.
So we build this permission control table and with auto-revocation mechanism
"""

__tablename__ = "aics_table_permission"
id = Column(Integer, Sequence("aics_table_permission_id_seq"), primary_key=True) # pylint: disable=invalid-name
user_id = Column(Integer, ForeignKey("ab_user.id"))
user = relationship(
security_manager.user_model, backref="table_permissions", foreign_keys=[user_id]
)

apply_date = Column(Date, nullable=False, default=datetime.now().date())
expire_date = Column(Date, nullable=False, default=datetime.now().date()+timedelta(days=6*365/12))
force_terminate_date = Column(DateTime)
is_active = Column(Boolean, default=True)

table_permissions = relationship(
security_manager.permissionview_model, secondary=assoc_tableperm_permissionview, backref="table_perm"
)

# Following properties are used for display fields that are not expected to be changed
@property
def username_detail(self):
return f'{self.user.get_full_name()} ({self.user.username})'

@username_detail.setter
def username_detail(self, value):
pass

@property
def exp_or_terminate_date(self):
if self.force_terminate_date != None:
return f'{str(self.force_terminate_date.date())}(T)'
return self.expire_date

@exp_or_terminate_date.setter
def exp_or_terminate_date(self, value):
pass

@property
def table_permission_list(self):
table_perm = [str(perm.view_menu) for perm in self.table_permissions]
return ', '.join(table_perm)

@table_permission_list.setter
def table_permission_list(self, value):
pass

@property
def status(self):
if self.is_active:
return 'Active'
elif self.force_terminate_date != None:
return 'Force Revoked'
else:
return 'Expired'

@status.setter
def status(self, value):
pass
12 changes: 4 additions & 8 deletions superset/models/user_attributes.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,12 @@ class UserAttribute(Model, AuditMixinNullable):
# This is related to SQLA. Use @property from python or @hybrid_property from SQLA are both OK here.
# Ref: https://docs.sqlalchemy.org/en/13/orm/extensions/hybrid.html#defining-expression-behavior-distinct-from-attribute-behavior
@property
def username(self):
return self.user.username
def username_detail(self):
return f'{self.user.get_full_name()} ({self.user.username})'

# As we need to show username in add/edit views, we need to define a setter to deal with the assignment of the property
@username.setter
def username(self, value):
@username_detail.setter
def username_detail(self, value):
pass

@property
Expand All @@ -67,7 +67,3 @@ def new_access_key(self):
@new_access_key.setter
def new_access_key(self, value):
self._new_key = value

@property
def changed_by_name(self):
return self.changed_by_
32 changes: 32 additions & 0 deletions superset/security.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
# pylint: disable=C,R,W
"""A set of constants and methods to manage permissions and security"""
import logging
from datetime import datetime
from typing import Callable, List, Optional, Set, Tuple, TYPE_CHECKING, Union

from flask import current_app, g
Expand Down Expand Up @@ -288,6 +289,32 @@ def get_table_access_link(self, tables: List[str]) -> Optional[str]:

return conf.get("PERMISSION_INSTRUCTIONS_LINK")

def _has_aics_table_permission(self, user: object, permission_name: str, view_name: str):
"""
Customize table permission check
"""

from superset import db
from superset.models.table_permission import TablePermission

# Anonymous user is not allowed
if user.is_anonymous:
return False

# get permission of specified permission name and view name
perm = self.find_permission_view_menu( permission_name, view_name)

user_table_permissions = db.session.query(TablePermission).filter(
TablePermission.user_id == user.id,
TablePermission.is_active == True,
TablePermission.expire_date > datetime.now().date()
)

for table_perm in user_table_permissions:
if perm in table_perm.table_permissions:
return True


def _datasource_access_by_name(
self, database: "Database", table_name: str, schema: str = None
) -> bool:
Expand Down Expand Up @@ -316,9 +343,14 @@ def _datasource_access_by_name(
)

for datasource in datasources:
# Check permission using AppBuilder Roles
if self.can_access("datasource_access", datasource.perm):
return True

# Check AICS table permissions
if self._has_aics_table_permission(g.user, "datasource_access", datasource.perm):
return True

return False

def _get_schema_and_table(
Expand Down
18 changes: 13 additions & 5 deletions superset/utils/log.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@ def wrapper(*args, **kwargs):
database=extra_info.get("database"),
schema=extra_info.get("schema"),
sql=extra_info.get("sql"),
err_msg=extra_info.get("err_msg")
err_msg=extra_info.get("err_msg"),
log_msg=extra_info.get("log_msg")
)
return value

Expand Down Expand Up @@ -159,6 +160,7 @@ def log(self, user_id, action, *args, **kwargs):
database = kwargs.get("database")
schema = kwargs.get("schema")
sql = kwargs.get("sql")
log_msg = kwargs.get("log_msg")
err_msg = kwargs.get("err_msg")
success = "true" if err_msg == None else "false"

Expand All @@ -178,12 +180,18 @@ def log(self, user_id, action, *args, **kwargs):
user_id=user_id,
)
logs.append(log)
self.appinsights(
{'level': 'info', 'success': success, 'state':'finish',
json_log = {'level': 'info', 'success': success, 'state':'finish',
'function': action, 'json': json_string, 'duration': duration_ms,
'referrer': referrer, 'user_id': user_id,
'database': database, 'schema': schema, 'sql': sql, 'err_msg': err_msg
})
'database': database, 'schema': schema, 'sql': sql
}
if err_msg != None:
json_log['err_msg'] = err_msg

if log_msg != None:
json_log['log_msg'] = log_msg

self.appinsights(json_log)

sesh = current_app.appbuilder.get_session
sesh.bulk_save_objects(logs)
Expand Down
Loading

0 comments on commit 8d2aaa9

Please sign in to comment.