]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - lib/rbot/botuser.rb
Adjust auth framework to work with improved Irc framework and fix some issues in...
[user/henk/code/ruby/rbot.git] / lib / rbot / botuser.rb
1 #-- vim:sw=2:et\r
2 #++\r
3 # :title: User management\r
4 #\r
5 # rbot user management\r
6 # Author:: Giuseppe Bilotta (giuseppe.bilotta@gmail.com)\r
7 # Copyright:: Copyright (c) 2006 Giuseppe Bilotta\r
8 # License:: GPLv2\r
9 \r
10 require 'singleton'\r
11 \r
12 \r
13 module Irc\r
14 \r
15 \r
16   # This module contains the actual Authentication stuff\r
17   #\r
18   module Auth\r
19 \r
20     BotConfig.register BotConfigStringValue.new( 'auth.password',\r
21       :default => 'rbotauth', :wizard => true,\r
22       :desc => 'Password for the bot owner' )\r
23     # BotConfig.register BotConfigIntegerValue.new( 'auth.default_level',\r
24     #   :default => 10, :wizard => true,\r
25     #   :desc => 'The default level for new/unknown users' )\r
26 \r
27     # Generate a random password of length _l_\r
28     #\r
29     def random_password(l=8)\r
30       pwd = ""\r
31       8.times do\r
32         pwd += (rand(26) + (rand(2) == 0 ? 65 : 97) ).chr\r
33       end\r
34       return pwd\r
35     end\r
36 \r
37 \r
38     # An Irc::Auth::Command defines a command by its "path":\r
39     #\r
40     #   base::command::subcommand::subsubcommand::subsubsubcommand\r
41     #\r
42     class Command\r
43 \r
44       attr_reader :command, :path\r
45 \r
46       # A method that checks if a given _cmd_ is in a form that can be\r
47       # reduced into a canonical command path, and if so, returns it\r
48       #\r
49       def sanitize_command_path(cmd)\r
50         pre = cmd.to_s.downcase.gsub(/^\*?(?:::)?/,"").gsub(/::$/,"")\r
51         return pre if pre.empty?\r
52         return pre if pre =~ /^\S+(::\S+)*$/\r
53         raise TypeError, "#{cmd.inspect} is not a valid command"\r
54       end\r
55 \r
56       # Creates a new Command from a given string; you can then access\r
57       # the command as a symbol with the :command method and the whole\r
58       # path as :path\r
59       #\r
60       #   Command.new("core::auth::save").path => [:"*", :"core", :"core::auth", :"core::auth::save"]\r
61       #\r
62       #   Command.new("core::auth::save").command => :"core::auth::save"\r
63       #\r
64       def initialize(cmd)\r
65         cmdpath = sanitize_command_path(cmd).split('::')\r
66         seq = cmdpath.inject(["*"]) { |list, cmd|\r
67           list << (list.length > 1 ? list.last + "::" : "") + cmd\r
68         }\r
69         @path = seq.map { |k|\r
70           k.to_sym\r
71         }\r
72         @command = path.last\r
73         debug "Created command #{@command.inspect} with path #{@path.join(', ')}"\r
74       end\r
75 \r
76       # Returs self\r
77       def to_irc_auth_command\r
78         self\r
79       end\r
80 \r
81     end\r
82 \r
83   end\r
84 \r
85 end\r
86 \r
87 \r
88 class String\r
89 \r
90   # Returns an Irc::Auth::Comand from the receiver\r
91   def to_irc_auth_command\r
92     Irc::Auth::Command.new(self)\r
93   end\r
94 \r
95 end\r
96 \r
97 \r
98 module Irc\r
99 \r
100 \r
101   module Auth\r
102 \r
103 \r
104     # This class describes a permission set\r
105     class PermissionSet\r
106 \r
107       # Create a new (empty) PermissionSet\r
108       #\r
109       def initialize\r
110         @perm = {}\r
111       end\r
112 \r
113       # Inspection simply inspects the internal hash\r
114       def inspect\r
115         @perm.inspect\r
116       end\r
117 \r
118       # Sets the permission for command _cmd_ to _val_,\r
119       #\r
120       def set_permission(str, val)\r
121         cmd = str.to_irc_auth_command\r
122         case val\r
123         when true, false\r
124           @perm[cmd.command] = val\r
125         when nil\r
126           @perm.delete(cmd.command)\r
127         else\r
128           raise TypeError, "#{val.inspect} must be true or false" unless [true,false].include?(val)\r
129         end\r
130       end\r
131 \r
132       # Resets the permission for command _cmd_\r
133       #\r
134       def reset_permission(cmd)\r
135         set_permission(cmd, nil)\r
136       end\r
137 \r
138       # Tells if command _cmd_ is permitted. We do this by returning\r
139       # the value of the deepest Command#path that matches.\r
140       #\r
141       def permit?(str)\r
142         cmd = str.to_irc_auth_command\r
143         allow = nil\r
144         cmd.path.reverse.each { |k|\r
145           if @perm.has_key?(k)\r
146             allow = @perm[k]\r
147             break\r
148           end\r
149         }\r
150         return allow\r
151       end\r
152 \r
153     end\r
154 \r
155 \r
156     # This is the basic class for bot users: they have a username, a password, a\r
157     # list of netmasks to match against, and a list of permissions.\r
158     #\r
159     class BotUser\r
160 \r
161       attr_reader :username\r
162       attr_reader :password\r
163       attr_reader :netmasks\r
164 \r
165       # Create a new BotUser with given username\r
166       def initialize(username)\r
167         @username = BotUser.sanitize_username(username)\r
168         @password = nil\r
169         @netmasks = NetmaskList.new\r
170         @perm = {}\r
171       end\r
172 \r
173       # Inspection\r
174       def inspect\r
175         str = "<#{self.class}:#{'0x%08x' % self.object_id}:"\r
176         str << " @username=#{@username.inspect}"\r
177         str << " @netmasks=#{@netmasks.inspect}"\r
178         str << " @perm=#{@perm.inspect}"\r
179         str\r
180       end\r
181 \r
182       # Convert into a hash\r
183       def to_hash\r
184         {\r
185           :username => @username,\r
186           :password => @password,\r
187           :netmasks => @netmasks,\r
188           :perm => @perm\r
189         }\r
190       end\r
191 \r
192       # Restore from hash\r
193       def from_hash(h)\r
194         @username = h[:username] if h.has_key?(:username)\r
195         @password = h[:password] if h.has_key?(:password)\r
196         @netmasks = h[:netmasks] if h.has_key?(:netmasks)\r
197         @perm = h[:perm] if h.has_key?(:perm)\r
198       end\r
199 \r
200       # This method sets the password if the proposed new password\r
201       # is valid\r
202       def password=(pwd=nil)\r
203         if pwd\r
204           begin\r
205             raise InvalidPassword, "#{pwd} contains invalid characters" if pwd !~ /^[A-Za-z0-9]+$/\r
206             raise InvalidPassword, "#{pwd} too short" if pwd.length < 4\r
207             @password = pwd\r
208           rescue InvalidPassword => e\r
209             raise e\r
210           rescue => e\r
211             raise InvalidPassword, "Exception #{e.inspect} while checking #{pwd}"\r
212           end\r
213         else\r
214           reset_password\r
215         end\r
216       end\r
217 \r
218       # Resets the password by creating a new onw\r
219       def reset_password\r
220         @password = random_password\r
221       end\r
222 \r
223       # Sets the permission for command _cmd_ to _val_ on channel _chan_\r
224       #\r
225       def set_permission(cmd, val, chan="*")\r
226         k = chan.to_s.to_sym\r
227         @perm[k] = PermissionSet.new unless @perm.has_key?(k)\r
228         @perm[k].set_permission(cmd, val)\r
229       end\r
230 \r
231       # Resets the permission for command _cmd_ on channel _chan_\r
232       #\r
233       def reset_permission(cmd, chan ="*")\r
234         set_permission(cmd, nil, chan)\r
235       end\r
236 \r
237       # Checks if BotUser is allowed to do something on channel _chan_,\r
238       # or on all channels if _chan_ is nil\r
239       #\r
240       def permit?(cmd, chan=nil)\r
241         if chan\r
242           k = chan.to_s.to_sym\r
243         else\r
244           k = :*\r
245         end\r
246         allow = nil\r
247         if @perm.has_key?(k)\r
248           allow = @perm[k].permit?(cmd)\r
249         end\r
250         return allow\r
251       end\r
252 \r
253       # Adds a Netmask\r
254       #\r
255       def add_netmask(mask)\r
256         @netmasks << mask.to_irc_netmask\r
257       end\r
258 \r
259       # Removes a Netmask\r
260       #\r
261       def delete_netmask(mask)\r
262         m = mask.to_irc_netmask\r
263         @netmasks.delete(m)\r
264       end\r
265 \r
266       # Removes all <code>Netmask</code>s\r
267       #\r
268       def reset_netmask_list\r
269         @netmasks = NetmaskList.new\r
270       end\r
271 \r
272       # This method checks if BotUser has a Netmask that matches _user_\r
273       #\r
274       def knows?(usr)\r
275         user = usr.to_irc_user\r
276         known = false\r
277         @netmasks.each { |n|\r
278           if user.matches?(n)\r
279             known = true\r
280             break\r
281           end\r
282         }\r
283         return known\r
284       end\r
285 \r
286       # This method gets called when User _user_ wants to log in.\r
287       # It returns true or false depending on whether the password\r
288       # is right. If it is, the Netmask of the user is added to the\r
289       # list of acceptable Netmask unless it's already matched.\r
290       def login(user, password)\r
291         if password == @password\r
292           add_netmask(user) unless knows?(user)\r
293           debug "#{user} logged in as #{self.inspect}"\r
294           return true\r
295         else\r
296           return false\r
297         end\r
298       end\r
299 \r
300       # # This method gets called when User _user_ has logged out as this BotUser\r
301       # def logout(user)\r
302       #   delete_netmask(user) if knows?(user)\r
303       # end\r
304 \r
305       # This method sanitizes a username by chomping, downcasing\r
306       # and replacing any nonalphanumeric character with _\r
307       #\r
308       def BotUser.sanitize_username(name)\r
309         return name.to_s.chomp.downcase.gsub(/[^a-z0-9]/,"_")\r
310       end\r
311 \r
312     end\r
313 \r
314 \r
315     # This is the default BotUser: it's used for all users which haven't\r
316     # identified with the bot\r
317     #\r
318     class DefaultBotUserClass < BotUser\r
319 \r
320       private :login, :add_netmask, :delete_netmask\r
321 \r
322       include Singleton\r
323 \r
324       def initialize\r
325         super("everyone")\r
326         @default_perm = PermissionSet.new\r
327       end\r
328 \r
329       # Sets the default permission for the default user (i.e. the ones\r
330       # set by the BotModule writers) on all channels\r
331       #\r
332       def set_default_permission(cmd, val)\r
333         @default_perm.set_permission(Command.new(cmd), val)\r
334         debug "Default permissions now:\n#{@default_perm.inspect}"\r
335       end\r
336 \r
337       # default knows everybody\r
338       #\r
339       def knows?(user)\r
340         return true if user.to_irc_user\r
341       end\r
342 \r
343       # Resets the NetmaskList\r
344       def reset_netmask_list\r
345         super\r
346         add_netmask("*!*@*")\r
347       end\r
348 \r
349       # DefaultBotUser will check the default_perm after checking\r
350       # the global ones\r
351       # or on all channels if _chan_ is nil\r
352       #\r
353       def permit?(cmd, chan=nil)\r
354         allow = super(cmd, chan)\r
355         if allow.nil? && chan.nil?\r
356           allow = @default_perm.permit?(cmd)\r
357         end\r
358         return allow\r
359       end\r
360 \r
361     end\r
362 \r
363     # Returns the only instance of DefaultBotUserClass\r
364     #\r
365     def Auth.defaultbotuser\r
366       return DefaultBotUserClass.instance\r
367     end\r
368 \r
369     # This is the BotOwner: he can do everything\r
370     #\r
371     class BotOwnerClass < BotUser\r
372 \r
373       include Singleton\r
374 \r
375       def initialize\r
376         super("owner")\r
377       end\r
378 \r
379       def permit?(cmd, chan=nil)\r
380         return true\r
381       end\r
382 \r
383     end\r
384 \r
385     # Returns the only instance of BotOwnerClass\r
386     #\r
387     def Auth.botowner\r
388       return BotOwnerClass.instance\r
389     end\r
390 \r
391 \r
392     # This is the AuthManagerClass singleton, used to manage User/BotUser connections and\r
393     # everything\r
394     #\r
395     class AuthManagerClass\r
396 \r
397       include Singleton\r
398 \r
399       attr_reader :everyone\r
400       attr_reader :botowner\r
401 \r
402       # The instance manages two <code>Hash</code>es: one that maps\r
403       # <code>Irc::User</code>s onto <code>BotUser</code>s, and the other that maps\r
404       # usernames onto <code>BotUser</code>\r
405       def initialize\r
406         @everyone = Auth::defaultbotuser\r
407         @botowner = Auth::botowner\r
408         bot_associate(nil)\r
409       end\r
410 \r
411       def bot_associate(bot)\r
412         raise "Cannot associate with a new bot! Save first" if defined?(@has_changes) && @has_changes\r
413 \r
414         reset_hashes\r
415 \r
416         # Associated bot\r
417         @bot = bot\r
418 \r
419         # This variable is set to true when there have been changes\r
420         # to the botusers list, so that we know when to save\r
421         @has_changes = false\r
422       end\r
423 \r
424       def set_changed\r
425         @has_changes = true\r
426       end\r
427 \r
428       def reset_changed\r
429         @has_changes = false\r
430       end\r
431 \r
432       def changed?\r
433         @has_changes\r
434       end\r
435 \r
436       # resets the hashes\r
437       def reset_hashes\r
438         @botusers = Hash.new\r
439         @allbotusers = Hash.new\r
440         [everyone, botowner].each { |x|\r
441           @allbotusers[x.username.to_sym] = x\r
442         }\r
443       end\r
444 \r
445       def load_array(ary, forced)\r
446         raise "Won't load with unsaved changes" if @has_changes and not forced\r
447         reset_hashes\r
448         ary.each { |x|\r
449           raise TypeError, "#{x} should be a Hash" unless x.kind_of?(Hash)\r
450           u = x[:username]\r
451           unless include?(u)\r
452             create_botuser(u)\r
453           end\r
454           get_botuser(u).from_hash(x)\r
455         }\r
456         @has_changes=false\r
457       end\r
458 \r
459       def save_array\r
460         @allbotusers.values.map { |x|\r
461           x.to_hash\r
462         }\r
463       end\r
464 \r
465       # checks if we know about a certain BotUser username\r
466       def include?(botusername)\r
467         @allbotusers.has_key?(botusername.to_sym)\r
468       end\r
469 \r
470       # Maps <code>Irc::User</code> to BotUser\r
471       def irc_to_botuser(ircuser)\r
472         # TODO check netmasks\r
473         @botusers[ircuser.to_irc_user] || everyone\r
474       end\r
475 \r
476       # creates a new BotUser\r
477       def create_botuser(name, password=nil)\r
478         n = BotUser.sanitize_username(name)\r
479         k = n.to_sym\r
480         raise "BotUser #{n} exists" if include?(k)\r
481         bu = BotUser.new(n)\r
482         bu.password = password\r
483         @allbotusers[k] = bu\r
484       end\r
485 \r
486       # returns the botuser with name _name_\r
487       def get_botuser(name)\r
488         @allbotusers.fetch(BotUser.sanitize_username(name).to_sym)\r
489       end\r
490 \r
491       # Logs Irc::User _ircuser_ in to BotUser _botusername_ with password _pwd_\r
492       #\r
493       # raises an error if _botusername_ is not a known BotUser username\r
494       #\r
495       # It is possible to autologin by Netmask, on request\r
496       #\r
497       def login(user, botusername, pwd, bymask = false)\r
498         ircuser = user.to_irc_user\r
499         n = BotUser.sanitize_username(botusername)\r
500         k = n.to_sym\r
501         raise "No such BotUser #{n}" unless include?(k)\r
502         if @botusers.has_key?(ircuser)\r
503           # TODO\r
504           # @botusers[ircuser].logout(ircuser)\r
505         end\r
506         bu = @allbotusers[k]\r
507         if bymask && bu.knows?(ircuser)\r
508           @botusers[ircuser] = bu\r
509           return true\r
510         elsif bu.login(ircuser, pwd)\r
511           @botusers[ircuser] = bu\r
512           return true\r
513         end\r
514         return false\r
515       end\r
516 \r
517       # Checks if User _user_ can do _cmd_ on _chan_.\r
518       #\r
519       # Permission are checked in this order, until a true or false\r
520       # is returned:\r
521       # * associated BotUser on _chan_\r
522       # * associated BotUser on all channels\r
523       # * everyone on _chan_\r
524       # * everyone on all channels\r
525       #\r
526       def permit?(user, cmdtxt, channel=nil)\r
527         botuser = irc_to_botuser(user)\r
528         cmd = cmdtxt.to_irc_auth_command\r
529 \r
530         chan = channel\r
531         case chan\r
532         when User\r
533           chan = "?"\r
534         when Channel\r
535           chan = chan.name\r
536         end\r
537 \r
538         allow = nil\r
539 \r
540         allow = botuser.permit?(cmd, chan) if chan\r
541         return allow unless allow.nil?\r
542         allow = botuser.permit?(cmd)\r
543         return allow unless allow.nil?\r
544 \r
545         unless botuser == everyone\r
546           allow = everyone.permit?(cmd, chan) if chan\r
547           return allow unless allow.nil?\r
548           allow = everyone.permit?(cmd)\r
549           return allow unless allow.nil?\r
550         end\r
551 \r
552         raise "Could not check permission for user #{user.inspect} to run #{cmdtxt.inspect} on #{chan.inspect}"\r
553       end\r
554 \r
555       # Checks if command _cmd_ is allowed to User _user_ on _chan_\r
556       def allow?(cmdtxt, user, chan=nil)\r
557         permit?(user, cmdtxt, chan)\r
558       end\r
559 \r
560     end\r
561 \r
562     # Returns the only instance of AuthManagerClass\r
563     #\r
564     def Auth.authmanager\r
565       return AuthManagerClass.instance\r
566     end\r
567 \r
568   end\r
569 \r
570 end\r