π Access and Locks¶
Meerschaum is open by default. On an instance where nobody sets up teams, access or locks, every user sees and changes every pipe within their permissions, and nothing here needs a thought. When you need more, two reserved parameters say who a pipe belongs to and what may happen to it:
access: the pipe's owner, team and visibility. The API server, the MCP server and the web console decide with it who may see and change the pipe.locks: the operations the pipe refuses (syncs, clears, drops, ...), everywhere: the command line, Python, background jobs, the Syncing Engine, the API and the web console.
Who enforces what
Teams, visibility and permissions apply to the users an API server serves (every api:
instance, its MCP tools and its web console). Anyone holding an instance's database
credentials works with it directly and sees everything in it: the credentials are the trust
boundary. To keep teams' data apart for certain, give each team its own connectors and
database users. Locks are the exception: they refuse their operations whoever asks.
The command line does the same through mrsm lock pipes, mrsm edit pipes access and the team
actions: see Teams, Access and Locks.
Locks¶
A lock refuses its operations on a pipe until someone removes it. -y, --force and
--allow-protected (or allow_protected() in Python) never get past a lock.
import meerschaum as mrsm
pipe = mrsm.Pipe('sql:main', 'weather', instance='sql:main')
pipe.lock('drop', 'delete') # (True, "Locked Pipe('sql:main', 'weather') (drop, delete).")
pipe.locks # ['drop', 'delete']
pipe.clear(begin='2024-01-01') # allowed: no `clear` lock
pipe.drop()
# (False, "Cannot drop Pipe('sql:main', 'weather'): it is locked (drop). Unlock it with
# `mrsm unlock pipes -c sql:main -m weather --locks drop` or in the web console (Manage > Locks).")
pipe.unlock('drop') # still locked: delete
pipe.unlock() # every lock
pipe.lock() # `all`
| Lock | Refuses |
|---|---|
parameters |
Parameter edits (pipe.edit(), update_parameters(..., persist=True), set_access(), edit pipes). Changing the locks themselves stays allowed. |
sync |
Writes: syncs, replaced windows (replace=True), deduplication, verification syncs. |
clear |
Row deletes: clears, replaced windows, rebuilds. |
drop |
Drops, rebuilds and changes to the table's schema: verify schema (pipe.verify_schema(apply=True)), repartitioning, dropping indices. |
delete |
Deleting (unregistering) the pipe. |
all |
All of the above. |
Some operations need more than one kind unlocked:
| Operation | Refused by |
|---|---|
pipe.sync(df, begin=..., end=..., replace=True), sync pipes --replace |
sync, clear |
A rebuild (a definition change, approved with --yes) |
sync, clear, drop |
pipe.delete(), delete pipes (they drop the table first) |
drop, delete |
pipe.delete(drop=False) (the registration only) |
delete |
pipe.repartition(...), mrsm repartition pipes (it rebuilds the table and records the width) |
drop, parameters |
- A refusal is a
SuccessTuplelike any other ((False, "Cannot ...: it is locked (...)...")). It names the pipe, the lock and the command that removes it. - The registered locks are the ones in force, read again before every write: a lock set in another process or by another user applies at once. A pipe that is not registered keeps the locks you give it in memory and saves them when it is registered.
- Under a
parameterslock, syncs still write. What a sync would otherwise record in the parameters (the dtype of a new JSON, UUID, numeric or geometry column, a newautoincrement) is not recorded: the lock keeps the parameters as they are. - Renaming or deleting a team and deleting a user still update the
accessof locked pipes, so a pipe never names a team that no longer exists. - Meerschaum's internal pipes (users, tokens, plugins, teams, engine state) have no locks. Saved
Pipelines do: their registry pipe
Pipe('mrsm', 'pipeline', name)carrieslockslike any pipe.
Locks in Pipeline runs¶
In a Pipeline run (pipe.sync(parents=True), Pipeline.sync(),
mrsm sync pipes --parents, a scheduled Pipeline job), a pipe locked against syncs is not synced
and its status is locked. It is not a failure: the run succeeds, and the pipes downstream sync
from its current data. A pipe whose definition changed is rebuilt (cleared, then synced again)
only when drop and clear are unlocked; until then it is locked too, with --yes or
without. The run's warnings say which pipes were locked and how to unlock them.
A direct sync of a locked pipe (pipe.sync(), mrsm sync pipes -m weather) is refused like any
other locked write.
Access¶
pipe.set_access(team='analytics', visibility='private', edit='owner', owner='carol')
pipe.access
# {'owner': 'carol', 'team': 'analytics', 'visibility': 'private', 'edit': 'owner'}
pipe.owner, pipe.team, pipe.visibility, pipe.edit_mode
# ('carol', 'analytics', 'private', 'owner')
pipe.set_access(owner=None) # no owner (the other keys keep their values)
pipe.set_access(team=None) # back to Everyone
| Key | Values |
|---|---|
owner |
A username, or None. |
team |
A team name; None is the built-in team Everyone. |
visibility |
team (default): the team's members (and the owner). private: the owner, the team's managers and admins. everyone: every user. |
edit |
team (default): anyone who can see the pipe may change it within their permissions. owner: the owner, the team's managers and admins. |
A pipe without access belongs to Everyone, with visibility team and edit team: everyone sees
and changes it. access is an ordinary parameter, so compose files, --params and
pipe.parameters may set it too. Through an api: instance:
- a pipe someone registers belongs to them (admins may name another owner) and to their primary team unless they name another;
- only the owner, the team's managers and admins change a pipe's
access, lock it or unlock it; someone other than an admin may only give a pipe to a team they are in, and only managers and admins give it to another owner (an owner may give up ownership).
copy pipes keeps the source's locks and never copies its access. Deleting a user clears their
ownership: their pipes stay with their teams.
Teams¶
Every user is a member of the built-in team Everyone, which cannot be deleted, renamed or left
(its record may name managers). Admins create more teams; a user can be in several, as a
member or a manager. Managers add and remove members of their team and manage the access
of its pipes. Teams live on the instance beside users and tokens (the internal pipe
Pipe('mrsm', 'teams'), table mrsm_teams).
import meerschaum as mrsm
team = mrsm.Team('analytics', instance='sql:main', members=['alice', 'bob'], managers=['carol'])
team.register() # (True, "Registered team 'analytics'.")
team.add_members('dave') # saved at once
team.add_managers('bob')
team.remove_members('alice')
team.members # ['bob', 'carol', 'dave'] (managers included)
team.managers # ['bob', 'carol']
team.rename('data-team') # its pipes and jobs follow
team.delete() # its pipes and jobs move to Everyone
[t.name for t in mrsm.get_teams('sql:main')] # ['Everyone', ...]
- Team names follow the username rules (letters, digits and
_ - . @) and are unique without case:mrsm.Team('ANALYTICS')is the teamanalytics. - Members must be registered users.
- Renaming a team renames it in the
accessof its pipes and saved Pipelines, in its users' primary team and in the jobs started for it; deleting one moves them to Everyone.
Users, roles and permissions¶
Permissions are the token scopes (meerschaum.core.access.ALL_PERMISSIONS), from pipes:read to
webterm:use. Roles are presets of them:
| Role | Permissions |
|---|---|
viewer |
Read pipes, jobs, logs, plugins and the connectors list (secrets masked); read and edit their own account. |
editor |
Viewer, plus register, edit and sync pipes (SQL definitions included: pipes:register, pipes:write, pipes:define_sql), start existing jobs and run read-only SQL. |
operator |
Editor, plus run actions (actions:execute), use the web terminal (webterm:use), and create, stop, pause and delete jobs. |
admin |
The user type admin: every permission, users, teams and connector details included. |
A user's permissions: an admin holds all of them; a user with explicit permissions
(attributes['scopes'], '*' for every permission) holds those; anyone else holds their role's
(attributes['role']), else the instance's default role (api:permissions:default_role:
editor unless configured; Meerschaum 4's api:permissions:actions:non_admin: true, when your
configuration sets it, means operator).
from meerschaum.core import User
user = User('bob', instance='sql:main')
user.role # 'editor'
user.permissions # ['pipes:read', 'pipes:write', ...]
user.set_role('viewer') # saved; explicit permissions are removed
user.set_permissions(['pipes:read', 'logs:read']) # user.role == 'custom'
user.teams # [('Everyone', 'member'), ('analytics', 'manager')]
user.primary_team # 'analytics'
- A primary team (
attributes['primary_team']) is the team of the pipes a user registers through the API or the web console. Without one, it is their first team other than Everyone, else Everyone. - Scope lists written before these permissions existed keep what they allowed:
pipes:writethere also grantspipes:registerandpipes:define_sql, andactions:executegrantswebterm:use. - A token never holds more than its user: its scopes are intersected with the user's current permissions when it is used.
The rules in Python¶
meerschaum.core.access applies the rules the API server applies (they are the core's, so both
always agree). The web console uses it, and plugins can too:
from meerschaum.core.access import Principal, can_see, can_edit, filter_visible_pipes, get_principal
alice = get_principal('alice', instance='sql:main') # her permissions and teams
can_see(alice, pipe), can_edit(alice, pipe)
visible = filter_visible_pipes(alice, mrsm.get_pipes(instance='sql:main', as_list=True))
guest = Principal('guest', permissions=['pipes:read'], teams=[('analytics', 'member')])
can_see(guest, {'team': 'analytics', 'visibility': 'private'}) # False: managers only
| Function | Answers |
|---|---|
can_see(principal, access) |
Admins and the owner always; everyone: every user; team: its members; private: its managers. |
can_edit(principal, access) |
Who can see it (edit team), or the owner, the team's managers and admins (edit owner). |
can_manage_access(principal, access), can_lock(...) |
The owner, the team's managers and admins. |
can_see_email(viewer, user_teams, target_username=) |
Admins, the managers of a team the user is in, and the user themself. |
check_lock(locks, op), lock_refusal(pipe, locks, op) |
Whether a write ('sync', 'replace', 'clear', 'drop', 'rebuild', 'schema', 'delete', ...) is refused, and the message. |
user_permissions(type, attributes), role_permissions(role), token_permissions(scopes, user_permissions) |
Permissions, as described above. |
access may be a dictionary, a pipe or a pipe's parameters; a principal a Principal or a
dictionary with username, is_admin, permissions and teams.
Custom instance connectors¶
A custom instance connector gets teams, locks and access with no code of
its own: the defaults store teams in the internal pipe Pipe('mrsm', 'teams') through the
connector's own pipe methods (sync_pipe with upserts on team_id, get_pipe_data,
clear_pipe with params), as users and tokens are stored, and the core checks the locks before
every write it sends to the connector. Users are looked up by their exact username: a username that
starts with _ (which a params filter reads as a negation) never matches another user.