check database connect privilege when accessing portal
This commit is contained in:
parent
fd78a386e2
commit
75fcfecda1
3 changed files with 170 additions and 135 deletions
146
README.md
146
README.md
|
|
@ -16,7 +16,22 @@ configuration to automatically manage the development environment:
|
|||
|
||||
## Configuration
|
||||
|
||||
Refer to [the .env.example file](./.env.example) for configuration options.
|
||||
Refer to [the .env.example file](./.env.example) for configuration options. An
|
||||
external OAuth2 provider is required to manage authentication.
|
||||
|
||||
# The Phonograph Authorization Model
|
||||
|
||||
Refer to documentation in [docs/auth.md](./docs/auth.md).
|
||||
|
||||
# Copyright and License
|
||||
|
||||
All original source code in this repository is copyright (C) 2025 Second System
|
||||
Technologies LLC and distributed under the terms in
|
||||
[the "LICENSE" file](./LICENSE). Certain third-party assets within
|
||||
[the "static" directory](./static) may be governed by different licenses, for
|
||||
example the Open Font License or MIT License, as stated by their original
|
||||
authors. Copies of each relevant license have been included alongside these
|
||||
files as needed.
|
||||
|
||||
# LLM Code Policy
|
||||
|
||||
|
|
@ -44,136 +59,9 @@ Examples of LLM-assisted changes in practice:
|
|||
- Replacing SVG icons with similar webfont icons from a different icon pack.
|
||||
(Revision `ztrnxzqv` (Git `a8dd49f7`))
|
||||
|
||||
# The Phonograph Authorization Model
|
||||
|
||||
Postgres provides a sophisticated role based access control (RBAC) system, which
|
||||
Phonograph leverages to apply permissions consistently across the web UI and
|
||||
inbound PostgreSQL[^1] connections.
|
||||
|
||||
In order to efficiently pool database connections from the application server,
|
||||
most actions initiated via the web UI are run as the corresponding database user
|
||||
role using the
|
||||
[`SET ROLE` command](https://www.postgresql.org/docs/current/sql-set-role.html).
|
||||
`SET ROLE` **does not** provide great insulation against privilege escalation.
|
||||
**Queries which are not thoroughly validated and escaped must only be run via a
|
||||
dedicated connection initiated with the user-level role's credentials.**
|
||||
|
||||
Given complete freedom it is possible, in fact easy, to configure a Postgres
|
||||
table into what would be considered an "invalid" state by Phonograph, so table
|
||||
creation and ownership is restricted to the "root" Phonograph role, which acts
|
||||
on the behalf of the user in order to facilitate schema updates via the web
|
||||
interface.
|
||||
|
||||
## Permissions Granted via User Roles
|
||||
|
||||
### Accessing workspace databases
|
||||
|
||||
`GRANT CONNECT ON <database> TO <role>;`
|
||||
|
||||
This permission is granted when initially creating the workspace, as well as
|
||||
when accepting an invitation to a table.
|
||||
|
||||
Access to workspaces is controlled via the `CONNECT ON DATABASE` permission.
|
||||
However, it is unreasonable to query every backing database to compute the set
|
||||
of workspaces to which a user has access, so Phonograph caches workspace-level
|
||||
"connect" permissions in its own centralized table (`workspace_memberships`).
|
||||
|
||||
`workspace_memberships` rows are added whenever the `GRANT CONNECT` command is
|
||||
run, and are deleted after a `REVOKE CONNECT` command is run.
|
||||
|
||||
It is possible that an error occurs after `REVOKE CONNECT` but before the
|
||||
membership record is deleted. Therefore for authorization purposes, membership
|
||||
of a workspace is not a guarantee that the user has `CONNECT` privileges, just
|
||||
that they might. In cases where the root Postgres user is fetching potentially
|
||||
sensitive data on behalf of a user, the user's actual ability to connect to the
|
||||
database should always be confirmed.
|
||||
|
||||
### Accessing the `phono` schema
|
||||
|
||||
`GRANT USAGE ON <schema> TO <role>;`
|
||||
|
||||
This permission is granted when initially creating the workspace, as well as
|
||||
when accepting an invitation to a table.
|
||||
|
||||
## Creating, updating, and deleting columns
|
||||
|
||||
`GRANT <table owner role> TO <role>;`
|
||||
|
||||
This permission is granted when initially creating the table, as well as when
|
||||
accepting an invitation to a table, if the invitation includes "owner"
|
||||
permissions.
|
||||
|
||||
**This permission is only used via the web UI and must not be granted to service
|
||||
credentials, lest users alter table structure in unsupported ways.**
|
||||
|
||||
### Reading table data
|
||||
|
||||
`GRANT SELECT ON <table> TO <role>;`
|
||||
|
||||
This permission is granted when initially creating the table, as well as when
|
||||
accepting an invitation to the table.
|
||||
|
||||
Phonograph uses `SELECT` permissions to infer whether a table should be
|
||||
accessible to a user via the web UI.
|
||||
|
||||
### Inserting rows
|
||||
|
||||
`GRANT INSERT (<columns>) ON <table> TO <role>;`
|
||||
|
||||
Write-protected columns (`_id`, etc.) are excluded.
|
||||
|
||||
This permission is granted when initially creating the table, as well as when
|
||||
accepting an invitation to the table, if the invitation includes "edit"
|
||||
permissions.
|
||||
|
||||
These permissions must be updated for each relevant user role whenever a column
|
||||
is added; this is simplified by maintaining a single "writer" role per table.
|
||||
|
||||
### Updating rows
|
||||
|
||||
`GRANT UPDATE (<columns>) ON <table> TO <role>;`
|
||||
|
||||
Write-protected columns (`_id`, etc.) are excluded.
|
||||
|
||||
This permission is granted when initially creating the table, as well as when
|
||||
accepting an invitation to the table, if the invitation includes "edit"
|
||||
permissions.
|
||||
|
||||
These permissions must be updated for each relevant user role whenever a column
|
||||
is added; this is simplified by maintaining a single "writer" role per table.
|
||||
|
||||
## Actions Facilitated by Root
|
||||
|
||||
- Creating tables
|
||||
|
||||
## Service Credentials
|
||||
|
||||
Direct user PostgreSQL connections are performed using secondary `LOGIN` roles
|
||||
created by the user's primary workspace role (where the primary workspace role
|
||||
is e.g. `usr_{user_id}`). The credentials for these secondary roles are referred
|
||||
to as "service credentials" or "PostgreSQL credentials". Service credentials are
|
||||
created and assigned permissions by users in the web UI, and their permissions
|
||||
are revoked manually in the web UI and/or by cascading `REVOKE` commands
|
||||
targeting the primary workspace role.
|
||||
|
||||
Service credential role names have the format
|
||||
`svc_{user_id}_{8 chars (4 bytes) of random hex}`. With the user ID consuming 32
|
||||
characters, this balances name length with an ample space for possible names.
|
||||
|
||||
# Copyright and License
|
||||
|
||||
All original source code in this repository is copyright (C) 2025 Second System
|
||||
Technologies LLC and distributed under the terms in
|
||||
[the "LICENSE" file](./LICENSE). Certain third-party assets within
|
||||
[the "static" directory](./static) may be governed by different licenses, for
|
||||
example the Open Font License or MIT License, as stated by their original
|
||||
authors. Copies of each relevant license have been included alongside these
|
||||
files as needed.
|
||||
|
||||
# Footnotes
|
||||
|
||||
[^1]:
|
||||
Barring historical pedantry, "Postgres" and "PostgreSQL" are essentially
|
||||
[^1]: Barring historical pedantry, "Postgres" and "PostgreSQL" are essentially
|
||||
synonymous and are often used interchangeably. As a matter of convention
|
||||
throughout Phonograph docs, "Postgres" is largely used to refer to the
|
||||
database software, while "PostgreSQL" is typically used to refer to the
|
||||
|
|
|
|||
133
docs/auth.md
Normal file
133
docs/auth.md
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
# The Phonograph Authorization Model
|
||||
|
||||
Postgres provides a sophisticated role based access control (RBAC) system, which
|
||||
Phonograph leverages to apply permissions consistently across the web UI and
|
||||
inbound PostgreSQL[^1] connections.
|
||||
|
||||
In order to efficiently pool database connections from the application server,
|
||||
most actions initiated via the web UI are run as the corresponding database user
|
||||
role using the
|
||||
[`SET ROLE` command](https://www.postgresql.org/docs/current/sql-set-role.html).
|
||||
`SET ROLE` **does not** provide great insulation against privilege escalation.
|
||||
**Queries which are not thoroughly validated and escaped must only be run via a
|
||||
dedicated connection initiated with the user-level role's credentials.**
|
||||
|
||||
Given complete freedom it is possible, in fact easy, to configure a Postgres
|
||||
table into what would be considered an "invalid" state by Phonograph, so table
|
||||
creation and ownership is restricted to the "root" Phonograph role, which acts
|
||||
on the behalf of the user in order to facilitate schema updates via the web
|
||||
interface.
|
||||
|
||||
## Permissions Granted via User Roles
|
||||
|
||||
### Accessing workspace databases
|
||||
|
||||
`GRANT CONNECT ON <database> TO <role>;`
|
||||
|
||||
This permission is granted when initially creating the workspace, as well as
|
||||
when accepting an invitation to a table.
|
||||
|
||||
Access to workspaces is controlled via the `CONNECT ON DATABASE` permission.
|
||||
However, it is unreasonable to query every backing cluster to compute the set of
|
||||
workspaces to which a user has access, so Phonograph caches workspace-level
|
||||
"connect" permissions in its own centralized table (`workspace_memberships`).
|
||||
|
||||
`workspace_memberships` rows are added whenever the `GRANT CONNECT` command is
|
||||
run, and are deleted after a `REVOKE CONNECT` command is run.
|
||||
|
||||
It is possible that an error occurs after `REVOKE CONNECT` but before the
|
||||
membership record is deleted. Therefore for authorization purposes, membership
|
||||
of a workspace is not a guarantee that the user has `CONNECT` privileges, just
|
||||
that they might. The user's actual ability to connect to the database should
|
||||
always be confirmed before actually fetching data or metadata from a backing
|
||||
database.
|
||||
|
||||
**Caution!** The Postgres database `CONNECT` privilege has no effect after a
|
||||
connection has been established, and _it is not re-checked by Postgres when
|
||||
running the `SET USER` command_. **The Phonograph server is solely responsible
|
||||
for managing top-level workspace permissions when accessing data via
|
||||
established, pooled connections.**
|
||||
|
||||
### Accessing the `phono` schema
|
||||
|
||||
`GRANT USAGE ON <schema> TO <role>;`
|
||||
|
||||
This permission is granted when initially creating the workspace, as well as
|
||||
when accepting an invitation to a table.
|
||||
|
||||
## Creating, updating, and deleting columns
|
||||
|
||||
`GRANT <table owner role> TO <role>;`
|
||||
|
||||
This permission is granted when initially creating the table, as well as when
|
||||
accepting an invitation to a table, if the invitation includes "owner"
|
||||
permissions.
|
||||
|
||||
**This permission is only used via the web UI and must not be granted to service
|
||||
credentials, lest users alter table structure in unsupported ways.**
|
||||
|
||||
### Reading table data
|
||||
|
||||
`GRANT SELECT ON <table> TO <role>;`
|
||||
|
||||
This permission is granted when initially creating the table, as well as when
|
||||
accepting an invitation to the table.
|
||||
|
||||
Phonograph uses `SELECT` permissions to infer whether a table should be
|
||||
accessible to a user via the web UI.
|
||||
|
||||
### Inserting rows
|
||||
|
||||
`GRANT INSERT (<columns>) ON <table> TO <role>;`
|
||||
|
||||
Write-protected columns (`_id`, etc.) are excluded.
|
||||
|
||||
This permission is granted when initially creating the table, as well as when
|
||||
accepting an invitation to the table, if the invitation includes "edit"
|
||||
permissions.
|
||||
|
||||
These permissions must be updated for each relevant user role whenever a column
|
||||
is added; this is simplified by maintaining a single "writer" role per table.
|
||||
|
||||
Note that granting insert or update privileges on specific columns will be
|
||||
reflected in relation ACLs (that is, in the `pg_class` table) as the
|
||||
corresponding privilege on the relation overall. In other words, a role listed
|
||||
as having insert and/or update permissions in a relation's ACL items might not
|
||||
have insert and/or update permissions to all columns.
|
||||
|
||||
### Updating rows
|
||||
|
||||
`GRANT UPDATE (<columns>) ON <table> TO <role>;`
|
||||
|
||||
Write-protected columns (`_id`, etc.) are excluded.
|
||||
|
||||
This permission is granted when initially creating the table, as well as when
|
||||
accepting an invitation to the table, if the invitation includes "edit"
|
||||
permissions.
|
||||
|
||||
These permissions must be updated for each relevant user role whenever a column
|
||||
is added; this is simplified by maintaining a single "writer" role per table.
|
||||
|
||||
Note that granting insert or update privileges on specific columns will be
|
||||
reflected in relation ACLs (that is, in the `pg_class` table) as the
|
||||
corresponding privilege on the relation overall. In other words, a role listed
|
||||
as having insert and/or update permissions in a relation's ACL items might not
|
||||
have insert and/or update permissions to all columns.
|
||||
|
||||
## Actions Facilitated by Root
|
||||
|
||||
- Creating tables
|
||||
|
||||
## Service Credentials
|
||||
|
||||
Direct user PostgreSQL connections are performed using secondary `LOGIN` roles
|
||||
created by the user's primary workspace role (where the primary workspace role
|
||||
is e.g. `usr_{user_id}`). The credentials for these secondary roles are referred
|
||||
to as "service credentials" or "PostgreSQL credentials". Service credentials are
|
||||
created and assigned permissions by users in the web UI, and their permissions
|
||||
are revoked manually in the web UI and/or by cascading `REVOKE` commands
|
||||
targeting the primary workspace role.
|
||||
|
||||
Service credential role names have the format
|
||||
`svc_{user_id}_{8 chars (4 bytes) of random hex}`. With the user ID consuming 32
|
||||
characters, this balances name length with an ample space for possible names.
|
||||
|
|
@ -2,8 +2,8 @@ use std::collections::HashSet;
|
|||
|
||||
use derive_builder::Builder;
|
||||
use phono_backends::{
|
||||
client::WorkspaceClient, pg_acl::PgPrivilegeType, pg_class::PgClass, pg_role::RoleTree,
|
||||
rolnames::ROLE_PREFIX_USER,
|
||||
client::WorkspaceClient, pg_acl::PgPrivilegeType, pg_class::PgClass, pg_database::PgDatabase,
|
||||
pg_role::RoleTree, rolnames::ROLE_PREFIX_USER,
|
||||
};
|
||||
use sqlx::postgres::types::Oid;
|
||||
use tracing::{Instrument, info, info_span};
|
||||
|
|
@ -84,10 +84,6 @@ impl<'a> Accessor<Portal> for PortalAccessor<'a> {
|
|||
async fn fetch_one(self) -> Result<Portal, AccessError> {
|
||||
let spec = self.build()?;
|
||||
async {
|
||||
// FIXME: Do we need to explicitly verify that the actor has
|
||||
// database `CONNECT` privileges, or is that implicitly given by the
|
||||
// fact that a workspace client has already been acquired?
|
||||
|
||||
let portal = Portal::with_id(spec.id)
|
||||
.fetch_optional(spec.using_app_db)
|
||||
.await?
|
||||
|
|
@ -127,6 +123,24 @@ impl<'a> Accessor<Portal> for PortalAccessor<'a> {
|
|||
};
|
||||
|
||||
if let Some(actor_rolname) = actor_rolname {
|
||||
// Verify database CONNECT permissions.
|
||||
let pg_db = PgDatabase::current()
|
||||
.fetch_one(spec.using_workspace_client)
|
||||
.await?;
|
||||
if !pg_db.datacl.unwrap_or_default().iter().any(|acl| {
|
||||
// Currently database connect permissions are always granted
|
||||
// directly, though this may change in the future.
|
||||
// TODO: Generalize to inherited roles
|
||||
acl.grantee == actor_rolname
|
||||
&& acl
|
||||
.privileges
|
||||
.iter()
|
||||
.any(|privilege| privilege.privilege == PgPrivilegeType::Connect)
|
||||
}) {
|
||||
info!("actor lacks postgres connect privileges");
|
||||
return Err(AccessError::NotFound);
|
||||
}
|
||||
|
||||
// Verify ACL permissions.
|
||||
//
|
||||
// No need to explicitly check whether
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue