Skip to content

Commit 8ed87d1

Browse files
authored
Merge pull request #47 from jagerman/bans
Bans, unbans
2 parents 892d35f + 7315f6d commit 8ed87d1

File tree

12 files changed

+658
-130
lines changed

12 files changed

+658
-130
lines changed

api.yaml

Lines changed: 86 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -759,9 +759,14 @@ paths:
759759
/user/{sessionId}/ban:
760760
post:
761761
tags: [Users]
762-
summary: Bans or unbans a user.
762+
summary: Bans a user.
763763
description: >
764-
Applies or removes a ban of a user from specific rooms, or from the server globally.
764+
Applies a ban of a user from specific rooms, or from the server globally.
765+
766+
767+
The invoking user must have moderator (or admin) permission in all given rooms when
768+
specifying `rooms`, and must be a global server moderator (or admin) if using the `global`
769+
parameter.
765770
766771
767772
Note that the given session ID does not have to exist: it is possible to preemptively ban
@@ -774,7 +779,7 @@ paths:
774779
parameters:
775780
- $ref: "#/components/parameters/pathSessionId"
776781
requestBody:
777-
description: Details of the ban to apply. To unban a user, specify a negative timeout.
782+
description: Details of the ban to apply.
778783
required: true
779784
content:
780785
application/json:
@@ -791,6 +796,12 @@ paths:
791796
be a moderator (or admin) of all of the given rooms.
792797
793798
799+
You can specify a single element list `["*"]` to ban the user from all rooms on
800+
the server to which the calling user has moderator permissions. This differs
801+
from a global ban in that it doesn't apply to non-moderator-permission rooms,
802+
nor does it apply to newly created rooms or non-room endpoints.
803+
804+
794805
Exclusive of `global`.
795806
global:
796807
type: boolean
@@ -799,22 +810,20 @@ paths:
799810
user must be a server-level moderator or admin.
800811
801812
813+
The ban applies immediately to all server requests initiated by the user (not
814+
just room access).
815+
816+
802817
Exclusive of `rooms`.
803818
timeout:
804819
type: number
805820
format: double
806821
nullable: true
807822
example: 86400
808823
description: >
809-
How long the ban should apply, in seconds. If there is an existing ban on the
810-
user in the given rooms or globally this updates the existing expiry to the
811-
given value. If omitted or `null` the ban does not expire.
812-
813-
If this value is set to a negative value (`-1` is suggested) then any existing
814-
bans for this user are *removed* from the given rooms/server. Note, however,
815-
that server bans and room bans are independent: removing a server-level ban does
816-
not remove room-specific bans, and removing a room-level ban will not grant room
817-
access to a user who also has a server-level ban.
824+
How long the ban should apply, in seconds. If omitted (or `null`) then the ban
825+
applies forever. If an unban was previously scheduled then it is replaced (if a
826+
timeout is given) or deleted (if no timeout is provided).
818827
examples:
819828
tworooms:
820829
summary: "1-day ban from two rooms"
@@ -825,21 +834,77 @@ paths:
825834
summary: "Permanent server ban"
826835
value:
827836
global: true
828-
timeout: null
829-
delete_all: true
830-
unban:
831-
summary: "Unban a user from a room"
832-
value:
833-
rooms: ["lokinet"]
834-
global: false,
835-
timeout: -1
836837
responses:
837838
200:
838839
description: Ban applied successfully.
839840
content: {}
840841
403:
841842
description: >
842-
Permission denied. The user attempting to set the ban does not have moderator
843+
Permission denied. The user attempting to set the ban does not have the required
844+
moderator permissions for one or more of the given rooms (or server moderator permission
845+
for a global ban).
846+
content: {}
847+
/user/{sessionId}/unban:
848+
post:
849+
tags: [Users]
850+
summary: Removes a user ban.
851+
description: >
852+
Removes a room-specific or global ban of a user.
853+
854+
855+
Note that removing a room-specific ban does not affect an existing global ban, and removing
856+
a global ban does not affect existing room-specific bans.
857+
parameters:
858+
- $ref: "#/components/parameters/pathSessionId"
859+
requestBody:
860+
description: Details of the ban to remove.
861+
required: true
862+
content:
863+
application/json:
864+
schema:
865+
type: object
866+
properties:
867+
rooms:
868+
type: array
869+
items:
870+
$ref: "#/components/schemas/RoomToken"
871+
minItems: 1
872+
description: >
873+
List of room tokens from which the ban should be removed (if present). The
874+
invoking user must be a moderator (or admin) of all of the given rooms.
875+
876+
877+
You can specify a single element list `["*"]` to unban the user from all rooms
878+
on the server to which the calling user has moderator permissions. This isn't
879+
the same as removing a global ban; this just removes any room-specific bans that
880+
may currently apply from moderated rooms.
881+
882+
883+
Exclusive of `global`.
884+
global:
885+
type: boolean
886+
description: >
887+
If true then remove a global server ban of this user. The invoking user must be
888+
a server-level moderator or admin.
889+
890+
891+
Exclusive of `rooms`.
892+
examples:
893+
tworooms:
894+
summary: "Remove ban from two rooms"
895+
value:
896+
rooms: ["session", "lokinet"]
897+
permaban:
898+
summary: "Remove server ban"
899+
value:
900+
global: true
901+
responses:
902+
200:
903+
description: Ban removed successfully.
904+
content: {}
905+
403:
906+
description: >
907+
Permission denied. The user attempting to remove the ban does not have moderator
843908
permissions for one or more of the given rooms (or server moderator permission for a
844909
global ban).
845910
content: {}

sogs/cleanup.py

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -94,24 +94,53 @@ def expire_nonce_history():
9494
def apply_permission_updates():
9595
with db.transaction():
9696
now = time.time()
97-
num_applied = query(
97+
num_perms = query(
9898
"""
99-
INSERT INTO user_permission_overrides (room, "user", read, write, upload, banned)
100-
SELECT room, "user", read, write, upload, banned FROM user_permission_futures
99+
INSERT INTO user_permission_overrides (room, "user", read, write, upload)
100+
SELECT room, "user", read, write, upload
101+
FROM user_permission_futures
101102
WHERE at <= :now
103+
ORDER BY at
102104
ON CONFLICT (room, "user") DO UPDATE SET
103105
read = COALESCE(excluded.read, user_permission_overrides.read),
104106
write = COALESCE(excluded.write, user_permission_overrides.write),
105-
upload = COALESCE(excluded.upload, user_permission_overrides.upload),
106-
banned = COALESCE(excluded.banned, user_permission_overrides.banned)
107+
upload = COALESCE(excluded.upload, user_permission_overrides.upload)
107108
""",
108109
now=now,
109110
).rowcount
110-
if not num_applied:
111-
return 0
112111

113-
query("DELETE FROM user_permission_futures WHERE at <= :now", now=now)
112+
if num_perms > 0:
113+
query("DELETE FROM user_permission_futures WHERE at <= :now", now=now)
114+
115+
num_room_bans, num_user_bans = 0, 0
116+
for uid, rid, banned in query(
117+
'SELECT "user", room, banned FROM user_ban_futures WHERE at <= :now ORDER BY at',
118+
now=now,
119+
):
120+
if rid is None:
121+
query("UPDATE users SET banned = :b WHERE id = :u", u=uid, b=banned)
122+
num_user_bans += 1
123+
else:
124+
query(
125+
"""
126+
INSERT INTO user_permission_overrides (room, "user", banned)
127+
VALUES (:r, :u, :b)
128+
ON CONFLICT (room, "user") DO UPDATE SET banned = excluded.banned
129+
""",
130+
r=rid,
131+
u=uid,
132+
b=banned,
133+
)
134+
num_room_bans += 1
135+
136+
if num_room_bans > 0 or num_user_bans > 0:
137+
query("DELETE FROM user_ban_futures WHERE at <= :now", now=now)
138+
139+
num_applied = num_perms + num_room_bans + num_user_bans
114140

115141
if num_applied > 0:
116-
app.logger.info("Applied {} scheduled user permission updates".format(num_applied))
142+
app.logger.info(
143+
f"Applied {num_applied} permission updates ({num_perms} perms; "
144+
f"{num_room_bans} room (un)bans; {num_user_bans} global (un)bans)"
145+
)
117146
return num_applied

sogs/db.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ def database_init():
128128
create_message_details_deleter,
129129
check_for_hacks,
130130
seqno_etc_updates,
131+
user_perm_future_updates,
131132
):
132133
with transaction(conn):
133134
if migrate(conn):
@@ -479,6 +480,76 @@ def seqno_etc_updates(conn):
479480
return True
480481

481482

483+
def user_perm_future_updates(conn):
484+
"""
485+
Break up user_permission_futures to not be (room,user) unique, and to move ban futures to a
486+
separate table.
487+
"""
488+
489+
if 'user_ban_futures' in metadata.tables:
490+
return False
491+
492+
logging.warning("Updating user_permission_futures")
493+
494+
if engine.name == 'sqlite':
495+
# Under sqlite we have to drop and recreate the whole thing. (Since we didn't have a
496+
# release out that was using futures yet, we don't bother trying to migrate data).
497+
conn.execute("DROP TABLE user_permission_futures")
498+
conn.execute(
499+
"""
500+
CREATE TABLE user_permission_futures (
501+
room INTEGER NOT NULL REFERENCES rooms ON DELETE CASCADE,
502+
user INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
503+
at FLOAT NOT NULL, /* when the change should take effect (unix epoch) */
504+
read BOOLEAN, /* Set this value @ at, if non-null */
505+
write BOOLEAN, /* Set this value @ at, if non-null */
506+
upload BOOLEAN /* Set this value @ at, if non-null */
507+
)
508+
"""
509+
)
510+
conn.execute("CREATE INDEX user_permission_futures_at ON user_permission_futures(at)")
511+
conn.execute(
512+
"""
513+
CREATE INDEX user_permission_futures_room_user ON user_permission_futures(room, user)
514+
"""
515+
)
516+
517+
conn.execute(
518+
"""
519+
CREATE TABLE user_ban_futures (
520+
room INTEGER REFERENCES rooms ON DELETE CASCADE,
521+
user INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
522+
at FLOAT NOT NULL, /* when the change should take effect (unix epoch) */
523+
banned BOOLEAN NOT NULL /* if true then ban at `at`, if false then unban */
524+
);
525+
"""
526+
)
527+
conn.execute("CREATE INDEX user_ban_futures_at ON user_ban_futures(at)")
528+
conn.execute("CREATE INDEX user_ban_futures_room_user ON user_ban_futures(room, user)")
529+
530+
else: # postgresql
531+
conn.execute(
532+
"""
533+
CREATE TABLE user_ban_futures (
534+
room INTEGER NOT NULL REFERENCES rooms ON DELETE CASCADE,
535+
"user" INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
536+
at FLOAT NOT NULL, /* when the change should take effect (unix epoch) */
537+
banned BOOLEAN NOT NULL /* if true then ban at `at`, if false then unban */
538+
);
539+
CREATE INDEX user_ban_futures_at ON user_ban_futures(at);
540+
CREATE INDEX user_ban_futures_room_user ON user_ban_futures(room, "user");
541+
542+
INSERT INTO user_ban_futures (room, "user", at, banned)
543+
SELECT room, "user", at, banned FROM user_permission_futures WHERE banned is NOT NULL;
544+
545+
ALTER TABLE user_permission_futures DROP PRIMARY KEY;
546+
ALTER TABLE user_permission_futures DROP COLUMN banned;
547+
"""
548+
)
549+
550+
return True
551+
552+
482553
def create_admin_user(dbconn):
483554
"""
484555
We create a dummy user (with id 0) for system tasks such as changing moderators from

sogs/model/room.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1040,28 +1040,36 @@ def ban_user(self, to_ban: User, *, mod: User, timeout: Optional[float] = None):
10401040
"""
10411041
INSERT INTO user_permission_overrides (room, "user", banned, moderator, admin)
10421042
VALUES (:r, :ban, TRUE, FALSE, FALSE)
1043-
ON CONFLICT (room, user) DO
1043+
ON CONFLICT (room, "user") DO
10441044
UPDATE SET banned = TRUE, moderator = FALSE, admin = FALSE
10451045
""",
10461046
r=self.id,
10471047
ban=to_ban.id,
10481048
)
10491049

1050+
# Replace (or remove) an existing scheduled unban:
1051+
query(
1052+
'DELETE FROM user_ban_futures WHERE room = :r AND "user" = :u AND NOT banned',
1053+
r=self.id,
1054+
u=to_ban.id,
1055+
)
10501056
if timeout:
10511057
query(
10521058
"""
1053-
INSERT INTO user_permission_futures (room, "user", at, banned)
1054-
VALUES (:r, :banned, :at, FALSE)
1059+
INSERT INTO user_ban_futures
1060+
(room, "user", banned, at) VALUES (:r, :u, FALSE, :at)
10551061
""",
10561062
r=self.id,
1057-
banned=to_ban.id,
1063+
u=to_ban.id,
10581064
at=time.time() + timeout,
10591065
)
10601066

10611067
if to_ban.id in self._perm_cache:
10621068
del self._perm_cache[to_ban.id]
10631069

1064-
app.logger.debug(f"Banned {to_ban} from {self} (banned by {mod})")
1070+
app.logger.debug(
1071+
f"Banned {to_ban} from {self} {f'for {timeout}s ' if timeout else ''}(banned by {mod})"
1072+
)
10651073

10661074
def unban_user(self, to_unban: User, *, mod: User):
10671075
"""

0 commit comments

Comments
 (0)