1. Courses
  2. Data
  3. Dashboards and Business Intelligence
  4. Stop 6

35 minIntermediate

Self-service without chaos

Self-service means people answer their own questions without waiting for the data team. Done carelessly, it gives you hundreds of dashboards, five versions of "sales" and data in front of people who should not see it. This lesson shows the controls that let many people explore safely: row-level access, certified and sandbox content on governed datasets, plain-language questions with checks, and a routine for retiring what nobody uses.

By the end you can

  • Design who-sees-what rules by role and enforce them in the database or semantic layer, with Postgres row-level security as the example
  • Set up certified, team and sandbox content tiers on governed datasets, with a path to promote good work
  • Judge an AI-generated answer: check the metric, the joins, the dates and the filters it used
  • Monitor real usage and run a retire loop: no human views in 90 days, owner asked, archived, then deleted

The everyday picture

Think of a good public library. Anyone can walk in, take a book from the shelf and read it. Nobody has to ask a librarian first. That is self-service, and it is the whole point of the place.

Yet the library is not a free-for-all. The reference shelf (dictionaries, maps, the official statistics) is chosen and checked by librarians, and people trust it because of that. The community notice board lets anyone pin up a page, and everyone knows those pages are unchecked. Some rooms, like the archive of private letters, need a card with the right permission. And every year the librarians go through the shelves and move books nobody has borrowed in years to storage, so the useful books are easy to find. Self-service BI needs the same four things: open shelves, a trusted shelf, locked rooms and regular weeding.

Who can see what: row-level security

Once hundreds of people can build and open reports, the first question is what each of them is allowed to see. Most rules have two parts. Rows: a store manager sees her own store, a city manager sees the stores in her city, head office sees everything. Columns: almost nobody needs customer phone numbers or emails in a sales report, so those columns stay out of the shared datasets altogether.

(RLS) is the mechanism for the first part. It has three pieces:

  • A mapping from each user (or group) to the rows they may see, for example a table user_site with one row per user and store. It should be fed from the HR or staff system every night, so a manager who moves to another store loses the old one the next day.
  • A policy: a filter the system adds to every query on the protected table, such as "only rows whose site_id is in this user's mapping".
  • The user's identity reaching the place where the policy runs. This is the part that most often goes wrong.

The key property is that the filter is added by the server, after the user has written or chosen the query. The user cannot remove it. A filter set in the dashboard, such as a "Store" drop-down that defaults to the manager's store, is a convenience. Anyone who can edit the chart, export the data or open the dataset in another report can remove it.

Ama store manager City manager East city Same question net sales by store runs as the user Row policy keeps rows in scope user_site mapping looks up Ama: 1 store her own rows City: 54 stores East rows only
Row-level security. Both users ask the same question. The policy, which runs on the server, looks each user up in the mapping table and keeps only their rows, so the same chart shows Ama one store and the East city manager 54.

In PostgreSQL the policy is part of the table. You switch row security on, then write a policy whose USING condition every row must pass:

SQL
ALTER TABLE sale ENABLE ROW LEVEL SECURITY;

CREATE POLICY sale_by_store ON sale
  FOR SELECT
  TO bi_reader                      -- the role the BI tool connects as
  USING (site_id IN (
    SELECT site_id FROM user_site
    WHERE user_email = current_setting('app.user_email', true)
  ));

-- Before each user's queries, the BI tool (not the user) runs:
SET app.user_email = 'ama@mangrove.example';

Three details matter. First, BI tools usually connect with one shared account, so current_user is the same for everyone. The tool has to pass the real viewer, here in a custom setting that current_setting reads. This is only safe when users cannot run their own SET on that connection. Second, if the setting is missing, current_setting(…, true) returns null, no row matches and the user sees nothing. That is the right way to fail. Third, the table owner, superusers and roles with the BYPASSRLS attribute skip policies, so the BI account must be an ordinary role that does not own the table (or the table needs FORCE ROW LEVEL SECURITY).

You can also enforce the rule in the BI tool or semantic layer instead of the database. Power BI defines row-level security as roles with DAX filter expressions; Looker uses access filters tied to user attributes; most other tools have a similar feature. Pick one place and enforce it there for every path to the data, including exports, scheduled emails, extracts and summary tables. A policy on sale does not protect a weekly_store_sales table built from it.

Quick check

The store managers' dashboard opens with the Store filter set to the viewer's own store, read from their profile. Is that enough to keep Ama from seeing other stores' sales?

Loading the rest of the lesson…