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
This commit is contained in:
James E. Blair 2014-03-05 16:46:44 -08:00
parent becdd4b1eb
commit 9dad326acc
10 changed files with 436 additions and 50 deletions

View File

@ -279,6 +279,8 @@ node 'eavesdrop.openstack.org' {
statusbot_wiki_password => hiera('statusbot_wiki_password'), statusbot_wiki_password => hiera('statusbot_wiki_password'),
statusbot_wiki_url => 'https://wiki.openstack.org/w/api.php', statusbot_wiki_url => 'https://wiki.openstack.org/w/api.php',
statusbot_wiki_pageid => '1781', statusbot_wiki_pageid => '1781',
accessbot_nick => hiera('accessbot_nick'),
accessbot_password => hiera('accessbot_nick_password'),
} }
} }

View File

@ -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()

View File

@ -20,9 +20,12 @@ import irc.client
import logging import logging
import random import random
import string import string
import ssl
import sys import sys
import time
import yaml import yaml
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
@ -82,6 +85,7 @@ class CheckAccess(irc.client.SimpleIRCClient):
self.current_list = [] self.current_list = []
self.connection.privmsg('chanserv', 'access list %s' % self.connection.privmsg('chanserv', 'access list %s' %
self.current_channel) self.current_channel)
time.sleep(1)
return return
if msg.startswith('End of'): if msg.startswith('End of'):
found = False found = False
@ -108,13 +112,13 @@ class CheckAccess(irc.client.SimpleIRCClient):
def main(): def main():
parser = argparse.ArgumentParser(description='IRC channel access check') parser = argparse.ArgumentParser(description='IRC channel access check')
parser.add_argument('-l', dest='config', parser.add_argument('-l', dest='config',
default='/etc/irc/channels.yaml', default='/etc/accessbot/channels.yaml',
help='path to the config file') help='path to the config file')
parser.add_argument('-s', dest='server', parser.add_argument('-s', dest='server',
default='chat.freenode.net', default='chat.freenode.net',
help='IRC server') help='IRC server')
parser.add_argument('-p', dest='port', parser.add_argument('-p', dest='port',
default=6667, default=6697,
help='IRC port') help='IRC port')
parser.add_argument('nick', parser.add_argument('nick',
help='the nick for which access should be validated') help='the nick for which access should be validated')
@ -137,7 +141,13 @@ def main():
a = CheckAccess(channels, args.nick, flags) a = CheckAccess(channels, args.nick, flags)
mynick = ''.join(random.choice(string.ascii_uppercase) mynick = ''.join(random.choice(string.ascii_uppercase)
for x in range(16)) for x in range(16))
a.connect(args.server, int(args.port), mynick) 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() a.start()
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -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'],
}
}

View File

@ -0,0 +1,5 @@
[ircbot]
nick=<%= nick %>
pass=<%= password %>
server=<%= server %>
port=6697

View File

@ -0,0 +1,117 @@
# Copyright 2014 OpenStack Foundation
#
# 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.
# In general, to add a new channel for an official OpenStack project
# to this file, just add the name to the list in "channels" without
# anything else. Stackforge projects can optionally set "mask" to
# "full_mask" to keep full permissions.
# Global definitions
# First set up the access levels (map names in this file to chanserv flags):
access:
masters: +AFRfiorstv
topics: +t
meetbots: +O
operators: +Afortv
channel_op_mask: +AVOfortv
full_mask: +AFORVfiorstv
# Define access that should apply to all channels. The label 'mask'
# is special: anyone with perms on a channel that isn't otherwise
# listed for the channel or in the global list will have their access
# limited to the mask but otherwise left alone.
global:
masters:
- openstackinfra
operators:
- SergeyLukjanov
- clarkb
- fungi
- jeblair
- lifeless
- maffulli
- mtaylor
- ttx
topics:
- openstackstatus
mask:
channel_op_mask
# Individual channel configuration:
channels:
- name: edeploy
mask: full_mask
- name: fuel-dev
mask: full_mask
- name: heat
- name: magnetodb
mask: full_mask
- name: murano
mask: full_mask
- name: openstack
- name: openstack-101
- name: openstack-anvil
- name: openstack-bacon
- name: openstack-barbican
- name: openstack-board
- name: openstack-ceilometer
- name: openstack-chef
- name: openstack-cinder
- name: openstack-climate
- name: openstack-cloudkeep
- name: openstack-community
- name: openstack-dev
- name: openstack-dns
- name: openstack-doc
- name: openstack-entropy
- name: openstack-foundation
- name: openstack-gantt
- name: openstack-gate
- name: openstack-hyper-v
- name: openstack-infra
- name: openstack-ironic
- name: openstack-keystone
- name: openstack-manila
- name: openstack-marconi
- name: openstack-meeting
meetbots:
- open_stack
- name: openstack-meeting-3
meetbots:
- open_stack
- name: openstack-meeting-alt
meetbots:
- open_stack
- name: openstack-meniscus
- name: openstack-merges
- name: openstack-metering
- name: openstack-neutron
- name: openstack-nova
- name: openstack-opw
- name: openstack-oslo
- name: openstack-packaging
- name: openstack-qa
- name: openstack-raksha
- name: openstack-relmgr-office
- name: openstack-sdks
- name: openstack-state-management
- name: openstack-swift
- name: openstack-translation
- name: openstack-trove
- name: packstack-dev
- name: refstack
- name: storyboard
- name: syscompass
mask: full_mask
- name: tripleo

View File

@ -1,45 +0,0 @@
# Copyright 2014 OpenStack Foundation
#
# 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.
# Global definitions
# First set up the access levels (map names in this file to chanserv flags):
access:
masters: +AFRfiorstv
operators: +Aiortv
topics: +t
# Define access that should apply to all channels:
global:
masters:
- openstackinfra
operators:
- jeblair
- mtaylor
- clarkb
- fungi
- SergeyLukjanov
- ttx
- reed
topics:
- openstackstatus
# Individual channel configuration:
channels:
- name: openstack-infra
- name: openstack-meeting
topics:
- open_stack
- name: openstack-nova
operators:
- russellb

View File

@ -413,7 +413,7 @@ jobs:
- name: gate-config-irc-access - name: gate-config-irc-access
voting: false voting: false
files: files:
- 'modules/openstack_project/files/irc/channels.yaml' - 'modules/openstack_project/files/accessbot/channels.yaml'
# Continous publishing from master of the following documentation targets: # Continous publishing from master of the following documentation targets:
- name: openstack-admin-guide-cloud - name: openstack-admin-guide-cloud
branch: ^master$ branch: ^master$

View File

@ -12,6 +12,8 @@ class openstack_project::eavesdrop (
$statusbot_wiki_password = '', $statusbot_wiki_password = '',
$statusbot_wiki_url = '', $statusbot_wiki_url = '',
$statusbot_wiki_pageid = '', $statusbot_wiki_pageid = '',
$accessbot_nick = '',
$accessbot_password = '',
) { ) {
class { 'openstack_project::server': class { 'openstack_project::server':
iptables_public_tcp_ports => [80], iptables_public_tcp_ports => [80],
@ -80,4 +82,11 @@ class openstack_project::eavesdrop (
a2mod { 'headers': a2mod { 'headers':
ensure => present, ensure => present,
} }
class { 'accessbot':
nick => $accessbot_nick,
password => $accessbot_password,
server => $statusbot_server,
channel_file => 'puppet:///modules/openstack_project/files/accessbot/channels.yaml',
}
} }

View File

@ -17,7 +17,7 @@ commands = {posargs}
[testenv:irc] [testenv:irc]
deps = PyYAML deps = PyYAML
irc irc
commands = python modules/openstack_project/files/irc/checkaccess.py -l modules/openstack_project/files/irc/channels.yaml openstackinfra commands = python modules/accessbot/files/checkaccess.py -l modules/openstack_project/files/accessbot/channels.yaml openstackinfra
[flake8] [flake8]
show-source = True show-source = True