]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - lib/rbot/core/auth.rb
After changing a botuser setting, the settings for the issuing botuser were displayed...
[user/henk/code/ruby/rbot.git] / lib / rbot / core / auth.rb
1 #-- vim:sw=2:et\r
2 #++\r
3 \r
4 \r
5 class AuthModule < CoreBotModule\r
6 \r
7   def initialize\r
8     super\r
9     load_array(:default, true)\r
10     debug "initialized auth. Botusers: #{@bot.auth.save_array.inspect}"\r
11   end\r
12 \r
13   def save\r
14     save_array\r
15   end\r
16 \r
17   def save_array(key=:default)\r
18     if @bot.auth.changed?\r
19       @registry[key] = @bot.auth.save_array\r
20       @bot.auth.reset_changed\r
21       debug "saved botusers (#{key}): #{@registry[key].inspect}"\r
22     end\r
23   end\r
24 \r
25   def load_array(key=:default, forced=false)\r
26     debug "loading botusers (#{key}): #{@registry[key].inspect}"\r
27     @bot.auth.load_array(@registry[key], forced) if @registry.has_key?(key)\r
28   end\r
29 \r
30   # The permission parameters accept arguments with the following syntax:\r
31   #   cmd_path... [on #chan .... | in here | in private]\r
32   # This auxiliary method scans the array _ar_ to see if it matches\r
33   # the given syntax: it expects + or - signs in front of _cmd_path_\r
34   # elements when _setting_ = true\r
35   #\r
36   # It returns an array whose first element is the array of cmd_path,\r
37   # the second element is an array of locations and third an array of\r
38   # warnings occurred while parsing the strings\r
39   #\r
40   def parse_args(ar, setting)\r
41     cmds = []\r
42     locs = []\r
43     warns = []\r
44     doing_cmds = true\r
45     next_must_be_chan = false\r
46     want_more = false\r
47     last_idx = 0\r
48     ar.each_with_index { |x, i|\r
49       if doing_cmds # parse cmd_path\r
50         # check if the list is done\r
51         if x == "on" or x == "in"\r
52           doing_cmds = false\r
53           next_must_be_chan = true if x == "on"\r
54           next\r
55         end\r
56         if "+-".include?(x[0])\r
57           warns << ArgumentError("please do not use + or - in front of command #{x} when resetting") unless setting\r
58         else\r
59           warns << ArgumentError("+ or - expected in front of #{x}") if setting\r
60         end\r
61         cmds << x\r
62       else # parse locations\r
63         if x[-1].chr == ','\r
64           want_more = true\r
65         else\r
66           want_more = false\r
67         end\r
68         case next_must_be_chan\r
69         when false\r
70           locs << x.gsub(/^here$/,'_').gsub(/^private$/,'?')\r
71         else\r
72           warns << ArgumentError("#{x} doesn't look like a channel name") unless @bot.server.supports[:chantypes].include?(x[0])\r
73           locs << x\r
74         end\r
75         unless wants_more\r
76           last_idx = i\r
77           break\r
78         end\r
79       end\r
80     }\r
81     warns << "trailing comma" if wants_more\r
82     warns << "you probably forgot a comma" unless last_idx == ar.length - 1\r
83     return cmds, locs, warns\r
84   end\r
85 \r
86   def auth_set(m, params)\r
87     cmds, locs, warns = parse_args(params[:args])\r
88     errs = warns.select { |w| w.kind_of?(Exception) }\r
89     unless errs.empty?\r
90       m.reply "couldn't satisfy your request: #{errs.join(',')}"\r
91       return\r
92     end\r
93     user = params[:user].sub(/^all$/,"everyone")\r
94     begin\r
95       bu = @bot.auth.get_botuser(user)\r
96     rescue\r
97       return m.reply("couldn't find botuser #{user}")\r
98     end\r
99     if locs.empty?\r
100       locs << "*"\r
101     end\r
102     begin\r
103       locs.each { |loc|\r
104         ch = loc\r
105         if m.private?\r
106           ch = "?" if loc == "_"\r
107         else\r
108           ch = m.target.to_s if loc == "_"\r
109         end\r
110         cmds.each { |setval|\r
111           val = setval[0].chr == '+'\r
112           cmd = setval[1..-1]\r
113           bu.set_permission(cmd, val, ch)\r
114         }\r
115       }\r
116     rescue => e\r
117       m.reply "something went wrong while trying to set the permissions"\r
118       raise\r
119     end\r
120     @bot.auth.set_changed\r
121     debug "user #{user} permissions changed"\r
122     m.reply "ok, #{user} now also has permissions #{params[:args].join(' ')}"\r
123   end\r
124 \r
125   def get_botuser_for(user)\r
126     @bot.auth.irc_to_botuser(user)\r
127   end\r
128 \r
129   def get_botusername_for(user)\r
130     get_botuser_for(user).username\r
131   end\r
132 \r
133   def welcome(user)\r
134     "welcome, #{get_botusername_for(user)}"\r
135   end\r
136 \r
137   def auth_login(m, params)\r
138     begin\r
139       case @bot.auth.login(m.source, params[:botuser], params[:password])\r
140       when true\r
141         m.reply welcome(m.source)\r
142         @bot.auth.set_changed\r
143       else\r
144         m.reply "sorry, can't do"\r
145       end\r
146     rescue => e\r
147       m.reply "couldn't login: #{e}"\r
148       raise\r
149     end\r
150   end\r
151 \r
152   def auth_autologin(m, params)\r
153     u = do_autologin(m.source)\r
154     case u.username\r
155     when 'everyone'\r
156       m.reply "I couldn't find anything to let you login automatically"\r
157     else\r
158       m.reply welcome(m.source)\r
159     end\r
160   end\r
161 \r
162   def do_autologin(user)\r
163     @bot.auth.autologin(user)\r
164   end\r
165 \r
166   def auth_whoami(m, params)\r
167     rep = ""\r
168     # if m.public?\r
169     #   rep << m.source.nick << ", "\r
170     # end\r
171     rep << "you are "\r
172     rep << get_botusername_for(m.source).gsub(/^everyone$/, "no one that I know").gsub(/^owner$/, "my boss")\r
173     m.reply rep\r
174   end\r
175 \r
176   def help(plugin, topic="")\r
177     case topic\r
178     when /^login/\r
179       return "login [<botuser>] [<pass>]: logs in to the bot as botuser <botuser> with password <pass>. When using the full form, you must contact the bot in private. <pass> can be omitted if <botuser> allows login-by-mask and your netmask is among the known ones. if <botuser> is omitted too autologin will be attempted"\r
180     when /^whoami/\r
181       return "whoami: names the botuser you're linked to"\r
182     when /^permission syntax/\r
183       return "a permission is specified as module::path::to::cmd; when you want to enable it, prefix it with +; when you want to disable it, prefix it with -; when using the +reset+ command, do not use any prefix"\r
184     when /^permission/\r
185       return "permissions (re)set <permission> [in <channel>] for <user>: sets or resets the permissions for botuser <user> in channel <channel> (use ? to change the permissions for private addressing)"\r
186     when /^user show/\r
187       return "user show <what> : shows info about the user; <what> can be any of autologin, login-by-mask, netmasks"\r
188     when /^user (en|dis)able/\r
189       return "user enable|disable <what> : turns on or off <what> (autologin, login-by-mask)"\r
190     when /^user set/\r
191       return "user set password <blah> : sets the user password to <blah>; passwords can only contain upper and lowercase letters and numbers, and must be at least 4 characters long"\r
192     when /^user (add|rm)/\r
193       return "user add|rm netmask <mask> : adds/removes netmask <mask> from the list of netmasks known to the botuser you're linked to"\r
194     when /^user reset/\r
195       return "user reset <what> : resets <what> to the default values. <what> can be +netmasks+ (the list will be emptied), +autologin+ or +login-by-mask+ (will be reset to the default value) or +password+ (a new one will be generated and you'll be told in private)"\r
196     when /^user tell/\r
197       return "user tell <who> the password for <botuser> : contacts <who> in private to tell him/her the password for <botuser>"\r
198     when /^user create/\r
199       return "user create <name> <password> : create botuser named <name> with password <password>. The password can be omitted, in which case a random one will be generated. The <name> should only contain alphanumeric characters and the underscore (_)"\r
200     when /^user list/\r
201       return "user list : lists all the botusers"\r
202     when /^user destroy/\r
203       return "user destroy <botuser> <password> : destroys <botuser>; this function #{Bold}must#{Bold} be called in two steps. On the first call, no password must be specified: <botuser> is then queued for destruction. On the second call, you must specify the correct password for <botuser>, and it will be destroyed. If you want to cancel the destruction, issue the command +user cancel destroy <botuser>+"\r
204     when /^user/\r
205       return "user show, enable|disable, add|rm netmask, set, reset, tell, create, list, destroy"\r
206     else\r
207       return "#{name}: login, whoami, permission syntax, permissions, user"\r
208     end\r
209   end\r
210 \r
211   def need_args(cmd)\r
212     "sorry, I need more arguments to #{cmd}"\r
213   end\r
214 \r
215   def not_args(cmd, *stuff)\r
216     "I can only #{cmd} these: #{stuff.join(', ')}"\r
217   end\r
218 \r
219   def set_prop(botuser, prop, val)\r
220     k = prop.to_s.gsub("-","_")\r
221     botuser.send( (k + "=").to_sym, val)\r
222   end\r
223 \r
224   def reset_prop(botuser, prop)\r
225     k = prop.to_s.gsub("-","_")\r
226     botuser.send( ("reset_"+k).to_sym)\r
227   end\r
228 \r
229   def ask_bool_prop(botuser, prop)\r
230     k = prop.to_s.gsub("-","_")\r
231     botuser.send( (k + "?").to_sym)\r
232   end\r
233 \r
234   def auth_manage_user(m, params)\r
235     splits = params[:data]\r
236 \r
237     cmd = splits.first\r
238     return auth_whoami(m, params) if cmd.nil?\r
239 \r
240     botuser = get_botuser_for(m.source)\r
241     # By default, we do stuff on the botuser the irc user is bound to\r
242     butarget = botuser\r
243 \r
244     has_for = splits[-2] == "for"\r
245     butarget = @bot.auth.get_botuser(splits[-1]) if has_for\r
246     return m.reply("you can't mess with #{butarget.username}") if butarget == @bot.auth.botowner && botuser != butarget\r
247     splits.slice!(-2,2) if has_for\r
248 \r
249     bools = [:autologin, :"login-by-mask"]\r
250     can_set = [:password]\r
251     can_addrm = [:netmasks]\r
252     can_reset = bools + can_set + can_addrm\r
253 \r
254     case cmd.to_sym\r
255 \r
256     when :show\r
257       return "you can't see the properties of #{butarget.username}" if botuser != butarget and !botuser.permit?("auth::show::other")\r
258 \r
259       case splits[1]\r
260       when nil, "all"\r
261         props = can_reset\r
262       when "password"\r
263         if botuser != butarget\r
264           return m.reply("no way I'm telling you the master password!") if butarget == @bot.auth.botowner\r
265           return m.reply("you can't ask for someone else's password")\r
266         end\r
267         return m.reply("c'mon, you can't be asking me seriously to tell you the password in public!") if m.public?\r
268         return m.reply("the password for #{butarget.username} is #{butarget.password}")\r
269       else\r
270         props = splits[1..-1]\r
271       end\r
272 \r
273       str = []\r
274 \r
275       props.each { |arg|\r
276         k = arg.to_sym\r
277         next if k == :password\r
278         case k\r
279         when *bools\r
280           str << "can"\r
281           str.last << "not" unless ask_bool_prop(butarget, k)\r
282           str.last << " #{k}"\r
283         when :netmasks\r
284           str << "knows "\r
285           if butarget.netmasks.empty?\r
286             str.last << "no netmasks"\r
287           else\r
288             str.last << butarget.netmasks.join(", ")\r
289           end\r
290         end\r
291       }\r
292       return m.reply("#{butarget.username} #{str.join('; ')}")\r
293 \r
294     when :enable, :disable\r
295       return m.reply("you can't change the default user") if butarget == @bot.auth.everyone and !botuser.permit?("auth::edit::other::default")\r
296       return m.reply("you can't edit #{butarget.username}") if butarget != botuser and !botuser.permit?("auth::edit::other")\r
297 \r
298       return m.reply(need_args(cmd)) unless splits[1]\r
299       things = []\r
300       skipped = []\r
301       splits[1..-1].each { |a|\r
302         arg = a.to_sym\r
303         if bools.include?(arg)\r
304           set_prop(butarget, arg, cmd.to_sym == :enable)\r
305           things << a\r
306         else\r
307           skipped << a\r
308         end\r
309       }\r
310 \r
311       m.reply "I ignored #{skipped.join(', ')} because " + not_args(cmd, *bools) unless skipped.empty?\r
312       if things.empty?\r
313         m.reply "I haven't changed anything"\r
314       else\r
315         @bot.auth.set_changed\r
316         return auth_manage_user(m, {:data => ["show"] + things + ["for", butarget.username] })\r
317       end\r
318 \r
319     when :set\r
320       return m.reply("you can't change the default user") if butarget == @bot.auth.everyone and !botuser.permit?("auth::edit::default")\r
321       return m.reply("you can't edit #{butarget.username}") if butarget != botuser and !botuser.permit?("auth::edit::other")\r
322 \r
323       return m.reply(need_args(cmd)) unless splits[1]\r
324       arg = splits[1].to_sym\r
325       return m.reply(not_args(cmd, *can_set)) unless can_set.include?(arg)\r
326       argarg = splits[2]\r
327       return m.reply(need_args([cmd, splits[1]].join(" "))) unless argarg\r
328       if arg == :password && m.public?\r
329         return m.reply("is that a joke? setting the password in public?")\r
330       end\r
331       set_prop(butarget, arg, argarg)\r
332       @bot.auth.set_changed\r
333       auth_manage_user(m, {:data => ["show", arg, "for", butarget.username] })\r
334 \r
335     when :reset\r
336       return m.reply("you can't change the default user") if butarget == @bot.auth.everyone and !botuser.permit?("auth::edit::default")\r
337       return m.reply("you can't edit #{butarget.username}") if butarget != botuser and !botuser.permit?("auth::edit::other")\r
338 \r
339       return m.reply(need_args(cmd)) unless splits[1]\r
340       things = []\r
341       skipped = []\r
342       splits[1..-1].each { |a|\r
343         arg = a.to_sym\r
344         if can_reset.include?(arg)\r
345           reset_prop(butarget, arg)\r
346           things << a\r
347         else\r
348           skipped << a\r
349         end\r
350       }\r
351 \r
352       m.reply "I ignored #{skipped.join(', ')} because " + not_args(cmd, *can_reset) unless skipped.empty?\r
353       if things.empty?\r
354         m.reply "I haven't changed anything"\r
355       else\r
356         @bot.auth.set_changed\r
357         @bot.say m.source, "the password for #{butarget.username} is now #{butarget.password}" if things.include?("password")\r
358         return auth_manage_user(m, {:data => (["show"] + things - ["password"]) + ["for", butarget.username]})\r
359       end\r
360 \r
361     when :add, :rm, :remove, :del, :delete\r
362       return m.reply("you can't change the default user") if butarget == @bot.auth.everyone and !botuser.permit?("auth::edit::default")\r
363       return m.reply("you can't edit #{butarget.username}") if butarget != botuser and !botuser.permit?("auth::edit::other")\r
364 \r
365       arg = splits[1]\r
366       if arg.nil? or arg !~ /netmasks?/ or splits[2].nil?\r
367         return m.reply("I can only add/remove netmasks. See +help user add+ for more instructions")\r
368       end\r
369 \r
370       method = cmd.to_sym == :add ? :add_netmask : :delete_netmask\r
371 \r
372       failed = []\r
373 \r
374       splits[2..-1].each { |mask|\r
375         begin\r
376           butarget.send(method, mask.to_irc_netmask(:server => @bot.server))\r
377         rescue\r
378           failed << mask\r
379         end\r
380       }\r
381       m.reply "I failed to #{cmd} #{failed.join(', ')}" unless failed.empty?\r
382       @bot.auth.set_changed\r
383       return auth_manage_user(m, {:data => ["show", "netmasks", "for", butarget.username] })\r
384 \r
385     else\r
386       m.reply "sorry, I don't know how to #{m.message}"\r
387     end\r
388   end\r
389 \r
390   def auth_tell_password(m, params)\r
391     user = params[:user]\r
392     begin\r
393       botuser = @bot.auth.get_botuser(params[:botuser])\r
394     rescue\r
395       return m.reply("coudln't find botuser #{params[:botuser]})")\r
396     end\r
397     m.reply "I'm not telling the master password to anyway, pal" if botuser == @bot.auth.botowner\r
398     msg = "the password for botuser #{botuser.username} is #{botuser.password}"\r
399     @bot.say user, msg\r
400     @bot.say m.source, "I told #{user} that " + msg\r
401   end\r
402 \r
403   def auth_create_user(m, params)\r
404     name = params[:name]\r
405     password = params[:password]\r
406     return m.reply("are you nuts, creating a botuser with a publicly known password?") if m.public? and not password.nil?\r
407     begin\r
408       bu = @bot.auth.create_botuser(name, password)\r
409       @bot.auth.set_changed\r
410     rescue => e\r
411       m.reply "failed to create #{name}: #{e}"\r
412       debug e.inspect + "\n" + e.backtrace.join("\n")\r
413       return\r
414     end\r
415     m.reply "created botuser #{bu.username}"\r
416   end\r
417 \r
418   def auth_list_users(m, params)\r
419     # TODO name regexp to filter results\r
420     list = @bot.auth.save_array.inject([]) { |list, x| list << x[:username] } - ['everyone', 'owner']\r
421     if defined?(@destroy_q)\r
422       list.map! { |x|\r
423         @destroy_q.include?(x) ? x + " (queued for destruction)" : x\r
424       }\r
425     end\r
426     return m.reply("I have no botusers other than the default ones") if list.empty?\r
427     return m.reply("botuser#{'s' if list.length > 1}: #{list.join(', ')}")\r
428   end\r
429 \r
430   def auth_destroy_user(m, params)\r
431     @destroy_q = [] unless defined?(@destroy_q)\r
432     buname = params[:name]\r
433     return m.reply("You can't destroy #{buname}") if ["everyone", "owner"].include?(buname)\r
434     cancel = m.message.split[1] == 'cancel'\r
435     password = params[:password]\r
436 \r
437     buser_array = @bot.auth.save_array\r
438     buser_hash = buser_array.inject({}) { |h, u|\r
439       h[u[:username]] = u\r
440       h\r
441     }\r
442 \r
443     return m.reply("no such botuser #{buname}") unless buser_hash.keys.include?(buname)\r
444 \r
445     if cancel\r
446       if @destroy_q.include?(buname)\r
447         @destroy_q.delete(buname)\r
448         m.reply "#{buname} removed from the destruction queue"\r
449       else\r
450         m.reply "#{buname} was not queued for destruction"\r
451       end\r
452       return\r
453     end\r
454 \r
455     if password.nil?\r
456       if @destroy_q.include?(buname)\r
457         rep = "#{buname} already queued for destruction"\r
458       else\r
459         @destroy_q << buname\r
460         rep = "#{buname} queued for destruction"\r
461       end\r
462       return m.reply(rep + ", use #{Bold}user destroy #{buname} <password>#{Bold} to destroy it")\r
463     else\r
464       begin\r
465         return m.reply("#{buname} is not queued for destruction yet") unless @destroy_q.include?(buname)\r
466         return m.reply("wrong password for #{buname}") unless buser_hash[buname][:password] == password\r
467         buser_array.delete_if { |u|\r
468           u[:username] == buname\r
469         }\r
470         @destroy_q.delete(buname)\r
471         @bot.auth.load_array(buser_array, true)\r
472         @bot.auth.set_changed\r
473       rescue => e\r
474         return m.reply("failed: #{e}")\r
475       end\r
476       return m.reply("botuser #{buname} destroyed")\r
477     end\r
478 \r
479   end\r
480 \r
481   def auth_copy_ren_user(m, params)\r
482     source = Auth::BotUser.sanitize_username(params[:source])\r
483     dest = Auth::BotUser.sanitize_username(params[:dest])\r
484     return m.reply("please don't touch the default users") if (["everyone", "owner"] | [source, dest]).length < 4\r
485 \r
486     buser_array = @bot.auth.save_array\r
487     buser_hash = buser_array.inject({}) { |h, u|\r
488       h[u[:username]] = u\r
489       h\r
490     }\r
491 \r
492     return m.reply("no such botuser #{source}") unless buser_hash.keys.include?(source)\r
493     return m.reply("botuser #{dest} exists already") if buser_hash.keys.include?(dest)\r
494 \r
495     copying = m.message.split[1] == "copy"\r
496     begin\r
497       if copying\r
498         h = buser_hash[source].dup \r
499       else\r
500         h = buser_hash[source]\r
501       end\r
502       h[:username] = dest\r
503       buser_array << h if copying\r
504 \r
505       @bot.auth.load_array(buser_array, true)\r
506       @bot.auth.set_changed\r
507     rescue => e\r
508       return m.reply("failed: #{e}")\r
509     end\r
510     return m.reply("botuser #{source} copied to #{dest}") if copying\r
511     return m.reply("botuser #{source} renamed to #{dest}")\r
512 \r
513   end\r
514 \r
515 end\r
516 \r
517 auth = AuthModule.new\r
518 \r
519 auth.map "user create :name :password",\r
520   :action => 'auth_create_user',\r
521   :defaults => {:password => nil},\r
522   :auth_path => 'user::manage::create!'\r
523 \r
524 auth.map "user cancel destroy :name :password",\r
525   :action => 'auth_destroy_user',\r
526   :defaults => { :password => nil },\r
527   :auth_path => 'user::manage::destroy::cancel!'\r
528 \r
529 auth.map "user destroy :name :password",\r
530   :action => 'auth_destroy_user',\r
531   :defaults => { :password => nil },\r
532   :auth_path => 'user::manage::destroy!'\r
533 \r
534 auth.map "user copy :source :dest",\r
535   :action => 'auth_copy_ren_user',\r
536   :auth_path => 'user::manage::copy!'\r
537 \r
538 auth.map "user rename :source :dest",\r
539   :action => 'auth_copy_ren_user',\r
540   :auth_path => 'user::manage::rename!'\r
541 \r
542 auth.default_auth("user::manage", false)\r
543 \r
544 auth.map "user tell :user the password for :botuser",\r
545   :action => 'auth_tell_password',\r
546   :auth_path => 'user::tell'\r
547 \r
548 auth.map "user list",\r
549   :action => 'auth_list_users',\r
550   :auth_path => 'user::list!'\r
551 \r
552 auth.map "user *data",\r
553   :action => 'auth_manage_user'\r
554 \r
555 auth.default_auth("user", true)\r
556 auth.default_auth("edit::other", false)\r
557 \r
558 auth.map "whoami",\r
559   :action => 'auth_whoami',\r
560   :auth_path => '!*!'\r
561 \r
562 auth.map "login :botuser :password",\r
563   :action => 'auth_login',\r
564   :public => false,\r
565   :defaults => { :password => nil },\r
566   :auth_path => '!login!'\r
567 \r
568 auth.map "login :botuser",\r
569   :action => 'auth_login',\r
570   :auth_path => '!login!'\r
571 \r
572 auth.map "login",\r
573   :action => 'auth_autologin',\r
574   :auth_path => '!login!'\r
575 \r
576 auth.map "permissions set *args for :user",\r
577   :action => 'auth_set',\r
578   :auth_path => ':edit::set:'\r
579 \r
580 auth.map "permissions reset *args for :user",\r
581   :action => 'auth_reset',\r
582   :auth_path => ':edit::reset:'\r
583 \r
584 auth.default_auth('*', false)\r
585 \r