From abce7a8fc036e849ee20f386b003b0d93b97fead Mon Sep 17 00:00:00 2001 From: Giuseppe Bilotta Date: Tue, 1 Aug 2006 00:10:09 +0000 Subject: New Auth framework, initial commit --- lib/rbot/botuser.rb | 586 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 586 insertions(+) create mode 100644 lib/rbot/botuser.rb (limited to 'lib') diff --git a/lib/rbot/botuser.rb b/lib/rbot/botuser.rb new file mode 100644 index 00000000..db33cd27 --- /dev/null +++ b/lib/rbot/botuser.rb @@ -0,0 +1,586 @@ +#-- vim:sw=2:et +#++ +# :title: User management +# +# rbot user management +# Author:: Giuseppe Bilotta (giuseppe.bilotta@gmail.com) +# Copyright:: Copyright (c) 2006 Giuseppe Bilotta +# License:: GPLv2 + +#-- +##### +#### +### Discussion on IRC on how to implement it +## +# +# a. do we want user groups together with users? +# hmm +# let me think about it +# generally I would say: as simple as possible while keeping it as flexible as need be +# I think we can put user groups in place afterwards if we build the structure right +# prolly, yes +# so +# each plugin registers a name +# so rather than auth level we have +name -name +# yes +# much better +# the default is +name for every plugin, except when the plugin tells otherwise +# although.. +# if I only want to allow you access to one plugin +# I have lots of typing to do +# nope +# we allow things like -* +# ok +# and + has precedence +# hm no, not good either +# because we want bot -* +onething and +* -onething to work +# but then: one plugin currently can have several levels, no? +# of course +# commandedit, commanddel, commandfoo +# name.command ? +# yep +# (then you can't have dots in commands +# maybe name:command +# or name::comand +# like a namespace +# ehehehe yeah I like it :) +# tel +# brb +# usermod setcaps eean -* +# usermod setcaps eean +quiz::edit +# great +# or even +# auth eean -*, +quiz::edit +# awesome +# auth eean -*, +quiz::edit, +command, -command::del +# yes +# you know, the default should be -* +# because +# in the time between adding the user and changing auth +# it's insecure +# user could do havoc +# useradd eean, then eean does "~quit", before I change auth +# nope +# perhaps we should allow combining useradd with auth +# the default should be +* -important stuff +# ok +# how to specify channel stuff? +# for one, when you issue the command on the channel itself +# then it's channel relative +# perhaps +# or +# yes but I was thinking more about the syntax +# auth eean #rbot -quiz +# hm +# or maybe: treat channels like users: auth #rbot -quiz +# would shut up quiz in #rbot +# hm +# heh +# auth * #rbot -quiz +# not sure I'm making sense here ;) +# I think syntax should be auth [usermask] [channelmask] [modes] +# yes +# modes separated by comma? +# where channelmask is implied to be * +# no we can have it spacesplit +# great +# ok +# modes are detected by +- +# so you can do something like auth markey #rbot -quiz #amarok -chuck +# also I like "auth" a lot more than "usermod foo" +# yep +# I don't understand why the 'mod' +# we could have all auth commands start with use +# user +# user add +# user list +# user del +# yes +# user auth +# hm +# and maybe auth as a synonym for user auth +# this is also uncomfortable: usermod wants the full user mask +# you have to copy/paste it +# no +# can't you use *? +# sorry not sure +# but this shows, it's not inuitive +# I've read the docs +# but didn't know how to use it really +# markey!*@* +# that's not very intuitive +# we could use nick as a synonym for nick!*@* if it's too much for you :D +# usermod markey foo should suffice +# rememember: you're a hacker. when rbot gets many new users, they will often be noobs +# gotta make things simple to use +# but the hostmask is only needed for the user creation +# really? then forget what I said, sorry +# I think so +# ,help auth +# Auth module (User authentication) topics: setlevel, useradd, userdel, usermod, auth, levels, users, whoami, identify +# ,help usermod +# no help for topic usermod +# ,help auth usermod +# usermod => Modify s settings. Valid s are: hostmask, (+|-)hostmask, password, level (private addressing only) +# see? it's username, not nick :D +# btw, help usermod should also work +# ,help auth useradd +# useradd => Add user , you still need to set him up correctly (private addressing only) +# instead of help auth usermode +# when it's not ambiguous +# and the help for useradd is wrong +# for the website, we could make a logo contest :) the current logo looks like giblet made it in 5 minutes ;) +# ah well, for 1.0 maybe +# so a user on rbot is given by +# username, password, hostmasks, permissions +# yup +# the default permission is +* -importantstuff +# how defines importantstuff? +# you mean like core and auth? +# yes +# ok +# but we can decide about this :) +# some plugins are dangerous by default +# like command plugin +# you can do all sorts of nasty shit with it +# then command plugin will do something like: command.defaultperm("-command") +# yes, good point +# this is then added to the default permissions (user * channel *) +# when checking for auth, we go like this: +# hm +# check user * channel * +# then user name channel * +# then user * channel name +# then user name channel name +# for each of these combinations we match against * first, then against command, and then against command::subcommand +# yup +# setting or resetting it depending on wether it's + or - +# the final result gives us the permission +# implementation detail +# username and passwords are strings +# (I might rename the command plugin, the name is somewhat confusing) +# yeah +# hostmasks are hostmasks +# also I'm pondering to restrict it more: disallow access to @bot +# permissions are in the form [ [channel, {command => bool, ...}] ...] +#++ + +module Irc + + # This method raises a TypeError if _user_ is not of class User + # + def error_if_not_user(user) + raise TypeError, "#{user.inspect} must be of type Irc::User and not #{user.class}" unless user.class <= User + end + + # This method raises a TypeError if _chan_ is not of class Chan + # + def error_if_not_channel(chan) + raise TypeError, "#{chan.inspect} must be of type Irc::User and not #{chan.class}" unless chan.class <= Channel + end + + + # This module contains the actual Authentication stuff + # + module Auth + + # Generate a random password of length _l_ + # + def random_password(l=8) + pwd = "" + 8.times do + pwd += (rand(26) + (rand(2) == 0 ? 65 : 97) ).chr + end + return pwd + end + + + # An Irc::Auth::Command defines a command by its "path": + # + # base::command::subcommand::subsubcommand::subsubsubcommand + # + class Command + + attr_reader :command, :path + + # A method that checks if a given _cmd_ is in a form that can be + # reduced into a canonical command path, and if so, returns it + # + def sanitize_command_path(cmd) + pre = cmd.to_s.downcase.gsub(/^\*?(?:::)?/,"").gsub(/::$/,"") + return pre if pre.empty? + return pre if pre =~ /^\S+(::\S+)*$/ + raise TypeError, "#{cmd.inspect} is not a valid command" + end + + # Creates a new Command from a given string; you can then access + # the command as a symbol with the :command method and the whole + # path as :path + # + # Command.new("core::auth::save").path => [:"", :core, :"core::auth", :"core::auth::save"] + # + # Command.new("core::auth::save").command => :"core::auth::save" + # + def initialize(cmd) + cmdpath = sanitize_command_path(cmd).split('::') + seq = cmdpath.inject([""]) { |list, cmd| + list << (list.last ? list.last + "::" : "") + cmd + } + @path = seq.map { |k| + k.to_sym + } + @command = path.last + end + end + + # This method raises a TypeError if _user_ is not of class User + # + def error_if_not_command(cmd) + raise TypeError, "#{cmd.inspect} must be of type Irc::Auth::Command and not #{cmd.class}" unless cmd.class <= Command + end + + + # This class describes a permission set + class PermissionSet + + # Create a new (empty) PermissionSet + # + def initialize + @perm = {} + end + + # Sets the permission for command _cmd_ to _val_, + # creating intermediate permissions if needed. + # + def set_permission(cmd, val) + raise TypeError, "#{val.inspect} must be true or false" unless [true,false].include?(val) + error_if_not_command(cmd) + cmd.path.each { |k| + set_permission(k.to_s, true) unless @perm.has_key?(k) + } + @perm[path.last] = val + end + + # Tells if command _cmd_ is permitted. We do this by returning + # the value of the deepest Command#path that matches. + # + def permit?(cmd) + error_if_not_command(cmd) + allow = nil + cmd.path.reverse.each { |k| + if @perm.has_key?(k) + allow = @perm[k] + break + end + } + return allow + end + end + + + # This is the basic class for bot users: they have a username, a password, a + # list of netmasks to match against, and a list of permissions. + # + class BotUser + + attr_reader :username + attr_reader :password + attr_reader :netmasks + + # Create a new BotUser with given username + def initialize(username) + @username = BotUser.sanitize_username(username) + @password = nil + @netmasks = NetmaskList.new + @perm = {} + end + + # Resets the password by creating a new onw + def reset_password + @password = random_password + end + + # Sets the permission for command _cmd_ to _val_ on channel _chan_ + # + def set_permission(cmd, val, chan="*") + k = chan.to_s.to_sym + @perm[k] = PermissionSet.new unless @perm.has_key?(k) + @perm[k].set_permission(cmd, val) + end + + # Checks if BotUser is allowed to do something on channel _chan_, + # or on all channels if _chan_ is nil + # + def permit?(cmd, chan=nil) + if chan + k = chan.to_s.to_sym + else + k = :* + end + allow = nil + if @perm.has_key?(k) + allow = @perm[k].permit?(cmd) + end + return allow + end + + # Adds a Netmask + # + def add_netmask(mask) + case mask + when Netmask + @netmasks << mask + else + @netmasks << Netmask(mask) + end + end + + # Removes a Netmask + # + def delete_netmask(mask) + case mask + when Netmask + m = mask + else + m << Netmask(mask) + end + @netmasks.delete(m) + end + + # Removes all Netmasks + def reset_netmask_list + @netmasks = NetmaskList.new + end + + # This method checks if BotUser has a Netmask that matches _user_ + def knows?(user) + error_if_not_user(user) + known = false + @netmasks.each { |n| + if user.matches?(n) + known = true + break + end + } + return known + end + + # This method gets called when User _user_ wants to log in. + # It returns true or false depending on whether the password + # is right. If it is, the Netmask of the user is added to the + # list of acceptable Netmask unless it's already matched. + def login(user, password) + if password == @password + add_netmask(user) unless knows?(user) + return true + else + return false + end + end + + # # This method gets called when User _user_ has logged out as this BotUser + # def logout(user) + # delete_netmask(user) if knows?(user) + # end + + # This method sanitizes a username by chomping, downcasing + # and replacing any nonalphanumeric character with _ + # + def BotUser.sanitize_username(name) + return name.to_s.chomp.downcase.gsub(/[^a-z0-9]/,"_") + end + + # This method sets the password if the proposed new password + # is valid + def password=(pwd=nil) + if pwd + begin + raise InvalidPassword, "#{pwd} contains invalid characters" if pwd !~ /^[A-Za-z0-9]+$/ + raise InvalidPassword, "#{pwd} too short" if pwd.length < 4 + @password = pwd + rescue InvalidPassword => e + raise e + rescue => e + raise InvalidPassword, "Exception #{e.inspect} while checking #{pwd}" + end + else + reset_password + end + end + end + + + # This is the anonymous BotUser: it's used for all users which haven't + # identified with the bot + # + class AnonBotUserClass < BotUser + include Singleton + def initialize + super("anonymous") + end + private :login, :add_netmask, :delete_netmask + + # Anon knows everybody + def knows?(user) + error_if_not_user(user) + return true + end + + # Resets the NetmaskList + def reset_netmask_list + super + add_netmask("*!*@*") + end + end + + # Returns the only instance of AnonBotUserClass + # + def anonbotuser + return AnonBotUserClass.instance + end + + # This is the BotOwner: he can do everything + # + class BotOwnerClass < BotUser + include Singleton + def initialize + super("owner") + end + + def permit?(cmd, chan=nil) + return true + end + end + + # Returns the only instance of BotOwnerClass + # + def botowner + return BotOwneClass.instance + end + + + # This is the AuthManagerClass singleton, used to manage User/BotUser connections and + # everything + # + class AuthManagerClass + include Singleton + + # The instance manages two Hashes: one that maps + # Irc::Users onto BotUsers, and the other that maps + # usernames onto BotUser + def initialize + reset_hashes + + # This variable is set to true when there have been changes + # to the botusers list, so that we know when to save + @has_changes = false + end + + # resets the hashes + def reset_hashes + @botusers = Hash.new + @allbotusers = Hash.new + [anonbotuser, botowner].each { |x| @allbotusers[x.username.to_sym] = x } + end + + # load botlist from userfile + def load_merge(filename=nil) + # TODO + raise NotImplementedError + @has_changes = true + end + + def load(filename=nil) + reset_hashes + load_merge(filename) + end + + # save botlist to userfile + def save(filename=nil) + return unless @has_changes + # TODO + raise NotImplementedError + end + + # checks if we know about a certain BotUser username + def include?(botusername) + @allbotusers.has_key?(botusername.to_sym) + end + + # Maps Irc::User to BotUser + def irc_to_botuser(ircuser) + error_if_not_user(ircuser) + return @botusers[ircuser] || anonbotuser + end + + # creates a new BotUser + def create_botuser(name, password=nil) + n = BotUser.sanitize_username(name) + k = n.to_sym + raise "BotUser #{n} exists" if include?(k) + bu = BotUser.new(n) + bu.password = password + @allbotusers[k] = bu + end + + # Logs Irc::User _ircuser_ in to BotUser _botusername_ with password _pwd_ + # + # raises an error if _botusername_ is not a known BotUser username + # + # It is possible to autologin by Netmask, on request + # + def login(ircuser, botusername, pwd, bymask = false) + error_if_not_user(ircuser) + n = BotUser.sanitize_username(name) + k = n.to_sym + raise "No such BotUser #{n}" unless include?(k) + if @botusers.has_key?(ircuser) + # TODO + # @botusers[ircuser].logout(ircuser) + end + bu = @allbotusers[k] + if bymask && bu.knows?(user) + @botusers[ircuser] = bu + return true + elsif bu.login(ircuser, pwd) + @botusers[ircuser] = bu + return true + end + return false + end + + # Checks if User _user_ can do _cmd_ on _chan_. + # + # Permission are checked in this order, until a true or false + # is returned: + # * associated BotUser on _chan_ + # * associated BotUser on all channels + # * anonbotuser on _chan_ + # * anonbotuser on all channels + # + def permit?(user, cmdtxt, chan=nil) + error_if_not_user(user) + cmd = Command.new(cmdtxt) + allow = nil + botuser = @botusers[user] + allow = botuser.permit?(cmd, chan) if chan + return allow unless allow.nil? + allow = botuser.permit?(cmd) + return allow unless allow.nil? + unless botuser == anonbotuser + allow = anonbotuser.permit?(cmd, chan) if chan + return allow unless allow.nil? + allow = anonbotuser.permit?(cmd) + return allow unless allow.nil? + end + raise "Could not check permission for user #{user.inspect} to run #{cmdtxt.inspect} on #{chan.inspect}" + end + end + + # Returns the only instance of AuthManagerClass + # + def authmanager + return AuthManagerClass.instance + end + end +end -- cgit v1.2.3