-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #52 from alanbriolat/plugin/auth
User tracking and permissions/authentication
- Loading branch information
Showing
13 changed files
with
638 additions
and
30 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,12 @@ | ||
[@bot] | ||
nickname = Mathison | ||
channels = #cs-york #cs-york-dev | ||
plugins = logger linkinfo hoogle imgur csyork | ||
plugins = logger linkinfo hoogle imgur csyork auth | ||
|
||
[linkinfo] | ||
scan_limit = 2 | ||
|
||
[auth] | ||
@everything = * *:* | ||
Alan = @everything | ||
Haegin = @everything |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
from collections import defaultdict | ||
|
||
from csbot.plugin import Plugin | ||
|
||
|
||
class PermissionDB(defaultdict): | ||
"""A helper class for assembling the permissions database.""" | ||
def __init__(self): | ||
super(PermissionDB, self).__init__(set) | ||
self._groups = defaultdict(set) | ||
self._current = None | ||
|
||
def process(self, entity, permissions): | ||
"""Process a configuration entry, where *entity* is an account name, | ||
``@group`` name or ``*`` and *permissions* is a space-separated list of | ||
permissions to grant. | ||
""" | ||
self._current = (entity, permissions) | ||
|
||
if entity.startswith('@'): | ||
if entity in self._groups: | ||
self._error('Group "{}" already defined'.format(entity)) | ||
entity_perms = self._groups[entity] | ||
else: | ||
entity_perms = self[entity] | ||
|
||
for permission in permissions.split(): | ||
if ':' in permission: | ||
self._add_channel_permissions(entity_perms, permission) | ||
elif permission.startswith('@'): | ||
self._copy_group_permissions(entity_perms, permission) | ||
else: | ||
self._add_bot_permission(entity_perms, permission) | ||
|
||
self._current = None | ||
|
||
def get_permissions(self, entity): | ||
"""Get the set of permissions for *entity*. | ||
The union of the permissions for *entity* and the universal (``*``) | ||
permissions is returned. If *entity* is ``None``, only the universal | ||
permissions are returned. | ||
""" | ||
if entity is None: | ||
return set(self.get('*', set())) | ||
else: | ||
return self.get(entity, set()) | self.get('*', set()) | ||
|
||
def check(self, entity, permission, channel=None): | ||
"""Check if *entity* has *permission*. | ||
If *channel* is present, check for a channel permission, otherwise check | ||
for a bot permission. Compatible wildcard permissions are also checked. | ||
""" | ||
if channel is None: | ||
checks = {permission, '*'} | ||
else: | ||
checks = {(channel, permission), (channel, '*'), | ||
('*', permission), ('*', '*')} | ||
|
||
return len(checks & self.get_permissions(entity)) > 0 | ||
|
||
def _add_channel_permissions(self, entity_perms, permission): | ||
channel, _, permissions = permission.partition(':') | ||
|
||
if not (channel == '*' or channel.startswith('#')): | ||
self._error('Invalid channel name "{}", must be * or #channel' | ||
.format(channel)) | ||
if permissions == '': | ||
self._error('No permissions specified for channel "{}"' | ||
.format(channel)) | ||
|
||
for p in permissions.split(','): | ||
entity_perms.add((channel, p)) | ||
|
||
def _copy_group_permissions(self, entity_perms, group): | ||
if group not in self._groups: | ||
self._error('Permission group "{}" undefined'.format(group)) | ||
if group == self._current[0]: | ||
self._error('Recursive group definition') | ||
entity_perms.update(self._groups[group]) | ||
|
||
def _add_bot_permission(self, entity_perms, permission): | ||
entity_perms.add(permission) | ||
|
||
def _error(self, s, *args): | ||
entity, perms = self._current | ||
raise ValueError('{} (in: {} = {})'.format(s, entity, perms), *args) | ||
|
||
|
||
class Auth(Plugin): | ||
PLUGIN_DEPENDS = ['usertrack'] | ||
|
||
def setup(self): | ||
super(Auth, self).setup() | ||
|
||
self._permissions = PermissionDB() | ||
for entity, permissions in self.config.items(): | ||
self._permissions.process(entity, permissions) | ||
|
||
for e, p in self._permissions.iteritems(): | ||
self.log.debug((e, p)) | ||
|
||
def check(self, nick, perm, channel=None): | ||
account = self.bot.plugins['usertrack'].get_user(nick)['account'] | ||
return self._permissions.check(account, perm, channel) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
from collections import defaultdict | ||
from copy import deepcopy | ||
|
||
from csbot.plugin import Plugin | ||
from csbot.util import nick | ||
|
||
|
||
class UserDict(defaultdict): | ||
def __missing__(self, key): | ||
user = self.create_user(key) | ||
self[key] = user | ||
return user | ||
|
||
@staticmethod | ||
def create_user(nick): | ||
return { | ||
'nick': nick, | ||
'account': None, | ||
'channels': set(), | ||
} | ||
|
||
def copy_or_create(self, nick): | ||
if nick in self: | ||
return deepcopy(self[nick]) | ||
else: | ||
return self.create_user(nick) | ||
|
||
|
||
class UserTrack(Plugin): | ||
def setup(self): | ||
super(UserTrack, self).setup() | ||
self._users = UserDict() | ||
|
||
@Plugin.hook('core.channel.joined') | ||
def _channel_joined(self, e): | ||
user = self._users[nick(e['user'])] | ||
user['channels'].add(e['channel']) | ||
|
||
@Plugin.hook('core.channel.left') | ||
def _channel_left(self, e): | ||
user = self._users[nick(e['user'])] | ||
user['channels'].discard(e['channel']) | ||
# Lost sight of the user, can't reliably track them any more | ||
if len(user['channels']) == 0: | ||
del self._users[nick(e['user'])] | ||
|
||
@Plugin.hook('core.channel.names') | ||
def _channel_names(self, e): | ||
for name, prefixes in e['names']: | ||
user = self._users[name] | ||
user['channels'].add(e['channel']) | ||
|
||
@Plugin.hook('core.user.identified') | ||
def _user_identified(self, e): | ||
user = self._users[nick(e['user'])] | ||
user['account'] = e['account'] | ||
|
||
@Plugin.hook('core.user.renamed') | ||
def _user_renamed(self, e): | ||
# Retrieve user record | ||
user = self._users[e['oldnick']] | ||
# Remove old nick entry | ||
del self._users[user['nick']] | ||
# Rename user | ||
user['nick'] = e['newnick'] | ||
# Add under new nick | ||
self._users[user['nick']] = user | ||
|
||
@Plugin.hook('core.user.quit') | ||
def _user_quit(self, e): | ||
# User is gone, remove record | ||
del self._users[nick(e['user'])] | ||
|
||
def get_user(self, nick): | ||
"""Get a copy of the user record for *nick*. | ||
""" | ||
return self._users.copy_or_create(nick) | ||
|
||
@Plugin.command('account', help=('account [nick]: show Freenode account for' | ||
' a nick, or for yourself if omitted')) | ||
def account_command(self, e): | ||
nick_ = e['data'] or nick(e['user']) | ||
account = self.get_user(nick_)['account'] | ||
if account is None: | ||
e.protocol.msg(e['reply_to'], | ||
u'{} is not authenticated'.format(nick_)) | ||
else: | ||
e.protocol.msg(e['reply_to'], | ||
u'{} is authenticated as {}'.format(nick_, account)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.