Deploy msgboard
Browse files- src/msgboard/db.py +17 -1
- src/msgboard/store.py +18 -2
src/msgboard/db.py
CHANGED
|
@@ -458,10 +458,26 @@ CREATE INDEX messages_response_dm ON messages(channel_id, sender_id, created_at,
|
|
| 458 |
CREATE INDEX attachments_published_at ON attachments(published, created_at);
|
| 459 |
"""
|
| 460 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 461 |
# Index i holds the SQL that moves the schema from version i to i + 1.
|
| 462 |
MIGRATIONS = [SCHEMA_V1, SCHEMA_V2, SCHEMA_V3, SCHEMA_V4, SCHEMA_V5, SCHEMA_V6, SCHEMA_V7, SCHEMA_V8, SCHEMA_V9,
|
| 463 |
SCHEMA_V10, SCHEMA_V11, SCHEMA_V12, SCHEMA_V13, SCHEMA_V14, SCHEMA_V15, SCHEMA_V16, SCHEMA_V17, SCHEMA_V18, SCHEMA_V19, SCHEMA_V20, SCHEMA_V21, SCHEMA_V22,
|
| 464 |
-
SCHEMA_V23, SCHEMA_V24, SCHEMA_V25]
|
| 465 |
SCHEMA_VERSION = len(MIGRATIONS)
|
| 466 |
|
| 467 |
|
|
|
|
| 458 |
CREATE INDEX attachments_published_at ON attachments(published, created_at);
|
| 459 |
"""
|
| 460 |
|
| 461 |
+
# Agents are owned by people: an agent that an agent invited moves to the person at the top of the chain.
|
| 462 |
+
SCHEMA_V26 = """
|
| 463 |
+
WITH RECURSIVE up(agent, cur, path) AS (
|
| 464 |
+
SELECT p.id, p.owner_id, ',' || p.id || ',' FROM principals p JOIN principals o ON o.id = p.owner_id
|
| 465 |
+
WHERE o.identity_type != 'user'
|
| 466 |
+
UNION ALL
|
| 467 |
+
SELECT u.agent, o.owner_id, u.path || o.id || ',' FROM up u JOIN principals o ON o.id = u.cur
|
| 468 |
+
WHERE o.identity_type != 'user' AND o.owner_id IS NOT NULL AND instr(u.path, ',' || o.id || ',') = 0
|
| 469 |
+
),
|
| 470 |
+
root(agent, person) AS (
|
| 471 |
+
SELECT u.agent, u.cur FROM up u JOIN principals h ON h.id = u.cur WHERE h.identity_type = 'user'
|
| 472 |
+
)
|
| 473 |
+
UPDATE principals SET owner_id = (SELECT person FROM root WHERE root.agent = principals.id)
|
| 474 |
+
WHERE id IN (SELECT agent FROM root);
|
| 475 |
+
"""
|
| 476 |
+
|
| 477 |
# Index i holds the SQL that moves the schema from version i to i + 1.
|
| 478 |
MIGRATIONS = [SCHEMA_V1, SCHEMA_V2, SCHEMA_V3, SCHEMA_V4, SCHEMA_V5, SCHEMA_V6, SCHEMA_V7, SCHEMA_V8, SCHEMA_V9,
|
| 479 |
SCHEMA_V10, SCHEMA_V11, SCHEMA_V12, SCHEMA_V13, SCHEMA_V14, SCHEMA_V15, SCHEMA_V16, SCHEMA_V17, SCHEMA_V18, SCHEMA_V19, SCHEMA_V20, SCHEMA_V21, SCHEMA_V22,
|
| 480 |
+
SCHEMA_V23, SCHEMA_V24, SCHEMA_V25, SCHEMA_V26]
|
| 481 |
SCHEMA_VERSION = len(MIGRATIONS)
|
| 482 |
|
| 483 |
|
src/msgboard/store.py
CHANGED
|
@@ -1061,6 +1061,18 @@ def _join_channels(conn, invite, principal_id: int, now: float) -> None:
|
|
| 1061 |
"VALUES (?, ?, ?, ?)", (ch["id"], principal_id, invite["created_by"], now))
|
| 1062 |
|
| 1063 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1064 |
def join(conn, code, name, display_name, now: float) -> dict:
|
| 1065 |
"""Redeem an invite (or a sign-in link). Returns the identity and its token."""
|
| 1066 |
row = _find_invite(conn, code)
|
|
@@ -1103,10 +1115,14 @@ def join(conn, code, name, display_name, now: float) -> dict:
|
|
| 1103 |
audit(conn, pid, "join.human", f"principal:{pid}", {"invite": row["id"], "role": row["role"]}, now)
|
| 1104 |
return {"token": token, "principal": principal_json(conn, _principal_row(conn, pid)), "created": True, "next": JOIN_NEXT}
|
| 1105 |
|
| 1106 |
-
# Agent or service account
|
|
|
|
| 1107 |
needed = auth.owner_roles_for(row["role"])
|
| 1108 |
if inviter["role"] not in needed:
|
| 1109 |
raise ApiError(410, "invite_inactive", "the person who made this invite can no longer give out this access")
|
|
|
|
|
|
|
|
|
|
| 1110 |
if row["agent_name"]: # a named invite decides the name; the agent need not (and may not) pick another
|
| 1111 |
if name not in (None, "", row["agent_name"]):
|
| 1112 |
raise bad_request(f"this invite is for the agent {row['agent_name']!r}: leave name out or use that one")
|
|
@@ -1127,7 +1143,7 @@ def join(conn, code, name, display_name, now: float) -> dict:
|
|
| 1127 |
"""INSERT INTO principals (issuer, external_id, identity_type, role, display_name, created_at, created_by,
|
| 1128 |
joined_via, owner_id, can_create_channels, invite_id)
|
| 1129 |
VALUES (?, ?, ?, ?, ?, ?, ?, 'invite', ?, ?, ?)""",
|
| 1130 |
-
(issuer, name, row["identity_type"], row["role"], display_name, now, inviter["id"],
|
| 1131 |
row["can_create_channels"], row["id"])).lastrowid
|
| 1132 |
conn.execute("UPDATE invites SET uses = uses + 1 WHERE id = ?", (row["id"],))
|
| 1133 |
_join_channels(conn, row, pid, now)
|
|
|
|
| 1061 |
"VALUES (?, ?, ?, ?)", (ch["id"], principal_id, invite["created_by"], now))
|
| 1062 |
|
| 1063 |
|
| 1064 |
+
def _human_owner(conn, row):
|
| 1065 |
+
"""The person at the top of an ownership chain: the row itself for a person, else its owner's owner, and so on.
|
| 1066 |
+
A chain that loops or ends without a person is refused rather than answered with an agent."""
|
| 1067 |
+
seen = set()
|
| 1068 |
+
while row["identity_type"] != "user":
|
| 1069 |
+
if row["owner_id"] is None or row["id"] in seen:
|
| 1070 |
+
raise ApiError(410, "invite_inactive", "no person owns the agent that made this invite")
|
| 1071 |
+
seen.add(row["id"])
|
| 1072 |
+
row = _principal_row(conn, row["owner_id"])
|
| 1073 |
+
return row
|
| 1074 |
+
|
| 1075 |
+
|
| 1076 |
def join(conn, code, name, display_name, now: float) -> dict:
|
| 1077 |
"""Redeem an invite (or a sign-in link). Returns the identity and its token."""
|
| 1078 |
row = _find_invite(conn, code)
|
|
|
|
| 1115 |
audit(conn, pid, "join.human", f"principal:{pid}", {"invite": row["id"], "role": row["role"]}, now)
|
| 1116 |
return {"token": token, "principal": principal_json(conn, _principal_row(conn, pid)), "created": True, "next": JOIN_NEXT}
|
| 1117 |
|
| 1118 |
+
# Agent or service account. It is owned by a person: the inviter, or, when an agent made the invite,
|
| 1119 |
+
# the person who owns that agent. The inviting agent stays on record as created_by.
|
| 1120 |
needed = auth.owner_roles_for(row["role"])
|
| 1121 |
if inviter["role"] not in needed:
|
| 1122 |
raise ApiError(410, "invite_inactive", "the person who made this invite can no longer give out this access")
|
| 1123 |
+
owner = _human_owner(conn, inviter)
|
| 1124 |
+
if owner["disabled"] or owner["role"] not in needed:
|
| 1125 |
+
raise ApiError(410, "invite_inactive", "the person who owns the agent that made this invite can no longer give out this access")
|
| 1126 |
if row["agent_name"]: # a named invite decides the name; the agent need not (and may not) pick another
|
| 1127 |
if name not in (None, "", row["agent_name"]):
|
| 1128 |
raise bad_request(f"this invite is for the agent {row['agent_name']!r}: leave name out or use that one")
|
|
|
|
| 1143 |
"""INSERT INTO principals (issuer, external_id, identity_type, role, display_name, created_at, created_by,
|
| 1144 |
joined_via, owner_id, can_create_channels, invite_id)
|
| 1145 |
VALUES (?, ?, ?, ?, ?, ?, ?, 'invite', ?, ?, ?)""",
|
| 1146 |
+
(issuer, name, row["identity_type"], row["role"], display_name, now, inviter["id"], owner["id"],
|
| 1147 |
row["can_create_channels"], row["id"])).lastrowid
|
| 1148 |
conn.execute("UPDATE invites SET uses = uses + 1 WHERE id = ?", (row["id"],))
|
| 1149 |
_join_channels(conn, row, pid, now)
|