From f7e2d263339d9883d134562624d9118159824f8c Mon Sep 17 00:00:00 2001 From: "James E. Blair" Date: Wed, 5 Mar 2014 16:46:44 -0800 Subject: [PATCH] Add a script to manage IRC perms Run it whenever there is a change to the YAML channel config. The script will ensure everyone listed in global has those perms and anyone else found with access on a channel will be left as-is except that their access will be limited to the relevant mask. Move it and the previous change to add a permission checking script into a new module, 'accessbot'. Support SSL in both scripts. Add a 1 second sleep in the check script to avoid flood protection. Add all known channels to the channel config. Closes-Bug: 1190296 Change-Id: I5072cb56ae83a70f4fa955362b8db909b2956d70 --- files/accessbot.py | 214 +++++++++++++++++++++++++++++++++++ files/checkaccess.py | 154 +++++++++++++++++++++++++ manifests/init.pp | 74 ++++++++++++ templates/accessbot.conf.erb | 5 + 4 files changed, 447 insertions(+) create mode 100755 files/accessbot.py create mode 100755 files/checkaccess.py create mode 100644 manifests/init.pp create mode 100644 templates/accessbot.conf.erb diff --git a/files/accessbot.py b/files/accessbot.py new file mode 100755 index 0000000..6f657a5 --- /dev/null +++ b/files/accessbot.py @@ -0,0 +1,214 @@ +#! /usr/bin/env python + +# Copyright 2011, 2013-2014 OpenStack Foundation +# Copyright 2012 Hewlett-Packard Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import ConfigParser +import argparse +import irc.client +import logging +import ssl +import sys +import time +import yaml + +logging.basicConfig(level=logging.DEBUG) + + +class SetAccess(irc.client.SimpleIRCClient): + log = logging.getLogger("setaccess") + + 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 + self.server = server + self.port = int(port) + self.noop = noop + self.channels = [x['name'] for x in self.config['channels']] + self.current_channel = None + self.current_list = [] + self.changes = [] + self.identified = False + if self.port == 6697: + factory = irc.connection.Factory(wrapper=ssl.wrap_socket) + self.connect(self.server, self.port, self.nick, + connect_factory=factory) + else: + self.connect(self.server, self.port, self.nick) + + 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'): + self.identified = True + self.advance() + return + if auth != '+' or nick != 'ChanServ': + self.log.debug("Ignoring message from unauthenticated " + "user %s" % nick) + return + self.failed = False + self.advance(msg) + + def _get_access_list(self, channel_name): + ret = {} + 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 (self.config['global'].items() + + channel.items()): + if access == 'mask': + mask = self.config['access'].get(nicks) + continue + flags = self.config['access'].get(access) + if flags is None: + continue + for nick in nicks: + ret[nick] = flags + return mask, ret + + def _get_access_change(self, current, target, mask): + remove = '' + add = '' + change = '' + for x in current: + if x in '+-': + 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 + + def _get_access_changes(self): + mask, target = 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)) + all_nicks = set() + current = {} + changes = [] + for nick, flags, msg in self.current_list: + all_nicks.add(nick) + current[nick] = flags + 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) + if change: + changes.append('access #%s add %s %s' % (self.current_channel, + nick, change)) + return changes + + def advance(self, msg=None): + if self.changes: + if self.noop: + for change in self.changes: + self.log.info('NOOP: ' + change) + self.changes = [] + else: + change = self.changes.pop() + self.log.info(change) + self.connection.privmsg('chanserv', change) + time.sleep(1) + return + if not self.current_channel: + if not self.channels: + self.connection.quit() + return + self.current_channel = self.channels.pop() + self.current_list = [] + self.connection.privmsg('chanserv', 'access list #%s' % + self.current_channel) + time.sleep(1) + return + if 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(): + parser = argparse.ArgumentParser(description='IRC channel access check') + parser.add_argument('-c', dest='config', nargs=1, + help='specify the config file') + parser.add_argument('-l', dest='channels', + default='/etc/irc/channels.yaml', + help='path to the channel config') + parser.add_argument('--noop', dest='noop', + action='store_true', + help="Don't make any changes") + args = parser.parse_args() + + config = ConfigParser.ConfigParser() + config.read(args.config) + + channels = yaml.load(open(args.channels)) + + a = SetAccess(channels, args.noop, + config.get('ircbot', 'nick'), + config.get('ircbot', 'pass'), + config.get('ircbot', 'server'), + config.get('ircbot', 'port')) + a.start() + + +if __name__ == "__main__": + main() diff --git a/files/checkaccess.py b/files/checkaccess.py new file mode 100755 index 0000000..4038404 --- /dev/null +++ b/files/checkaccess.py @@ -0,0 +1,154 @@ +#! /usr/bin/env python + +# Copyright 2011, 2013-2014 OpenStack Foundation +# Copyright 2012 Hewlett-Packard Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +import irc.client +import logging +import random +import string +import ssl +import sys +import time +import yaml + + +logging.basicConfig(level=logging.INFO) + + +class CheckAccess(irc.client.SimpleIRCClient): + log = logging.getLogger("checkaccess") + + def __init__(self, channels, nick, flags): + irc.client.SimpleIRCClient.__init__(self) + self.identify_msg_cap = False + self.channels = channels + self.nick = nick + self.flags = flags + self.current_channel = None + self.current_list = [] + self.failed = True + + def on_disconnect(self, connection, event): + if self.failed: + sys.exit(1) + else: + 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.advance() + + 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 != '+' or nick != 'ChanServ': + self.log.debug("Ignoring message from unauthenticated " + "user %s" % nick) + return + self.failed = False + self.advance(msg) + + def advance(self, msg=None): + if not self.current_channel: + if not self.channels: + self.connection.quit() + return + self.current_channel = self.channels.pop() + self.current_list = [] + self.connection.privmsg('chanserv', 'access list %s' % + self.current_channel) + time.sleep(1) + return + if msg.startswith('End of'): + found = False + for nick, flags, msg in self.current_list: + if nick == self.nick and flags == self.flags: + self.log.info('%s access ok on %s' % + (self.nick, self.current_channel)) + found = True + break + if not found: + self.failed = True + print ("%s does not have permissions on %s:" % + (self.nick, self.current_channel)) + for nick, flags, msg in self.current_list: + print msg + print + self.current_channel = None + self.advance() + return + parts = msg.split() + self.current_list.append((parts[1], parts[2], msg)) + + +def main(): + parser = argparse.ArgumentParser(description='IRC channel access check') + parser.add_argument('-l', dest='config', + default='/etc/accessbot/channels.yaml', + help='path to the config file') + parser.add_argument('-s', dest='server', + default='chat.freenode.net', + help='IRC server') + parser.add_argument('-p', dest='port', + default=6697, + help='IRC port') + parser.add_argument('nick', + help='the nick for which access should be validated') + args = parser.parse_args() + + config = yaml.load(open(args.config)) + channels = [] + for channel in config['channels']: + channels.append('#' + channel['name']) + + access_level = None + for level, names in config['global'].items(): + if args.nick in names: + access_level = level + if access_level is None: + raise Exception("Unable to determine global access level for %s" % + args.nick) + flags = config['access'][access_level] + + a = CheckAccess(channels, args.nick, flags) + mynick = ''.join(random.choice(string.ascii_uppercase) + for x in range(16)) + port = int(args.port) + if port == 6697: + factory = irc.connection.Factory(wrapper=ssl.wrap_socket) + a.connect(args.server, int(args.port), mynick, + connect_factory=factory) + else: + a.connect(args.server, int(args.port), mynick) + a.start() + +if __name__ == "__main__": + main() diff --git a/manifests/init.pp b/manifests/init.pp new file mode 100644 index 0000000..e49cf8b --- /dev/null +++ b/manifests/init.pp @@ -0,0 +1,74 @@ +# == Class: accessbot +# +class accessbot( + $nick = '', + $password = '', + $server = '', + $channel_file = '', +) { + + user { 'accessbot': + ensure => present, + home => '/home/accessbot', + shell => '/bin/bash', + gid => 'accessbot', + managehome => true, + require => Group['accessbot'], + } + + group { 'accessbot': + ensure => present, + } + + exec { 'run_accessbot' : + command => '/usr/local/bin/accessbot -c /etc/accessbot/accessbot.config -l /etc/accessbot/channels.yaml >> /var/log/accessbot/accessbot.log 2>&1', + path => '/usr/local/bin:/usr/bin:/bin/', + refreshonly => true, + subscribe => File['/etc/accessbot/channels.yaml'], + require => [File['/etc/accessbot/channels.yaml'], + File['/etc/accessbot/accessbot.config'], + File['/usr/local/bin/accessbot']], + } + + file { '/etc/accessbot': + ensure => directory, + } + + file { '/var/log/accessbot': + ensure => directory, + owner => 'accessbot', + group => 'accessbot', + mode => '0775', + require => User['accessbot'], + } + + file { '/etc/accessbot/accessbot.config': + ensure => present, + content => template('accessbot/accessbot.config.erb'), + group => 'accessbot', + mode => '0440', + owner => 'root', + replace => true, + require => User['accessbot'], + } + + file { '/etc/accessbot/channels.yaml': + ensure => present, + source => $channel_file, + group => 'accessbot', + mode => '0440', + owner => 'root', + replace => true, + require => User['accessbot'], + } + + file { '/usr/local/bin/accessbot': + ensure => present, + source => 'puppet:///modules/accessbot/files/accessbot.py', + group => 'accessbot', + mode => '0440', + owner => 'root', + replace => true, + require => User['accessbot'], + } +} diff --git a/templates/accessbot.conf.erb b/templates/accessbot.conf.erb new file mode 100644 index 0000000..a11ea48 --- /dev/null +++ b/templates/accessbot.conf.erb @@ -0,0 +1,5 @@ +[ircbot] +nick=<%= nick %> +pass=<%= password %> +server=<%= server %> +port=6697