Skip to content

Commit dff6ec7

Browse files
committed
Add 'accessible' permission flag
This adds a new 'accessible' permission bit that allows restricting all metadata room info. This allows you to distinguish between rooms that should be entirely secret (i.e. that random users can't learn anything about): read=False, accessible=False and rooms where users can get metadata but not read messages: read=False, accessible=True
1 parent 00e220b commit dff6ec7

File tree

11 files changed

+327
-67
lines changed

11 files changed

+327
-67
lines changed

api.yaml

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ paths:
3737
tags: [Rooms]
3838
summary: "Returns a list of available rooms on the server."
3939
description: >
40-
Rooms to which the user does not have access (e.g. because they are banned) are not
41-
included.
40+
Rooms to which the user does not have access (e.g. because they are banned or the room or
41+
requires specific access) are not included.
4242
responses:
4343
200:
4444
description: successful operation
@@ -63,8 +63,8 @@ paths:
6363
$ref: "#/components/schemas/Room"
6464
403:
6565
description: >
66-
Forbidden. Returned if the user is banned from the room or otherwise does not have read
67-
access to the room.
66+
Forbidden. Returned if the user is banned from the room or does not have access to the
67+
room.
6868
content: {}
6969
put:
7070
tags: [Rooms]
@@ -104,6 +104,15 @@ paths:
104104
Sets the default "read" permission (if true: users can read messages) for users
105105
in this room who do not otherwise have specific permissions applied.
106106
example: true
107+
default_accessible:
108+
type: boolean
109+
description: >
110+
Sets the default "accessible" permission for users in this room who do not
111+
otherwise have specific permissions applied. This accessible permission only
112+
affects users who do not have read permission: accessible without read still
113+
allows the user to access room metadata (but not messages), while both false
114+
prevents room metadata access as well.
115+
example: true
107116
default_write:
108117
type: boolean
109118
description: >
@@ -186,6 +195,8 @@ paths:
186195
$ref: "#/components/schemas/Room/properties/global_admin"
187196
default_read:
188197
$ref: "#/components/schemas/Room/properties/default_read"
198+
default_accessible:
199+
$ref: "#/components/schemas/Room/properties/default_accessible"
189200
default_write:
190201
$ref: "#/components/schemas/Room/properties/default_write"
191202
default_upload:
@@ -1043,6 +1054,17 @@ paths:
10431054
room's messages even if the room's default allows reading. Specifying this as
10441055
null will explicitly delete any user-specific read override (effectively
10451056
returning the user's read permission to the room's default).
1057+
accessible:
1058+
type: boolean
1059+
nullable: true
1060+
example: false
1061+
description: >
1062+
If true this grants permission to read the room's metadata when the user doesn't
1063+
have read permission. That is, having this true and read false means the user
1064+
cannot read messages, but can get information about the room, while both false
1065+
means the user cannot access any details of the room. Specifying this as null
1066+
will explicitly delete any user-specific accessible override, returning the
1067+
user's effective permission to the room's default.
10461068
write:
10471069
type: boolean
10481070
nullable: true
@@ -1659,6 +1681,15 @@ components:
16591681
Whether new users have permission to read posts in this room by default. This property
16601682
is only returned if the calling user has moderator/admin permissions.
16611683
example: true
1684+
default_accessible:
1685+
type: boolean
1686+
description: >
1687+
Whether new users have permission to access this room's information. This property only has
1688+
an effect for users who do not have read permission; it is designed to be set to false
1689+
(along with default_read) in order to create a room that users can neither read messages
1690+
from nor access any room metadata. This property is only returned if the calling user
1691+
has moderator/admin permissions.
1692+
example: true
16621693
default_write:
16631694
type: boolean
16641695
description: >

sogs/migrations/__init__.py

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
message_views,
1010
new_columns,
1111
new_tables,
12+
room_accessible,
1213
seqno_etc,
1314
user_perm_futures,
1415
v_0_1_x,
@@ -33,23 +34,11 @@ def migrate(conn):
3334
seqno_etc,
3435
message_views,
3536
user_perm_futures,
37+
room_accessible,
3638
):
3739
changes = False
3840
with db.transaction(conn):
3941
changes = migration.migrate(conn)
4042
if changes:
4143
db.metadata.clear()
4244
db.metadata.reflect(bind=db.engine, views=True)
43-
44-
45-
# migrate_v01x,
46-
# add_new_tables,
47-
# add_new_columns,
48-
# create_message_details_deleter,
49-
# check_for_hacks,
50-
# seqno_etc_updates,
51-
# update_message_views,
52-
# user_perm_future_updates,
53-
54-
# add_accessible_perm_bit,
55-
# ):

sogs/migrations/room_accessible.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import logging
2+
3+
4+
def migrate(conn):
5+
"""Add the room.accessible permission flag, and associated column/view changes"""
6+
7+
from .. import db
8+
9+
if 'accessible' in db.metadata.tables['rooms'].c:
10+
return False
11+
12+
logging.warning("DB migration: adding 'accessible' room permission columns")
13+
14+
conn.execute("ALTER TABLE rooms ADD COLUMN accessible BOOLEAN NOT NULL DEFAULT TRUE")
15+
conn.execute("ALTER TABLE user_permission_overrides ADD COLUMN accessible BOOLEAN")
16+
conn.execute("DROP TRIGGER IF EXISTS user_perms_empty_cleanup")
17+
conn.execute("DROP VIEW IF EXISTS user_permissions")
18+
19+
sqlite = db.engine.name == "sqlite"
20+
conn.execute(
21+
f"""
22+
CREATE VIEW user_permissions AS
23+
SELECT
24+
rooms.id AS room,
25+
users.id AS {'user' if sqlite else '"user"'},
26+
users.session_id,
27+
CASE WHEN users.banned THEN TRUE ELSE COALESCE(user_permission_overrides.banned, FALSE) END AS banned,
28+
CASE WHEN users.moderator THEN TRUE ELSE COALESCE(user_permission_overrides.read, rooms.read) END AS read,
29+
CASE WHEN users.moderator THEN TRUE ELSE COALESCE(user_permission_overrides.accessible, rooms.accessible) END AS accessible,
30+
CASE WHEN users.moderator THEN TRUE ELSE COALESCE(user_permission_overrides.write, rooms.write) END AS write,
31+
CASE WHEN users.moderator THEN TRUE ELSE COALESCE(user_permission_overrides.upload, rooms.upload) END AS upload,
32+
CASE WHEN users.moderator THEN TRUE ELSE COALESCE(user_permission_overrides.moderator, FALSE) END AS moderator,
33+
CASE WHEN users.admin THEN TRUE ELSE COALESCE(user_permission_overrides.admin, FALSE) END AS admin,
34+
-- room_moderator will be TRUE if the user is specifically listed as a moderator of the room
35+
COALESCE(user_permission_overrides.moderator OR user_permission_overrides.admin, FALSE) AS room_moderator,
36+
-- global_moderator will be TRUE if the user is a global moderator/admin (note that this is
37+
-- *not* exclusive of room_moderator: a moderator/admin could be listed in both).
38+
COALESCE(users.moderator OR users.admin, FALSE) as global_moderator,
39+
-- visible_mod will be TRUE if this mod is a publicly viewable moderator of the room
40+
CASE
41+
WHEN user_permission_overrides.moderator OR user_permission_overrides.admin THEN user_permission_overrides.visible_mod
42+
WHEN users.moderator OR users.admin THEN users.visible_mod
43+
ELSE FALSE
44+
END AS visible_mod
45+
FROM
46+
users {'JOIN' if sqlite else 'CROSS JOIN'} rooms LEFT OUTER JOIN user_permission_overrides ON
47+
(users.id = user_permission_overrides.{'user' if sqlite else '"user"'} AND rooms.id = user_permission_overrides.room)
48+
""" # noqa E501
49+
)
50+
if sqlite:
51+
conn.execute(
52+
"""
53+
CREATE TRIGGER user_perms_empty_cleanup AFTER UPDATE ON user_permission_overrides
54+
FOR EACH ROW WHEN NOT (NEW.banned OR NEW.moderator OR NEW.admin)
55+
AND COALESCE(NEW.accessible, NEW.read, NEW.write, NEW.upload) IS NULL
56+
BEGIN
57+
DELETE from user_permission_overrides WHERE room = NEW.room AND user = NEW.user;
58+
END
59+
"""
60+
)
61+
62+
else:
63+
conn.execute(
64+
"""
65+
CREATE TRIGGER user_perms_empty_cleanup AFTER UPDATE ON user_permission_overrides
66+
FOR EACH ROW WHEN (NOT (NEW.banned OR NEW.moderator OR NEW.admin)
67+
AND COALESCE(NEW.accessible, NEW.read, NEW.write, NEW.upload) IS NULL)
68+
EXECUTE PROCEDURE trigger_user_perms_empty_cleanup();
69+
"""
70+
)
71+
72+
return True

sogs/model/room.py

Lines changed: 64 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ class Room:
4646
info_updates - counter on room metadata that is automatically incremented whenever room
4747
metadata (name, description, image, etc.) changes for the room.
4848
default_read - True if default user permissions includes read permission
49+
default_accessible - True if default user permissions include accessible permission
4950
default_write - True if default user permissions includes write permission
5051
default_upload - True if default user permissions includes file upload permission
5152
"""
@@ -104,8 +105,8 @@ def _refresh(self, *, id=None, token=None, row=None, perms=False):
104105
'info_updates',
105106
)
106107
)
107-
self._default_read, self._default_write, self._default_upload = (
108-
bool(row[c]) for c in ('read', 'write', 'upload')
108+
self._default_read, self._default_accessible, self._default_write, self._default_upload = (
109+
bool(row[c]) for c in ('read', 'accessible', 'write', 'upload')
109110
)
110111

111112
if (
@@ -310,6 +311,16 @@ def default_read(self):
310311
"""Returns True if this room is publicly readable (e.g. by a new user)"""
311312
return self._default_read
312313

314+
@property
315+
def default_accessible(self):
316+
"""
317+
Returns True if this room has the publicly accessible (e.g. by a new user) permission set.
318+
Note that the the accessible permission only applies when `read` is false: if a user has
319+
read permission then they implicitly have accessibility permission even if this field is
320+
false.
321+
"""
322+
return self._default_accessible
323+
313324
@property
314325
def default_write(self):
315326
"""Returns True if this room is publicly writable (e.g. by a new user)"""
@@ -329,6 +340,19 @@ def default_read(self, read: bool):
329340
query("UPDATE rooms SET read = :read WHERE id = :r", r=self.id, read=read)
330341
self._refresh(perms=True)
331342

343+
@default_accessible.setter
344+
def default_accessible(self, accessible: bool):
345+
"""Sets the default accessible permission of the room"""
346+
347+
if accessible != self._default_accessible:
348+
with db.transaction():
349+
query(
350+
"UPDATE rooms SET accessible = :accessible WHERE id = :r",
351+
r=self.id,
352+
accessible=accessible,
353+
)
354+
self._refresh(perms=True)
355+
332356
@default_write.setter
333357
def default_write(self, write: bool):
334358
"""Sets the default write permission of the room"""
@@ -367,6 +391,7 @@ def check_permission(
367391
admin=False,
368392
moderator=False,
369393
read=False,
394+
accessible=False,
370395
write=False,
371396
upload=False,
372397
):
@@ -382,6 +407,9 @@ def check_permission(
382407
- admin -- if true then the user must have admin access to the room
383408
- moderator -- if true then the user must have moderator (or admin) access to the room
384409
- read -- if true then the user must have read access
410+
- accessible -- if true then the user must have accessible access; note that this permission
411+
is satisfied by *either* the `accessible` or `read` database flags (that is: read implies
412+
accessible).
385413
- write -- if true then the user must have write access
386414
- upload -- if true then the user must have upload access; this should usually be combined
387415
with write=True.
@@ -392,9 +420,10 @@ def check_permission(
392420
"""
393421

394422
if user is None:
395-
is_banned, can_read, can_write, can_upload, is_mod, is_admin = (
423+
is_banned, can_read, can_access, can_write, can_upload, is_mod, is_admin = (
396424
False,
397425
bool(self.default_read),
426+
bool(self.default_accessible),
398427
bool(self.default_write),
399428
bool(self.default_upload),
400429
False,
@@ -404,15 +433,24 @@ def check_permission(
404433
if user.id not in self._perm_cache:
405434
row = query(
406435
"""
407-
SELECT banned, read, write, upload, moderator, admin FROM user_permissions
436+
SELECT banned, read, accessible, write, upload, moderator, admin
437+
FROM user_permissions
408438
WHERE room = :r AND "user" = :u
409439
""",
410440
r=self.id,
411441
u=user.id,
412442
).first()
413443
self._perm_cache[user.id] = [bool(c) for c in row]
414444

415-
is_banned, can_read, can_write, can_upload, is_mod, is_admin = self._perm_cache[user.id]
445+
(
446+
is_banned,
447+
can_read,
448+
can_access,
449+
can_write,
450+
can_upload,
451+
is_mod,
452+
is_admin,
453+
) = self._perm_cache[user.id]
416454

417455
if is_admin:
418456
return True
@@ -424,6 +462,7 @@ def check_permission(
424462
return False
425463
return (
426464
not is_banned
465+
and (not accessible or can_access or can_read)
427466
and (not read or can_read)
428467
and (not write or can_write)
429468
and (not upload or can_upload)
@@ -437,6 +476,9 @@ def check_unbanned(self, user: Optional[User]):
437476
def check_read(self, user: Optional[User] = None):
438477
return self.check_permission(user, read=True)
439478

479+
def check_accessible(self, user: Optional[User] = None):
480+
return self.check_permission(user, accessible=True)
481+
440482
def check_write(self, user: Optional[User] = None):
441483
return self.check_permission(user, write=True)
442484

@@ -1118,19 +1160,20 @@ def get_bans(self):
11181160

11191161
def set_permissions(self, user: User, *, mod: User, **perms):
11201162
"""
1121-
Grants or removes read, write, and/or upload permissions to the given user in this room.
1122-
`mod` must have moderator access in the room.
1163+
Grants or removes read, accessible, write, and/or upload permissions to the given user in
1164+
this room. `mod` must have moderator access in the room.
11231165
1124-
Permitted keyword args are: read, write, upload. Each can be set to True, False, or None to
1125-
apply an explicit grant, explicit revocation, or return to room defaults, respectively.
1126-
(That is, None removes the override, if currently present, so that the user permission will
1127-
use the room default; the others set this user's permission to allowed/disallowed).
1166+
Permitted keyword args are: read, accessible, write, upload. Each can be set to True,
1167+
False, or None to apply an explicit grant, explicit revocation, or return to room defaults,
1168+
respectively. (That is, None removes the override, if currently present, so that the user
1169+
permission will use the room default; the others set this user's permission to
1170+
allowed/disallowed).
11281171
11291172
If a permission key is omitted then it will not be changed at all if it already exists, and
11301173
will be NULL if a new permission row is being created.
11311174
"""
11321175

1133-
perm_types = ('read', 'write', 'upload')
1176+
perm_types = ('read', 'accessible', 'write', 'upload')
11341177

11351178
if any(k not in perm_types for k in perms.keys()):
11361179
raise ValueError(f"Room.set_permissions: only {', '.join(perm_types)} may be specified")
@@ -1156,6 +1199,7 @@ def set_permissions(self, user: User, *, mod: User, **perms):
11561199
r=self.id,
11571200
u=user.id,
11581201
read=perms.get('read'),
1202+
accessible=perms.get('accessible'),
11591203
write=perms.get('write'),
11601204
upload=perms.get('upload'),
11611205
)
@@ -1348,6 +1392,7 @@ def get_rooms_with_permission(
13481392
*,
13491393
tokens: Optional[Union[list, tuple]] = None,
13501394
read: Optional[bool] = None,
1395+
accessible: Optional[bool] = None,
13511396
write: Optional[bool] = None,
13521397
upload: Optional[bool] = None,
13531398
banned: Optional[bool] = None,
@@ -1363,7 +1408,7 @@ def get_rooms_with_permission(
13631408
omitted, all rooms are returned. Note that rooms are returned sorted by token, *not* in
13641409
the order specified here; duplicates are not returned; nor are entries for non-existent
13651410
tokens.
1366-
read/write/upload/banned/moderator/admin:
1411+
read/accessible/write/upload/banned/moderator/admin:
13671412
Any of these that are specified as non-None must match the user's permissions for the room.
13681413
For example `read=True, write=False` would return all rooms where the user has read-only
13691414
access but not rooms in which the user has both or neither read and write permissions.
@@ -1387,6 +1432,8 @@ def get_rooms_with_permission(
13871432
WHERE "user" = :u {'AND token IN :tokens' if tokens else ''}
13881433
{'' if banned is None else ('AND' if banned else 'AND NOT') + ' perm.banned'}
13891434
{'' if read is None else ('AND' if read else 'AND NOT') + ' perm.read'}
1435+
{'' if accessible is None else ('AND' if accessible else 'AND NOT') +
1436+
' (perm.read OR perm.accessible)'}
13901437
{'' if write is None else ('AND' if write else 'AND NOT') + ' perm.write'}
13911438
{'' if upload is None else ('AND' if upload else 'AND NOT') + ' perm.upload'}
13921439
{'' if moderator is None else ('AND' if moderator else 'AND NOT') + ' perm.moderator'}
@@ -1400,15 +1447,15 @@ def get_rooms_with_permission(
14001447
]
14011448

14021449

1403-
def get_readable_rooms(user: Optional[User] = None):
1450+
def get_accessible_rooms(user: Optional[User] = None):
14041451
"""
1405-
Get a list of rooms that a user can access; if user is None then return all publicly readable
1452+
Get a list of rooms that a user can access; if user is None then return all publicly accessible
14061453
rooms.
14071454
"""
14081455
if user is None:
1409-
result = query("SELECT * FROM rooms WHERE read ORDER BY token")
1456+
result = query("SELECT * FROM rooms WHERE (read OR accessible) ORDER BY token")
14101457
else:
1411-
return get_rooms_with_permission(user, read=True, banned=False)
1458+
return get_rooms_with_permission(user, accessible=True, banned=False)
14121459
return [Room(row) for row in result]
14131460

14141461

0 commit comments

Comments
 (0)