Skip to content

feat(sql): expose user identity as Snowflake session variable for row-level security#108

Closed
robertlacok wants to merge 1 commit into
mainfrom
rl/snowflake-rls-session-variable
Closed

feat(sql): expose user identity as Snowflake session variable for row-level security#108
robertlacok wants to merge 1 commit into
mainfrom
rl/snowflake-rls-session-variable

Conversation

@robertlacok

@robertlacok robertlacok commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

What

Sets a Snowflake session variable carrying the executing user's identity before each SQL query runs, so Snowflake-side row-level security (row access policies or secure views) can filter rows per user.

Deepnote already appends an audit comment containing the user's email to every SQL block. This change:

  1. Parses user_email out of that comment (_extract_rls_user_email).
  2. For Snowflake connections only, runs SET DEEPNOTE_USER_EMAIL = '<email>' on the same connection inside the existing engine.begin() block, immediately before the query.

A policy or secure view can then read it via GETVARIABLE('DEEPNOTE_USER_EMAIL'):

CREATE OR REPLACE SECURE VIEW compensation_rls AS
  SELECT * FROM compensation
  WHERE manager_email = GETVARIABLE('DEEPNOTE_USER_EMAIL');

Why these choices

  • Set on every Snowflake query, always. Session variables persist on the physical connection when it returns to the SQLAlchemy pool, so skipping the SET could leak a previous user's identity to the next query on that connection.
  • Empty value = deny by default. Anonymous viewers (no email) get an empty variable, which matches no rows.
  • Injection-safe. The extracted value is restricted to a safe character set before being embedded in the SET statement.

Testing

  • Unit-tested _extract_rls_user_email (normal, missing-email, and adversarial inputs — unsafe characters are stripped).
  • Verified end-to-end against a live Snowflake account via a secure view: two different identities each see only their own rows, an empty identity sees none, and no cross-user leak occurs when reusing a pooled connection.

Notes

  • Native row access policies require Snowflake Enterprise Edition; the mechanism is identical for a secure view on other editions.
  • Draft — opening early for visibility/feedback.

Summary by CodeRabbit

  • New Features
    • Added support for Snowflake row-level security with automatic user email identification and session configuration during query execution.

…-level security

Parse the user email already carried in the audit comment appended to each SQL
query and set it as a Snowflake session variable (DEEPNOTE_USER_EMAIL) on the
same connection before the query runs. Row access policies or secure views can
then filter rows via GETVARIABLE('DEEPNOTE_USER_EMAIL').

The variable is set on every Snowflake query (session variables persist on
pooled connections, so a stale value could otherwise leak another user's rows);
an empty value denies all rows by default for anonymous viewers. The value is
restricted to a safe character set before being embedded in the SET statement.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Snowflake row-level security support is added to _execute_sql_on_engine in sql_execution.py. A new helper _extract_rls_user_email parses the Deepnote audit comment embedded in the SQL text using regex to pull out user_email, sanitizes it to a restricted character set, and returns an empty string if absent. When the SQLAlchemy engine dialect is snowflake, the execution path calls this helper and issues SET DEEPNOTE_USER_EMAIL = '<email>' via SQLAlchemy text(...) on the connection before running the original query. The text symbol is added to the SQLAlchemy import for this purpose.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Updates Docs ⚠️ Warning Snowflake RLS feature implemented in sql_execution.py but no documentation added to docs/ or README explaining how to use it. Add user documentation explaining Snowflake RLS setup and usage, and update the roadmap in deepnote-internal.
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: exposing user identity as a Snowflake session variable for row-level security, which matches the PR's core objective.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

Copy link
Copy Markdown

📦 Python package built successfully!

  • Version: 2.3.1.dev3+7127ce0
  • Wheel: deepnote_toolkit-2.3.1.dev3+7127ce0-py3-none-any.whl
  • Install:
    pip install "deepnote-toolkit @ https://deepnote-staging-runtime-artifactory.s3.amazonaws.com/deepnote-toolkit-packages/2.3.1.dev3%2B7127ce0/deepnote_toolkit-2.3.1.dev3%2B7127ce0-py3-none-any.whl"

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@deepnote_toolkit/sql/sql_execution.py`:
- Around line 629-643: The function _extract_rls_user_email lacks explicit type
annotations for its parameter and return value. Add a type hint for the query
parameter to indicate it accepts an optional string (using Optional[str] or str
| None based on the `query or ""` usage pattern), and add a return type hint of
str since the function always returns a string.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 59e36f91-a04e-4a2d-9ad9-b8e854c7431e

📥 Commits

Reviewing files that changed from the base of the PR and between 3967dd5 and 0005477.

📒 Files selected for processing (1)
  • deepnote_toolkit/sql/sql_execution.py

Comment on lines +629 to +643
def _extract_rls_user_email(query):
"""Extract the Deepnote user email from the audit comment embedded in *query*.

Deepnote appends an audit comment such as ``/* {"user_email":"a@b.com",...} */`` to
every SQL block. We reuse that identity for row-level security. Returns "" when no
email is present (e.g. anonymous app viewers) so that the policy denies all rows.

The value is restricted to a safe character set so it can be embedded directly into a
``SET`` statement without risk of SQL injection, regardless of the caller.
"""
match = re.search(r'"user_email"\s*:\s*"([^"]+)"', query or "")
if not match:
return ""
return re.sub(r"[^A-Za-z0-9.+@_-]", "", match.group(1))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add type hints.

Function lacks explicit type annotations for parameter and return value.

-def _extract_rls_user_email(query):
+def _extract_rls_user_email(query: str) -> str:
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _extract_rls_user_email(query):
"""Extract the Deepnote user email from the audit comment embedded in *query*.
Deepnote appends an audit comment such as ``/* {"user_email":"a@b.com",...} */`` to
every SQL block. We reuse that identity for row-level security. Returns "" when no
email is present (e.g. anonymous app viewers) so that the policy denies all rows.
The value is restricted to a safe character set so it can be embedded directly into a
``SET`` statement without risk of SQL injection, regardless of the caller.
"""
match = re.search(r'"user_email"\s*:\s*"([^"]+)"', query or "")
if not match:
return ""
return re.sub(r"[^A-Za-z0-9.+@_-]", "", match.group(1))
def _extract_rls_user_email(query: str) -> str:
"""Extract the Deepnote user email from the audit comment embedded in *query*.
Deepnote appends an audit comment such as ``/* {"user_email":"a@b.com",...} */`` to
every SQL block. We reuse that identity for row-level security. Returns "" when no
email is present (e.g. anonymous app viewers) so that the policy denies all rows.
The value is restricted to a safe character set so it can be embedded directly into a
``SET`` statement without risk of SQL injection, regardless of the caller.
"""
match = re.search(r'"user_email"\s*:\s*"([^"]+)"', query or "")
if not match:
return ""
return re.sub(r"[^A-Za-z0-9.+@_-]", "", match.group(1))
🧰 Tools
🪛 Ruff (0.15.17)

[warning] 629-629: Missing return type annotation for private function _extract_rls_user_email

(ANN202)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@deepnote_toolkit/sql/sql_execution.py` around lines 629 - 643, The function
_extract_rls_user_email lacks explicit type annotations for its parameter and
return value. Add a type hint for the query parameter to indicate it accepts an
optional string (using Optional[str] or str | None based on the `query or ""`
usage pattern), and add a return type hint of str since the function always
returns a string.

Sources: Coding guidelines, Linters/SAST tools

@deepnote-bot

Copy link
Copy Markdown

🚀 Review App Deployment Started

📝 Description 🌐 Link / Info
🌍 Review application ra-108
🔑 Sign-in URL Click to sign-in
📊 Application logs View logs
🔄 Actions Click to redeploy
🚀 ArgoCD deployment View deployment
Last deployed 2026-06-16 09:58:12 (UTC)
📜 Deployed commit 507bdcc263e0b2ac858f98b15f52ff0eccd642df
🛠️ Toolkit version 7127ce0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants