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