5 class AuthModule < CoreBotModule
\r
9 load_array(:default, true)
\r
10 debug "initialized auth. Botusers: #{@bot.auth.save_array.inspect}"
\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
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
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
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
40 def parse_args(ar, setting)
\r
45 next_must_be_chan = false
\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
53 next_must_be_chan = true if x == "on"
\r
56 if "+-".include?(x[0])
\r
57 warns << ArgumentError.new("please do not use + or - in front of command #{x} when resetting") unless setting
\r
59 warns << ArgumentError.new("+ or - expected in front of #{x}") if setting
\r
62 else # parse locations
\r
68 case next_must_be_chan
\r
70 locs << x.gsub(/^here$/,'_').gsub(/^private$/,'?')
\r
72 warns << ArgumentError("#{x} doesn't look like a channel name") unless @bot.server.supports[:chantypes].include?(x[0])
\r
81 warns << "trailing comma" if want_more
\r
82 warns << "you probably forgot a comma" unless last_idx == ar.length - 1
\r
83 return cmds, locs, warns
\r
86 def auth_edit_perm(m, params)
\r
88 setting = m.message.split[1] == "set"
\r
89 splits = params[:args]
\r
91 has_for = splits[-2] == "for"
\r
92 return usage unless has_for
\r
95 user = @bot.auth.get_botuser(splits[-1].sub(/^all$/,"everyone"))
\r
97 return m.reply("couldn't find botuser #{splits[-1]}")
\r
99 return m.reply("you can't change permissions for #{user.username}") if user == @bot.auth.botowner
\r
100 splits.slice!(-2,2) if has_for
\r
102 cmds, locs, warns = parse_args(splits, setting)
\r
103 errs = warns.select { |w| w.kind_of?(Exception) }
\r
106 m.reply "couldn't satisfy your request: #{errs.join(',')}"
\r
117 ch = "?" if loc == "_"
\r
119 ch = m.target.to_s if loc == "_"
\r
121 cmds.each { |setval|
\r
123 val = setval[0].chr == '+'
\r
124 cmd = setval[1..-1]
\r
125 user.set_permission(cmd, val, ch)
\r
128 user.reset_permission(cmd, ch)
\r
133 m.reply "something went wrong while trying to set the permissions"
\r
136 @bot.auth.set_changed
\r
137 debug "user #{user} permissions changed"
\r
141 def auth_view_perm(m, params)
\r
143 user = @bot.auth.get_botuser(params[:user].sub(/^all$/,"everyone"))
\r
145 return m.reply("couldn't find botuser #{params[:user]}")
\r
149 perm.each { |k, val|
\r
150 next if val.perm.empty?
\r
153 str << "on any channel: "
\r
155 str << "in private: "
\r
160 val.perm.each { |cmd, bool|
\r
161 sub << (bool ? "+" : "-")
\r
162 sub.last << cmd.to_s
\r
164 str.last << sub.join(', ')
\r
167 m.reply "no permissions set for #{user.username}"
\r
169 m.reply "permissions for #{user.username}:: #{str.join('; ')}"
\r
173 def get_botuser_for(user)
\r
174 @bot.auth.irc_to_botuser(user)
\r
177 def get_botusername_for(user)
\r
178 get_botuser_for(user).username
\r
182 "welcome, #{get_botusername_for(user)}"
\r
185 def auth_login(m, params)
\r
187 case @bot.auth.login(m.source, params[:botuser], params[:password])
\r
189 m.reply welcome(m.source)
\r
190 @bot.auth.set_changed
\r
192 m.reply "sorry, can't do"
\r
195 m.reply "couldn't login: #{e}"
\r
200 def auth_autologin(m, params)
\r
201 u = do_autologin(m.source)
\r
204 m.reply "I couldn't find anything to let you login automatically"
\r
206 m.reply welcome(m.source)
\r
210 def do_autologin(user)
\r
211 @bot.auth.autologin(user)
\r
214 def auth_whoami(m, params)
\r
217 # rep << m.source.nick << ", "
\r
220 rep << get_botusername_for(m.source).gsub(/^everyone$/, "no one that I know").gsub(/^owner$/, "my boss")
\r
224 def help(cmd, topic="")
\r
227 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
229 return "whoami: names the botuser you're linked to"
\r
233 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
235 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
240 return "user show <what> : shows info about the user; <what> can be any of autologin, login-by-mask, netmasks"
\r
241 when /^(en|dis)able/
\r
242 return "user enable|disable <what> : turns on or off <what> (autologin, login-by-mask)"
\r
244 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
246 return "user add|rm netmask <mask> : adds/removes netmask <mask> from the list of netmasks known to the botuser you're linked to"
\r
248 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
250 return "user tell <who> the password for <botuser> : contacts <who> in private to tell him/her the password for <botuser>"
\r
252 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
254 return "user list : lists all the botusers"
\r
256 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
258 return "user show, enable|disable, add|rm netmask, set, reset, tell, create, list, destroy"
\r
261 return "#{name}: login, whoami, permission syntax, permissions, user"
\r
266 "sorry, I need more arguments to #{cmd}"
\r
269 def not_args(cmd, *stuff)
\r
270 "I can only #{cmd} these: #{stuff.join(', ')}"
\r
273 def set_prop(botuser, prop, val)
\r
274 k = prop.to_s.gsub("-","_")
\r
275 botuser.send( (k + "=").to_sym, val)
\r
278 def reset_prop(botuser, prop)
\r
279 k = prop.to_s.gsub("-","_")
\r
280 botuser.send( ("reset_"+k).to_sym)
\r
283 def ask_bool_prop(botuser, prop)
\r
284 k = prop.to_s.gsub("-","_")
\r
285 botuser.send( (k + "?").to_sym)
\r
288 def auth_manage_user(m, params)
\r
289 splits = params[:data]
\r
292 return auth_whoami(m, params) if cmd.nil?
\r
294 botuser = get_botuser_for(m.source)
\r
295 # By default, we do stuff on the botuser the irc user is bound to
\r
298 has_for = splits[-2] == "for"
\r
299 butarget = @bot.auth.get_botuser(splits[-1]) if has_for
\r
300 return m.reply("you can't mess with #{butarget.username}") if butarget == @bot.auth.botowner && botuser != butarget
\r
301 splits.slice!(-2,2) if has_for
\r
303 bools = [:autologin, :"login-by-mask"]
\r
304 can_set = [:password]
\r
305 can_addrm = [:netmasks]
\r
306 can_reset = bools + can_set + can_addrm
\r
307 can_show = can_reset + ["perms"]
\r
312 return "you can't see the properties of #{butarget.username}" if botuser != butarget and !botuser.permit?("auth::show::other")
\r
318 if botuser != butarget
\r
319 return m.reply("no way I'm telling you the master password!") if butarget == @bot.auth.botowner
\r
320 return m.reply("you can't ask for someone else's password")
\r
322 return m.reply("c'mon, you can't be asking me seriously to tell you the password in public!") if m.public?
\r
323 return m.reply("the password for #{butarget.username} is #{butarget.password}")
\r
325 props = splits[1..-1]
\r
332 next if k == :password
\r
336 str.last << "not" unless ask_bool_prop(butarget, k)
\r
337 str.last << " #{k}"
\r
340 if butarget.netmasks.empty?
\r
341 str.last << "no netmasks"
\r
343 str.last << butarget.netmasks.join(", ")
\r
347 return m.reply("#{butarget.username} #{str.join('; ')}")
\r
349 when :enable, :disable
\r
350 return m.reply("you can't change the default user") if butarget == @bot.auth.everyone and !botuser.permit?("auth::edit::other::default")
\r
351 return m.reply("you can't edit #{butarget.username}") if butarget != botuser and !botuser.permit?("auth::edit::other")
\r
353 return m.reply(need_args(cmd)) unless splits[1]
\r
356 splits[1..-1].each { |a|
\r
358 if bools.include?(arg)
\r
359 set_prop(butarget, arg, cmd.to_sym == :enable)
\r
366 m.reply "I ignored #{skipped.join(', ')} because " + not_args(cmd, *bools) unless skipped.empty?
\r
368 m.reply "I haven't changed anything"
\r
370 @bot.auth.set_changed
\r
371 return auth_manage_user(m, {:data => ["show"] + things + ["for", butarget.username] })
\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
378 return m.reply(need_args(cmd)) unless splits[1]
\r
379 arg = splits[1].to_sym
\r
380 return m.reply(not_args(cmd, *can_set)) unless can_set.include?(arg)
\r
382 return m.reply(need_args([cmd, splits[1]].join(" "))) unless argarg
\r
383 if arg == :password && m.public?
\r
384 return m.reply("is that a joke? setting the password in public?")
\r
386 set_prop(butarget, arg, argarg)
\r
387 @bot.auth.set_changed
\r
388 auth_manage_user(m, {:data => ["show", arg, "for", butarget.username] })
\r
391 return m.reply("you can't change the default user") if butarget == @bot.auth.everyone and !botuser.permit?("auth::edit::default")
\r
392 return m.reply("you can't edit #{butarget.username}") if butarget != botuser and !botuser.permit?("auth::edit::other")
\r
394 return m.reply(need_args(cmd)) unless splits[1]
\r
397 splits[1..-1].each { |a|
\r
399 if can_reset.include?(arg)
\r
400 reset_prop(butarget, arg)
\r
407 m.reply "I ignored #{skipped.join(', ')} because " + not_args(cmd, *can_reset) unless skipped.empty?
\r
409 m.reply "I haven't changed anything"
\r
411 @bot.auth.set_changed
\r
412 @bot.say m.source, "the password for #{butarget.username} is now #{butarget.password}" if things.include?("password")
\r
413 return auth_manage_user(m, {:data => (["show"] + things - ["password"]) + ["for", butarget.username]})
\r
416 when :add, :rm, :remove, :del, :delete
\r
417 return m.reply("you can't change the default user") if butarget == @bot.auth.everyone and !botuser.permit?("auth::edit::default")
\r
418 return m.reply("you can't edit #{butarget.username}") if butarget != botuser and !botuser.permit?("auth::edit::other")
\r
421 if arg.nil? or arg !~ /netmasks?/ or splits[2].nil?
\r
422 return m.reply("I can only add/remove netmasks. See +help user add+ for more instructions")
\r
425 method = cmd.to_sym == :add ? :add_netmask : :delete_netmask
\r
429 splits[2..-1].each { |mask|
\r
431 butarget.send(method, mask.to_irc_netmask(:server => @bot.server))
\r
436 m.reply "I failed to #{cmd} #{failed.join(', ')}" unless failed.empty?
\r
437 @bot.auth.set_changed
\r
438 return auth_manage_user(m, {:data => ["show", "netmasks", "for", butarget.username] })
\r
441 m.reply "sorry, I don't know how to #{m.message}"
\r
445 def auth_tell_password(m, params)
\r
446 user = params[:user]
\r
448 botuser = @bot.auth.get_botuser(params[:botuser])
\r
450 return m.reply("coudln't find botuser #{params[:botuser]})")
\r
452 m.reply "I'm not telling the master password to anyway, pal" if botuser == @bot.auth.botowner
\r
453 msg = "the password for botuser #{botuser.username} is #{botuser.password}"
\r
455 @bot.say m.source, "I told #{user} that " + msg
\r
458 def auth_create_user(m, params)
\r
459 name = params[:name]
\r
460 password = params[:password]
\r
461 return m.reply("are you nuts, creating a botuser with a publicly known password?") if m.public? and not password.nil?
\r
463 bu = @bot.auth.create_botuser(name, password)
\r
464 @bot.auth.set_changed
\r
466 m.reply "failed to create #{name}: #{e}"
\r
467 debug e.inspect + "\n" + e.backtrace.join("\n")
\r
470 m.reply "created botuser #{bu.username}"
\r
473 def auth_list_users(m, params)
\r
474 # TODO name regexp to filter results
\r
475 list = @bot.auth.save_array.inject([]) { |list, x| list << x[:username] } - ['everyone', 'owner']
\r
476 if defined?(@destroy_q)
\r
478 @destroy_q.include?(x) ? x + " (queued for destruction)" : x
\r
481 return m.reply("I have no botusers other than the default ones") if list.empty?
\r
482 return m.reply("botuser#{'s' if list.length > 1}: #{list.join(', ')}")
\r
485 def auth_destroy_user(m, params)
\r
486 @destroy_q = [] unless defined?(@destroy_q)
\r
487 buname = params[:name]
\r
488 return m.reply("You can't destroy #{buname}") if ["everyone", "owner"].include?(buname)
\r
489 cancel = m.message.split[1] == 'cancel'
\r
490 password = params[:password]
\r
492 buser_array = @bot.auth.save_array
\r
493 buser_hash = buser_array.inject({}) { |h, u|
\r
494 h[u[:username]] = u
\r
498 return m.reply("no such botuser #{buname}") unless buser_hash.keys.include?(buname)
\r
501 if @destroy_q.include?(buname)
\r
502 @destroy_q.delete(buname)
\r
503 m.reply "#{buname} removed from the destruction queue"
\r
505 m.reply "#{buname} was not queued for destruction"
\r
511 if @destroy_q.include?(buname)
\r
512 rep = "#{buname} already queued for destruction"
\r
514 @destroy_q << buname
\r
515 rep = "#{buname} queued for destruction"
\r
517 return m.reply(rep + ", use #{Bold}user destroy #{buname} <password>#{Bold} to destroy it")
\r
520 return m.reply("#{buname} is not queued for destruction yet") unless @destroy_q.include?(buname)
\r
521 return m.reply("wrong password for #{buname}") unless buser_hash[buname][:password] == password
\r
522 buser_array.delete_if { |u|
\r
523 u[:username] == buname
\r
525 @destroy_q.delete(buname)
\r
526 @bot.auth.load_array(buser_array, true)
\r
527 @bot.auth.set_changed
\r
529 return m.reply("failed: #{e}")
\r
531 return m.reply("botuser #{buname} destroyed")
\r
536 def auth_copy_ren_user(m, params)
\r
537 source = Auth::BotUser.sanitize_username(params[:source])
\r
538 dest = Auth::BotUser.sanitize_username(params[:dest])
\r
539 return m.reply("please don't touch the default users") if (["everyone", "owner"] | [source, dest]).length < 4
\r
541 buser_array = @bot.auth.save_array
\r
542 buser_hash = buser_array.inject({}) { |h, u|
\r
543 h[u[:username]] = u
\r
547 return m.reply("no such botuser #{source}") unless buser_hash.keys.include?(source)
\r
548 return m.reply("botuser #{dest} exists already") if buser_hash.keys.include?(dest)
\r
550 copying = m.message.split[1] == "copy"
\r
554 buser_hash[source].each { |k, val|
\r
558 h = buser_hash[source]
\r
560 h[:username] = dest
\r
561 buser_array << h if copying
\r
563 @bot.auth.load_array(buser_array, true)
\r
564 @bot.auth.set_changed
\r
566 return m.reply("failed: #{e}")
\r
568 return m.reply("botuser #{source} copied to #{dest}") if copying
\r
569 return m.reply("botuser #{source} renamed to #{dest}")
\r
573 def auth_export(m, params)
\r
575 exportfile = "#{@bot.botclass}/new-auth.users"
\r
577 what = params[:things]
\r
579 has_to = what[-2] == "to"
\r
581 exportfile = "#{@bot.botclass}/#{what[-1]}"
\r
587 m.reply "selecting data to export ..."
\r
589 buser_array = @bot.auth.save_array
\r
590 buser_hash = buser_array.inject({}) { |h, u|
\r
591 h[u[:username]] = u
\r
596 we_want = buser_hash
\r
598 we_want = buser_hash.delete_if { |key, val|
\r
599 not what.include?(key)
\r
603 m.reply "preparing data for export ..."
\r
606 we_want.each { |k, val|
\r
613 yaml_hash[k][kk] = []
\r
615 yaml_hash[k][kk] << {
\r
616 :fullform => nm.fullform,
\r
617 :casemap => nm.casemap.to_s
\r
621 yaml_hash[k][kk] = v
\r
626 m.reply "failed to prepare data: #{e}"
\r
627 debug e.backtrace.dup.unshift(e.inspect).join("\n")
\r
631 m.reply "exporting to #{exportfile} ..."
\r
633 # m.reply yaml_hash.inspect
\r
634 File.open(exportfile, "w") do |file|
\r
635 file.puts YAML::dump(yaml_hash)
\r
638 m.reply "failed to export users: #{e}"
\r
639 debug e.backtrace.dup.unshift(e.inspect).join("\n")
\r
645 def auth_import(m, params)
\r
647 importfile = "#{@bot.botclass}/new-auth.users"
\r
649 what = params[:things]
\r
651 has_from = what[-2] == "from"
\r
653 importfile = "#{@bot.botclass}/#{what[-1]}"
\r
659 m.reply "reading #{importfile} ..."
\r
661 yaml_hash = YAML::load_file(importfile)
\r
663 m.reply "failed to import from: #{e}"
\r
664 debug e.backtrace.dup.unshift(e.inspect).join("\n")
\r
668 # m.reply yaml_hash.inspect
\r
670 m.reply "selecting data to import ..."
\r
673 we_want = yaml_hash
\r
675 we_want = yaml_hash.delete_if { |key, val|
\r
676 not what.include?(key)
\r
680 m.reply "parsing data from import ..."
\r
685 yaml_hash.each { |k, val|
\r
686 buser_hash[k] = { :username => k }
\r
690 buser_hash[k][kk] = []
\r
692 buser_hash[k][kk] << nm[:fullform].to_irc_netmask(:casemap => nm[:casemap].to_irc_casemap).to_irc_netmask(:server => @bot.server)
\r
695 buser_hash[k][kk] = v
\r
700 m.reply "failed to parse data: #{e}"
\r
701 debug e.backtrace.dup.unshift(e.inspect).join("\n")
\r
705 # m.reply buser_hash.inspect
\r
707 org_buser_array = @bot.auth.save_array
\r
708 org_buser_hash = org_buser_array.inject({}) { |h, u|
\r
709 h[u[:username]] = u
\r
713 # TODO we may want to do a(n optional) key-by-key merge
\r
715 org_buser_hash.merge!(buser_hash)
\r
716 new_buser_array = org_buser_hash.values
\r
717 @bot.auth.load_array(new_buser_array, true)
\r
718 @bot.auth.set_changed
\r
725 auth = AuthModule.new
\r
727 auth.map "user export *things",
\r
728 :action => 'auth_export',
\r
729 :defaults => { :things => ['all'] },
\r
730 :auth_path => ':manage:fedex:'
\r
732 auth.map "user import *things",
\r
733 :action => 'auth_import',
\r
734 :auth_path => ':manage:fedex:'
\r
736 auth.map "user create :name :password",
\r
737 :action => 'auth_create_user',
\r
738 :defaults => {:password => nil},
\r
739 :auth_path => ':manage:'
\r
741 auth.map "user cancel destroy :name :password",
\r
742 :action => 'auth_destroy_user',
\r
743 :defaults => { :password => nil },
\r
744 :auth_path => ':manage::destroy:'
\r
746 auth.map "user destroy :name :password",
\r
747 :action => 'auth_destroy_user',
\r
748 :defaults => { :password => nil },
\r
749 :auth_path => ':manage:'
\r
751 auth.map "user copy :source :dest",
\r
752 :action => 'auth_copy_ren_user',
\r
753 :auth_path => ':manage:'
\r
755 auth.map "user rename :source :dest",
\r
756 :action => 'auth_copy_ren_user',
\r
757 :auth_path => ':manage:'
\r
759 auth.default_auth("user::manage", false)
\r
761 auth.map "user tell :user the password for :botuser",
\r
762 :action => 'auth_tell_password',
\r
765 auth.map "user list",
\r
766 :action => 'auth_list_users',
\r
769 auth.map "user *data",
\r
770 :action => 'auth_manage_user'
\r
772 auth.default_auth("user", true)
\r
773 auth.default_auth("edit::other", false)
\r
776 :action => 'auth_whoami',
\r
777 :auth_path => '!*!'
\r
779 auth.map "login :botuser :password",
\r
780 :action => 'auth_login',
\r
782 :defaults => { :password => nil },
\r
783 :auth_path => '!login!'
\r
785 auth.map "login :botuser",
\r
786 :action => 'auth_login',
\r
787 :auth_path => '!login!'
\r
790 :action => 'auth_autologin',
\r
791 :auth_path => '!login!'
\r
793 auth.map "permissions set *args",
\r
794 :action => 'auth_edit_perm',
\r
795 :auth_path => ':edit::set:'
\r
797 auth.map "permissions reset *args",
\r
798 :action => 'auth_edit_perm',
\r
799 :auth_path => ':edit::reset:'
\r
801 auth.map "permissions view for :user",
\r
802 :action => 'auth_view_perm',
\r
805 auth.default_auth('*', false)
\r