-
Notifications
You must be signed in to change notification settings - Fork 2
/
rules.go
113 lines (102 loc) · 4.31 KB
/
rules.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
package main
import (
"context"
"fmt"
"regexp"
"strings"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
)
// AllowedLocalpartRegex is the regex matching localparts of users who are allowed to use the cleanup service.
// The default regex here matches mautrix-asmux bridge bots and the idea is to call the service with the as_token.
var AllowedLocalpartRegex = regexp.MustCompile("^_([a-z0-9-]+)_([a-z0-9-]+)_bot$")
// IsAllowedToUseService checks if the given user can use this cleanup service.
func IsAllowedToUseService(ctx context.Context, client *mautrix.Client, whoami *mautrix.RespWhoami) error {
client.UserID = whoami.UserID
localpart, _, err := client.UserID.Parse()
if err != nil {
return fmt.Errorf("failed to parse user ID: %w", err)
} else if !AllowedLocalpartRegex.MatchString(localpart) {
return fmt.Errorf("only bridge bots can clean up rooms")
}
return nil
}
func parseBridgeName(userID id.UserID) (bridgeUserLocalpart, bridgeName, homeserver string, err error) {
var botLocalpart string
// Parsing and the allowed localpart check should never fail at this point since
// they're also checked in IsAllowedToUseService, but handle them just in case anyway.
if botLocalpart, homeserver, err = userID.Parse(); err != nil {
err = fmt.Errorf("failed to parse user ID: %w", err)
} else if parts := AllowedLocalpartRegex.FindStringSubmatch(botLocalpart); len(parts) != 3 {
err = fmt.Errorf("didn't get expected number of parts from parsing user ID localpart")
} else {
bridgeUserLocalpart = parts[1]
bridgeName = parts[2]
}
return
}
// IsAllowedToCleanRoom checks if the given client has sufficient permissions in the room to include it in the cleanup.
//
// It returns the list of user IDs that should be kicked right away.
func IsAllowedToCleanRoom(ctx context.Context, client *mautrix.Client, roomID id.RoomID) ([]id.UserID, error) {
bridgeUserLocalpart, bridgeName, homeserver, err := parseBridgeName(client.UserID)
if err != nil {
return nil, err
}
// The localpart prefix for ghost users managed by the bridge.
bridgeGhostPrefix := fmt.Sprintf("_%s_%s_", bridgeUserLocalpart, bridgeName)
var randomBridgeGhostInRoom id.UserID
members, err := adminListRoomMembers(ctx, roomID)
if err != nil {
return nil, fmt.Errorf("failed to get members of %s: %w", roomID, err)
}
var usersToKick []id.UserID
// Make sure the room doesn't contain anyone except the user of the bridge, the bridge bot and bridge ghosts.
for _, member := range members {
memberLocalpart, memberHomeserver, _ := member.Parse()
if memberHomeserver != homeserver {
return nil, fmt.Errorf("room contains member '%s' from other homeserver '%s' (expected '%s')", member, memberHomeserver, homeserver)
} else if memberLocalpart == bridgeUserLocalpart {
// Found the bridge user, so schedule that user to be kicked from the room.
usersToKick = append(usersToKick, member)
} else if strings.HasPrefix(memberLocalpart, bridgeGhostPrefix) {
randomBridgeGhostInRoom = member
} else {
return nil, fmt.Errorf("room contains member '%s' that is not the bridge user nor a bridge ghost (expected '%s' or prefix '%s')", member, bridgeUserLocalpart, bridgeGhostPrefix)
}
}
// Copy the client and set AppServiceUserID to sure the power level request
// is always done by a user in the room.
appserviceClient := &mautrix.Client{
AppServiceUserID: randomBridgeGhostInRoom,
AccessToken: client.AccessToken,
UserAgent: client.UserAgent,
HomeserverURL: client.HomeserverURL,
UserID: client.UserID,
Client: client.Client,
Prefix: client.Prefix,
Store: client.Store,
Logger: client.Logger,
}
var pl event.PowerLevelsEventContent
err = appserviceClient.StateEvent(roomID, event.StatePowerLevels, "", &pl)
if err != nil {
return nil, fmt.Errorf("failed to get power levels of %s: %w", roomID, err)
}
// Make sure that the bridge bot or at least one bridged user has PL 100.
if pl.GetUserLevel(client.UserID) < 100 {
found := false
for userID, level := range pl.Users {
if level >= 100 && strings.HasPrefix(userID.String(), "@"+bridgeGhostPrefix) {
found = true
break
}
}
if !found {
return nil, fmt.Errorf("room doesn't have any bridge user with admin power level")
}
}
// All good, room is safe to delete.
return usersToKick, nil
}