Allowing users to only view data related to them in Apache Superset - apache-superset

I have some information related to different vendors in my database and I want to allow each registered vendor (representative person) to view slices/dashboards which contains only data related to them.
One possible solution could be to create separate views for each vendor as well as separate roles for each vendor. But it feels like a bad idea if you have 100+ vendors (as is my case); and it's not a flexible or scalable solution.
Is there some way to automatically filter a given view for each user? For example, we have a "general profit by product" bar chart, and user X can see only products of vendor X

What you're looking for is multi-tenancy support, and this is not currently supported out-of-the-box in Superset.
There is however an open PR for one possible solution: https://github.com/apache/incubator-superset/pull/3729
One option could be to re-use and/or adapt that code for your use-case.
Another option might be to look into JINJA_CONTEXT_ADDONS [https://github.com/apache/incubator-superset/blob/master/docs/installation.rst#sql-lab] and see whether you might be able to pass additional context to your query (e.g. your vendor_id) and restrict the scope of your query using that parameter.

Superset config has the below two configurations(DB_CONNECTION_MUTATOR, SQL_QUERY_MUTATOR), which can allow for multi-tenancy to an extent.
A callable that allows altering the database conneciton URL and params
on the fly, at runtime. This allows for things like impersonation or
arbitrary logic. For instance you can wire different users to
use different connection parameters, or pass their email address as the
username. The function receives the connection uri object, connection
params, the username, and returns the mutated uri and params objects.
Example:
def DB_CONNECTION_MUTATOR(uri, params, username, security_manager, source):
user = security_manager.find_user(username=username)
if user and user.email:
uri.username = user.email
return uri, params
Note that the returned uri and params are passed directly to sqlalchemy's
as such create_engine(url, **params)
DB_CONNECTION_MUTATOR = None
A function that intercepts the SQL to be executed and can alter it.
The use case is can be around adding some sort of comment header
with information such as the username and worker node information
def SQL_QUERY_MUTATOR(sql, username, security_manager):
dttm = datetime.now().isoformat()
return f"-- [SQL LAB] {username} {dttm}\n{sql}"
SQL_QUERY_MUTATOR = None

One easy way of solving this problem is by using pre-defined JINJA parameters.
Two parameters that can be used are '{{current_username() }}' and {{current_user_id() }}
First you need to ensure that you can use JINJA templates -
In superset_config.py add the following
FEATURE_FLAGS = {
"ENABLE_TEMPLATE_PROCESSING": True,
}
Restart
Now if you go to the SQL LAB and type the following -
SELECT '{{ current_username() }}',{{ current_user_id() }};
You should get an output
?column?
?column?__1
PayalC
5
Now all you have to do is append one of the two following sql snippet in all your queries.
select ........ from ...... where ...... vendorid={{ current_user_id() }}
select ........ from ...... where ...... vendorname='{{ current_username() }}'
vendorid={{ current_user_id() }} and/or
vendorname='{{ current_username() }}' will restrict the user to view only her data.
You could also make it more flexible by creating a table which has a mapping of user to vendorid. That table can be your added to all the queries and you could map multiple vendors to a single user or even all vendors to a single user for a super admin.

Related

How to make filtering non model data in flask-admin

I have to make dashboard like view in flask-admin that will use data retrieved from external API. I have already written a functions that get date ranges and return data from that range. I should use BaseView probably but I don't know how to actually write it to make filters work. This is example function that i have to use: charts = generate_data_for_dashboard('164', '6423FACA-FC71-489D-BF32-3A671AB747E3', '2018-03-01', '2018-09-01'). Those params should be chosen from 3 different dropdowns. So far I know only how to render views with pre coded data like this :
class DashboardView(BaseView):
kwargs = {}
#expose('/', methods=('GET',))
def statistics_charts(self):
user = current_user
company = g.company
offices = Office.query.filter_by(company_id=company.id)
self.kwargs['user'] = user
self.kwargs['company'] = company
charts = generate_data_for_dashboard('164', '6423FACA-FC71-489D-BF32-3A671AB747E3', '2018-03-01', '2018-09-01')
self.kwargs['chart1'] = charts[0]
self.kwargs['chart2'] = charts[1]
return self.render('stats/dashboard.html', **self.kwargs)
But I need some kind of form to filter it. In addition date filter dropdown should have dynamic options : current_week, last_week, current_month, last_month, last_year. Don't know where to start.
You should use WTForms to build a form. You then have to decide if you want the data to be fetched on Submit or without a reload of the page. In the former case, you can just return the fetched information on the response page in your statistics_charts view. But if you want the data to update without a reload, you'll need to use JavaScript to track the form field changes, send the AJAX request to the API, and then interpret the resulting JSON and update your dashboard graphs and tables as needed.
I have not used it, but this tutorial says you can use Dash for substantial parts of this task, while mostly writing in Python. So that could be something to check out. There is also flask_jsondash which might work for you.

How to reuse template in Flask-appbuilder with exposed custom handlers?

It is a very specific question regarding Flask-appbuilder. During my development, I found FAB's ModelView is suitable for admin role, but need more user logic handlers/views for complex designs.
There is a many to many relationship between devices and users, since each device could be shared between many users, and each user could own many device. So there is a secondary table called accesses, describes the access control between devices and users. In this table, I add "isHost" to just if the user owns the device. Therefore, we have two roles: host and (regular) user. However, these roles are not two roles defined as other applications, since one man can be either host or user in same time. In a very simple application, enforce the user to switch two roles are not very convinient. That makes things worse.
Anyway, I need design some custom handlers with traditional Flask/Jinja2 templates. For example:
class PageView(ModelView):
# FAB default URL: "/pageview/list"
datamodel = SQLAInterface(Page)
list_columns = ['name', 'date', 'get_url']
#expose("/p/<string:url>")
def p(self, url):
title = urllib.unquote(url)
r = db.session.query(Page).filter_by(name = title).first()
if r:
md = r.markdown
parser = mistune.Markdown()
body = parser(md)
return self.render_template('page.html', title = title, body = body)
else:
return self.render_template('404.html'), 404
Above markdown page URL is simple, since it is a seperate UI. But if I goes to DeviceView/AccountView/AccessView for list/show/add/edit operations. I realized that I need a unique styles of UI.
So, now how can I reuse the existing templates/widgets of FAB with custom sqlalchemy queries? Here is my code for DeviceView.
class DeviceView(ModelView):
datamodel = SQLAInterface(Device)
related_views = [EventView, AccessView]
show_template = 'appbuilder/general/model/show_cascade.html'
edit_template = 'appbuilder/general/model/edit_cascade.html'
#expose('/host')
#has_access
def host(self):
base_filters = [['name', FilterStartsWith, 'S'],]
#if there is not return, FAB will throw error
return "host view:{}".format(repr(base_filters))
#expose('/my')
#has_access
def my(self):
# A pure testing method
rec = db.session.query(Access).filter_by(id = 1).all()
if rec:
for r in rec:
print "rec, acc:{}, dev:{}, host:{}".format(r.account_id, r.device_id, r.is_host)
return self.render_template('list.html', title = "My Accesses", body = "{}".format(repr(r)))
else:
return repr(None)
Besides sqlalchemy code with render_template(), I guess base_filters can also help to define custom queries, however, I have no idea how to get query result and get them rendered.
Please give me some reference code or example if possible. Actually I have grep keywords of "db.session/render_template/expoaw"in FAB's github sources. But no luck.

Using ActiveRecord to search through an object's associations and return instances where all of the associations do/do not contain a certain value

I'm attempting to query an object's associations, and only return objects where all of their associations are of a certain value. For example, if a User has a memberships association, and memberships has an active(boolean) field, I want to return a collection of all users that only have memberships with active: false.
The query I'm working with right now is:
User.includes(:memberships).where(memberships: {active: false})
However, this gives me all users that have inactive memberships, but also all users that have both active: false and active: true memberships. I tried doing an extra .where.not on this, but of course, that returned the same group.
I've considered mapping over the collection of users and creating a new array, kicking out the users with active memberships, but I need to keep the final value as an AR Collection as I continue to query it further in the controller.
I would prefer to stick to using ActiveRecord for this, but if it's not doable and I need to use SQL instead, I'm also open to that.
Any help would be greatly appreciated. Thank you!
Couple things here - you want a LEFT OUTTER joins, so make sure your SQL query is being constructed properly. Check with
User.includes(:memberships).where(memberships: {active: false}).explain
or
User.includes(:memberships).where(memberships: {active: false}).to_sql
Second, try:
User.joins(:memberships).where("memberships.active = ?", false)
or perhaps give .merge a try, which I like to use:
User.joins(:memberships).merge(Membership.inactive)
assuming you have a scope/class method .inactive on the Membership model
def self.inactive
where(active: false)
end
.includes should really only be used if you need the relationship loaded into memory.
EDIT:
Easiest way to exclude any user that has a single (of many) memberships would be to split this up into 2 queries.
active_user_ids = Membership.active.pluck(:user_id).uniq
User.where("id NOT IN (?)", active_user_ids.join(","))
To exclude Users with both a valid and invalid membership try--
User.includes(:memberships).where(memberships: {active: false}).where.not(memberships: {active: true})
Also--you probably want to use #joins instead of #includes. If you don't need access to the Memberships except for the query, there is no need to load that into memory (which #include does).
This article has a nice explanation--
http://tomdallimore.com/blog/includes-vs-joins-in-rails-when-and-where/

How to find user group and use of caching in django?

I am new to django/python and working my way through my webapp. I need assistance in solving one of my problems.
In my app, I am planning to assign each user (from auth_user) to one of the group ( from auth_group). Each group can have multiple users. I have entry in auth_group, auth_user and auth_user_groups. Here is my question:
At time of login I want to check that logging user belongs to which group?
I want to keep that group info in session/cache so all pages I can show information about that group only.
If you have any sample code will be great.
Giving support to the very well #trinchet's answer with an example of context_processor code.
Puts inside your webapp a new file called context_processors.py and writes this lines on it:
def user_groups(request):
"""
Add `groups` var to the context with all the
groups the logged in user has, so you can access
in your templates to this var as: {{ groups }}
"""
groups = None
if request.user.is_authenticated():
groups = user.groups
return {'groups': groups}
Finally on your settings.py add 'webbapp.context_processors.user_groups'to TEMPLATE_CONTEXT_PROCESSOR:
TEMPLATE_CONTEXT_PROCESSORS = (
'webbapp.context_processors.user_groups',
)
1) Be user an instance of auth.models.User, you can get all groups the user belong to, through user.groups. If you want to ask at time of login then you should do this in your login view.
2) You can use session or cache approaches to deal with, this is irrelevant, but once you have the group you need to render the pages having this value, i mean, you need to provide the group to the template rendering, to do this I suggest to you using a custom context processor.

django admin actions on all the filtered objects

Admin actions can act on the selected objects in the list page.
Is it possible to act on all the filtered objects?
For example if the admin search for Product names that start with "T-shirt" which results with 400 products and want to increase the price of all of them by 10%.
If the admin can only modify a single page of result at a time it will take a lot of effort.
Thanks
The custom actions are supposed to be used on a group of selected objects, so I don't think there is a standard way of doing what you want.
But I think I have a hack that might work for you... (meaning: use at your own risk and it is untested)
In your action function the request.GET will contain the q parameter used in the admin search. So if you type "T-Shirt" in the search, you should see request.GET look something like:
<QueryDict: {u'q': [u'T-Shirt']}>
You could completely disregard the querystring parameter that your custom action function receives and build your own queryset based on that request.GET's q parameter. Something like:
def increase_price_10_percent(modeladmin, request, queryset):
if request.GET['q'] is None:
# Add some error handling
queryset=Product.objects.filter(name__contains=request.GET['q'])
# Your code to increase price in 10%
increase_price_10_percent.short_description = "Increases price 10% for all products in the search result"
I would make sure to forbid any requests where q is empty. And where you read name__contains you should be mimicking whatever filter you created for the admin of your product object (so, if the search is only looking at the name field, name__contains might suffice; if it looks at the name and description, you would have a more complex filter here in the action function too).
I would also, maybe, add an intermediate page stating what models will be affected and have the user click on "I really know what I'm doing" confirmation button. Look at the code for django.contrib.admin.actions for an example of how to list what objects are being deleted. It should point you in the right direction.
NOTE: the users would still have to select something in the admin page, otherwise the action function would never get called.
This is a more generic solution, is not fully tested(and its pretty naive), so it might break with strange filters. For me works with date filters, foreign key filters, boolean filters.
def publish(modeladmin,request,queryset):
kwargs = {}
for filter,arg in request.GET.items():
kwargs.update({filter:arg})
queryset = queryset.filter(**kwargs)
queryset.update(published=True)