diff --git a/docs/resources/grant.md b/docs/resources/grant.md index 81dad6d..25b3c71 100644 --- a/docs/resources/grant.md +++ b/docs/resources/grant.md @@ -45,6 +45,11 @@ grants: on: warehouse some_warehouse to: some_role + # Apps: USAGE on a Streamlit app so a role can open and run it + - priv: USAGE + on: streamlit somedb.someschema.some_streamlit + to: app_viewer_role + # AI: account-level privilege for Cortex AI SQL functions - priv: USE AI FUNCTIONS on: ACCOUNT diff --git a/docs/resources/streamlit.md b/docs/resources/streamlit.md index 799f09a..78c133d 100644 --- a/docs/resources/streamlit.md +++ b/docs/resources/streamlit.md @@ -69,4 +69,42 @@ streamlit_repo = Streamlit( * `query_warehouse` (string) - The name of the warehouse to use for queries in the app. * `comment` (string) - A comment or description for the Streamlit app. * `owner` (string or Role) - The role that owns the Streamlit app. Defaults to "SYSADMIN". -* `tags` (dict) - A dictionary of tags to associate with the Streamlit app. \ No newline at end of file +* `tags` (dict) - A dictionary of tags to associate with the Streamlit app. + +## Granting access + +A Streamlit app is a schema-scoped object. Grant `USAGE` on the app to let a +role open and run it — this is the whole access story for viewers when the app +uses owner's rights (all app queries execute as the app owner, so viewers need +no privileges on the underlying tables): + +```yaml +grants: + # Let a viewer role open and run the app. + - priv: USAGE + on: streamlit my_db.my_schema.my_streamlit + to: app_viewer_role + + # Schema-scope privilege to allow a role to create Streamlit apps. + - priv: CREATE STREAMLIT + on: schema my_db.my_schema + to: app_developer_role +``` + +```python +# Grant USAGE so a role can open the app +grant = Grant( + priv="USAGE", + on_streamlit="my_db.my_schema.my_streamlit", + to="app_viewer_role", +) +``` + +| Privilege | Purpose | +|-------------------|--------------------------------------------------------------------------| +| `USAGE` | Open, view, and run the Streamlit app (and `DESCRIBE` it). | +| `OWNERSHIP` | Full control. Set at create/deploy time — Snowflake does not support transferring streamlit ownership via `GRANT OWNERSHIP`. | +| `ALL` | All privileges above. | + +The schema-scope privilege `CREATE STREAMLIT` lets a role create apps in that +schema; creating an app with a `ROOT_LOCATION` stage also needs `CREATE STAGE`. \ No newline at end of file diff --git a/snowcap/data_provider.py b/snowcap/data_provider.py index f556b0e..b7a5671 100644 --- a/snowcap/data_provider.py +++ b/snowcap/data_provider.py @@ -2773,6 +2773,30 @@ def fetch_stream(session: SnowflakeConnection, fqn: FQN): raise NotImplementedError(f"Unsupported stream source type {data['source_type']}") +def fetch_streamlit(session: SnowflakeConnection, fqn: FQN): + streamlits = _show_resources(session, "STREAMLITS", fqn) + if len(streamlits) == 0: + return None + if len(streamlits) > 1: + raise Exception(f"Found multiple streamlits matching {fqn}") + + data = streamlits[0] + desc_result = execute(session, f"DESC STREAMLIT {fqn}", cacheable=True) + properties = desc_result[0] + return { + "name": data["name"], + # root_location is the stage the app was deployed from, e.g. + # '@db.schema.stage/app' — this is the `from_` field. + "from_": properties.get("root_location"), + "version": None, + "main_file": properties.get("main_file"), + "title": properties.get("title") or None, + "query_warehouse": data.get("query_warehouse") or None, + "comment": data.get("comment") or None, + "owner": _get_owner_identifier(data), + } + + def fetch_tag(session: SnowflakeConnection, fqn: FQN): try: show_result = execute(session, "SHOW TAGS IN ACCOUNT", cacheable=True) diff --git a/tests/test_grant.py b/tests/test_grant.py index a32961b..e30a4cf 100644 --- a/tests/test_grant.py +++ b/tests/test_grant.py @@ -174,6 +174,24 @@ def test_grant_on_cortex_search_service(): assert "MONITOR ON CORTEX SEARCH SERVICE" in monitor_grant.create_sql() +def test_grant_on_streamlit(): + """USAGE on a STREAMLIT parses and renders correctly. + + Streamlit is a schema-scoped app object. Granting USAGE lets a viewer + role open and run the app: + GRANT USAGE ON STREAMLIT .. TO ROLE r + OWNERSHIP is established at create/deploy time, not transferred via GRANT. + """ + grant = res.Grant( + priv="USAGE", + on_streamlit="somedb.someschema.someapp", + to="somerole", + ) + assert grant._data.on == "SOMEDB.SOMESCHEMA.SOMEAPP" + assert grant._data.on_type == ResourceType.STREAMLIT + assert "USAGE ON STREAMLIT" in grant.create_sql() + + def test_grant_database_role_to_database_role(): database = res.Database(name="somedb") parent = res.DatabaseRole(name="parent", database=database) diff --git a/tests/test_privs.py b/tests/test_privs.py index 6450721..84ff639 100644 --- a/tests/test_privs.py +++ b/tests/test_privs.py @@ -347,6 +347,25 @@ def test_ownership_privilege(self): assert CortexSearchServicePriv.OWNERSHIP.value == "OWNERSHIP" +############################################################################# +# StreamlitPriv Tests +############################################################################# + + +class TestStreamlitPriv: + """Tests for StreamlitPriv enum values (Snowflake Streamlit apps).""" + + def test_all_privilege(self): + assert StreamlitPriv.ALL.value == "ALL" + + def test_usage_privilege(self): + """USAGE is the priv needed to open and run a Streamlit app.""" + assert StreamlitPriv.USAGE.value == "USAGE" + + def test_ownership_privilege(self): + assert StreamlitPriv.OWNERSHIP.value == "OWNERSHIP" + + ############################################################################# # is_ownership_priv() Tests #############################################################################