Retool accessbot for OFTC
A number of changes are needed to fit accessbot to OFTC's RBAC-style permissions model and services syntax expectations. Most importantly, access list entries now use role names for graduated access tiers (member, chanop, master) rather than fine-grained option flags. In order to avoid future confusion, switch variable names and configuration keys to reflect that these are access levels rather than masks. While we're at it, skip setting the channel mlock if the result would be a no-op, so that we don't unnecessarily spam the ircd with pointless writes. Also add a bunch of inline comments so I can more easily remember the subtle nuances I spent a lot of time figuring out. Change-Id: Id11598fc42672359e1abef7b70cc23100b16ab12 Depends-on: https://review.opendev.org/792843
This commit is contained in:
parent
88139ef622
commit
258e8e8585
@ -34,7 +34,6 @@ class SetAccess(irc.client.SimpleIRCClient):
|
||||
|
||||
def __init__(self, config, noop, nick, password, server, port):
|
||||
irc.client.SimpleIRCClient.__init__(self)
|
||||
self.identify_msg_cap = False
|
||||
self.config = config
|
||||
self.nick = nick
|
||||
self.password = password
|
||||
@ -44,6 +43,7 @@ class SetAccess(irc.client.SimpleIRCClient):
|
||||
self.channels = [x['name'] for x in self.config['channels']]
|
||||
self.current_channel = None
|
||||
self.current_list = []
|
||||
self.current_mode = ''
|
||||
self.changes = []
|
||||
self.identified = False
|
||||
if self.port == 6697:
|
||||
@ -56,30 +56,19 @@ class SetAccess(irc.client.SimpleIRCClient):
|
||||
def on_disconnect(self, connection, event):
|
||||
sys.exit(0)
|
||||
|
||||
def on_welcome(self, c, e):
|
||||
self.identify_msg_cap = False
|
||||
self.log.debug("Requesting identify-msg capability")
|
||||
c.cap('REQ', 'identify-msg')
|
||||
c.cap('END')
|
||||
|
||||
def on_cap(self, c, e):
|
||||
self.log.debug("Received cap response %s" % repr(e.arguments))
|
||||
if e.arguments[0] == 'ACK' and 'identify-msg' in e.arguments[1]:
|
||||
self.log.debug("identify-msg cap acked")
|
||||
self.identify_msg_cap = True
|
||||
self.log.debug("Identifying to nickserv")
|
||||
c.privmsg("nickserv", "identify %s " % self.password)
|
||||
|
||||
def on_privnotice(self, c, e):
|
||||
if not self.identify_msg_cap:
|
||||
self.log.debug("Ignoring message because identify-msg "
|
||||
"cap not enabled")
|
||||
return
|
||||
nick = e.source.split('!')[0]
|
||||
auth = e.arguments[0][0]
|
||||
msg = e.arguments[0][1:]
|
||||
if auth == '+' and nick == 'NickServ' and not self.identified:
|
||||
if msg.startswith('You are now identified'):
|
||||
msg = e.arguments[0]
|
||||
if nick == 'NickServ' and not self.identified:
|
||||
if msg.startswith('authenticate yourself to services'):
|
||||
self.log.debug("Identifying to nickserv")
|
||||
# TODO (fungi): We should protect against sending our
|
||||
# password to a false NickServ, perhaps with
|
||||
# https://www.oftc.net/NickServ/CertFP/ or eventually
|
||||
# SASL once the ircd implements that
|
||||
c.privmsg("nickserv", "identify %s " % self.password)
|
||||
return
|
||||
elif msg.startswith('You are successfully identified'):
|
||||
self.identified = True
|
||||
# Prejoin and set ourselves as op in these channels,
|
||||
# to facilitate +f forwarding.
|
||||
@ -88,8 +77,10 @@ class SetAccess(irc.client.SimpleIRCClient):
|
||||
c.privmsg("chanserv", "op #%s" % channel)
|
||||
self.advance()
|
||||
return
|
||||
if auth != '+' or nick != 'ChanServ':
|
||||
self.log.debug("Ignoring message from unauthenticated "
|
||||
else:
|
||||
return
|
||||
if nick not in ('ChanServ', 'NickServ'):
|
||||
self.log.debug("Ignoring message from non-ChanServ "
|
||||
"user %s" % nick)
|
||||
return
|
||||
self.failed = False
|
||||
@ -99,95 +90,78 @@ class SetAccess(irc.client.SimpleIRCClient):
|
||||
ret = {}
|
||||
alumni = []
|
||||
mode = ''
|
||||
level = ''
|
||||
channel = None
|
||||
for c in self.config['channels']:
|
||||
if c['name'] == channel_name:
|
||||
channel = c
|
||||
if channel is None:
|
||||
raise Exception("Unknown channel %s" % (channel_name,))
|
||||
mask = ''
|
||||
for access, nicks in (list(self.config['global'].items()) +
|
||||
for key, value in (list(self.config['global'].items()) +
|
||||
list(channel.items())):
|
||||
if access == 'mask':
|
||||
mask = self.config['access'].get(nicks)
|
||||
if key == 'alumni':
|
||||
alumni += value
|
||||
continue
|
||||
if access == 'alumni':
|
||||
alumni += nicks
|
||||
if key == 'mode':
|
||||
mode = value
|
||||
continue
|
||||
if access == 'mode':
|
||||
mode = nicks
|
||||
continue
|
||||
flags = self.config['access'].get(access)
|
||||
if flags is None:
|
||||
continue
|
||||
for nick in nicks:
|
||||
ret[nick] = flags
|
||||
return mask, ret, alumni, mode
|
||||
|
||||
def _get_access_change(self, current, target, mask):
|
||||
remove = ''
|
||||
add = ''
|
||||
change = ''
|
||||
for x in current:
|
||||
if x in '+-':
|
||||
# If we get this far, we assume the key is an access
|
||||
# level matching an entry in the access list
|
||||
level = self.config['access'].get(key)
|
||||
if level is None:
|
||||
# Skip if this doesn't match a defined access level
|
||||
continue
|
||||
if target:
|
||||
if x not in target:
|
||||
remove += x
|
||||
else:
|
||||
if x not in mask:
|
||||
remove += x
|
||||
for x in target:
|
||||
if x in '+-':
|
||||
continue
|
||||
if x not in current:
|
||||
add += x
|
||||
if remove:
|
||||
change += '-' + remove
|
||||
if add:
|
||||
change += '+' + add
|
||||
return change
|
||||
for nick in value:
|
||||
ret[nick] = level
|
||||
return ret, alumni, mode
|
||||
|
||||
def _get_access_change(self, current, target):
|
||||
if current != target:
|
||||
return target
|
||||
|
||||
def _get_access_changes(self):
|
||||
mask, target, alumni, mode = self._get_access_list(self.current_channel)
|
||||
self.log.debug("Mask for %s: %s" % (self.current_channel, mask))
|
||||
self.log.debug("Target for %s: %s" % (self.current_channel, target))
|
||||
target, alumni, mode = self._get_access_list(
|
||||
self.current_channel)
|
||||
self.log.debug("Target #%s ACL: %s" % (self.current_channel, target))
|
||||
all_nicks = set()
|
||||
global_alumni = self.config.get('alumni', {})
|
||||
global_mode = self.config.get('mode', '')
|
||||
current = {}
|
||||
changes = []
|
||||
for nick, flags, msg in self.current_list:
|
||||
for nick, level, msg in self.current_list:
|
||||
if nick in global_alumni or nick in alumni :
|
||||
self.log.debug("%s is an alumni; removing access", nick)
|
||||
changes.append('access #%s del %s' % (self.current_channel, nick))
|
||||
continue
|
||||
all_nicks.add(nick)
|
||||
current[nick] = flags
|
||||
current[nick] = level
|
||||
for nick in target.keys():
|
||||
all_nicks.add(nick)
|
||||
for nick in all_nicks:
|
||||
change = self._get_access_change(current.get(nick, ''),
|
||||
target.get(nick, ''), mask)
|
||||
target.get(nick, ''))
|
||||
if change:
|
||||
changes.append('access #%s add %s %s' % (self.current_channel,
|
||||
nick, change))
|
||||
|
||||
# Set the mode. Note we always just hard-set the mode for
|
||||
# simplicity (per the man page mlock always clears and sets
|
||||
# anyway). Channel mode overrides global mode.
|
||||
#
|
||||
# Note for +f you need to be op in the target channel; see
|
||||
# op_channel option.
|
||||
# Set the mode if what we want differs from what's already there.
|
||||
# Channel mode overrides global mode.
|
||||
if not mode and global_mode:
|
||||
mode = global_mode
|
||||
self.log.debug("Setting mode to : %s" % mode)
|
||||
if mode:
|
||||
if not mode:
|
||||
mode = '+'
|
||||
if sorted(mode) != sorted(self.current_mode):
|
||||
self.log.debug("Current mode for #%s is %s, replacing with %s" % (
|
||||
self.current_channel, self.current_mode, mode))
|
||||
changes.append('set #%s mlock %s' % (self.current_channel, mode))
|
||||
|
||||
return changes
|
||||
|
||||
def advance(self, msg=None):
|
||||
# Some service responses include a number of embedded 0x02 bytes
|
||||
if msg:
|
||||
msg = msg.replace('\x02', '')
|
||||
if self.changes:
|
||||
if self.noop:
|
||||
for change in self.changes:
|
||||
@ -204,19 +178,41 @@ class SetAccess(irc.client.SimpleIRCClient):
|
||||
self.connection.quit()
|
||||
return
|
||||
self.current_channel = self.channels.pop()
|
||||
# Clear the mode string before we request it, so if we get
|
||||
# no response we won't have the modes from an earlier channel
|
||||
self.current_mode = ''
|
||||
# Sending a set mlock with no value prompts the service to
|
||||
# respond with the current mlock value so we can compare
|
||||
# against it later
|
||||
self.connection.privmsg('chanserv', 'set #%s mlock' %
|
||||
self.current_channel)
|
||||
# Clear the access list before we request it, so if we get
|
||||
# no response we won't have the list from an earlier channel
|
||||
self.current_list = []
|
||||
self.connection.privmsg('chanserv', 'access list #%s' %
|
||||
self.connection.privmsg('chanserv', 'access #%s list' %
|
||||
self.current_channel)
|
||||
time.sleep(1)
|
||||
return
|
||||
if msg.startswith('End of'):
|
||||
# We tokenize every server message, and perform some rough
|
||||
# heuristics in order to determine what kind of response we're
|
||||
# dealing with and whether it's something we know how to parse
|
||||
parts = msg.split()
|
||||
# If the third word look like an access level, assume this is
|
||||
# an access list entry and that the second word is a
|
||||
# corresponding nick
|
||||
if parts[2] in ('MASTER', 'CHANOP', 'MEMBER'):
|
||||
self.current_list.append((parts[1], parts[2], msg))
|
||||
# If the message starts with "MLOCK is SET to" then assume the
|
||||
# fifth word is the channel's mode string
|
||||
elif msg.startswith('MLOCK is SET to'):
|
||||
self.current_mode = parts[4]
|
||||
# If the message starts with "End of" then assume this marks
|
||||
# the end of an access list
|
||||
elif msg.startswith('End of'):
|
||||
self.changes = self._get_access_changes()
|
||||
self.current_channel = None
|
||||
self.advance()
|
||||
return
|
||||
parts = msg.split()
|
||||
if parts[2].startswith('+'):
|
||||
self.current_list.append((parts[1], parts[2], msg))
|
||||
|
||||
|
||||
def main():
|
||||
|
Loading…
Reference in New Issue
Block a user