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