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