4 # :title: rbot auth management from IRC
\r
6 # Author:: Giuseppe "Oblomov" Bilotta <giuseppe.bilotta@gmail.com>
\r
7 # Copyright:: (C) 2006,2007 Giuseppe Bilotta
\r
10 class AuthModule < CoreBotModule
\r
14 load_array(:default, true)
\r
15 debug "initialized auth. Botusers: #{@bot.auth.save_array.inspect}"
\r
22 def save_array(key=:default)
\r
23 if @bot.auth.changed?
\r
24 @registry[key] = @bot.auth.save_array
\r
25 @bot.auth.reset_changed
\r
26 debug "saved botusers (#{key}): #{@registry[key].inspect}"
\r
30 def load_array(key=:default, forced=false)
\r
31 debug "loading botusers (#{key}): #{@registry[key].inspect}"
\r
32 @bot.auth.load_array(@registry[key], forced) if @registry.has_key?(key)
\r
35 # The permission parameters accept arguments with the following syntax:
\r
36 # cmd_path... [on #chan .... | in here | in private]
\r
37 # This auxiliary method scans the array _ar_ to see if it matches
\r
38 # the given syntax: it expects + or - signs in front of _cmd_path_
\r
39 # elements when _setting_ = true
\r
41 # It returns an array whose first element is the array of cmd_path,
\r
42 # the second element is an array of locations and third an array of
\r
43 # warnings occurred while parsing the strings
\r
45 def parse_args(ar, setting)
\r
50 next_must_be_chan = false
\r
53 ar.each_with_index { |x, i|
\r
54 if doing_cmds # parse cmd_path
\r
55 # check if the list is done
\r
56 if x == "on" or x == "in"
\r
58 next_must_be_chan = true if x == "on"
\r
61 if "+-".include?(x[0])
\r
62 warns << ArgumentError.new("please do not use + or - in front of command #{x} when resetting") unless setting
\r
64 warns << ArgumentError.new("+ or - expected in front of #{x}") if setting
\r
67 else # parse locations
\r
73 case next_must_be_chan
\r
75 locs << x.gsub(/^here$/,'_').gsub(/^private$/,'?')
\r
77 warns << ArgumentError("#{x} doesn't look like a channel name") unless @bot.server.supports[:chantypes].include?(x[0])
\r
86 warns << "trailing comma" if want_more
\r
87 warns << "you probably forgot a comma" unless last_idx == ar.length - 1
\r
88 return cmds, locs, warns
\r
91 def auth_edit_perm(m, params)
\r
93 setting = m.message.split[1] == "set"
\r
94 splits = params[:args]
\r
96 has_for = splits[-2] == "for"
\r
97 return usage(m) unless has_for
\r
100 user = @bot.auth.get_botuser(splits[-1].sub(/^all$/,"everyone"))
\r
102 return m.reply("couldn't find botuser #{splits[-1]}")
\r
104 return m.reply("you can't change permissions for #{user.username}") if user == @bot.auth.botowner
\r
105 splits.slice!(-2,2) if has_for
\r
107 cmds, locs, warns = parse_args(splits, setting)
\r
108 errs = warns.select { |w| w.kind_of?(Exception) }
\r
111 m.reply "couldn't satisfy your request: #{errs.join(',')}"
\r
122 ch = "?" if loc == "_"
\r
124 ch = m.target.to_s if loc == "_"
\r
126 cmds.each { |setval|
\r
128 val = setval[0].chr == '+'
\r
129 cmd = setval[1..-1]
\r
130 user.set_permission(cmd, val, ch)
\r
133 user.reset_permission(cmd, ch)
\r
138 m.reply "something went wrong while trying to set the permissions"
\r
141 @bot.auth.set_changed
\r
142 debug "user #{user} permissions changed"
\r
146 def auth_view_perm(m, params)
\r
148 if params[:user].nil?
\r
149 user = get_botusername_for(m.source)
\r
150 return m.reply("you are owner, you can do anything") if user == @bot.auth.botwoner
\r
152 user = @bot.auth.get_botuser(params[:user].sub(/^all$/,"everyone"))
\r
153 return m.reply("owner can do anything") if user.username == "owner"
\r
156 return m.reply("couldn't find botuser #{params[:user]}")
\r
160 perm.each { |k, val|
\r
161 next if val.perm.empty?
\r
164 str << "on any channel: "
\r
166 str << "in private: "
\r
171 val.perm.each { |cmd, bool|
\r
172 sub << (bool ? "+" : "-")
\r
173 sub.last << cmd.to_s
\r
175 str.last << sub.join(', ')
\r
178 m.reply "no permissions set for #{user.username}"
\r
180 m.reply "permissions for #{user.username}:: #{str.join('; ')}"
\r
184 def get_botuser_for(user)
\r
185 @bot.auth.irc_to_botuser(user)
\r
188 def get_botusername_for(user)
\r
189 get_botuser_for(user).username
\r
193 "welcome, #{get_botusername_for(user)}"
\r
196 def auth_auth(m, params)
\r
197 params[:botuser] = 'owner'
\r
198 auth_login(m,params)
\r
201 def auth_login(m, params)
\r
203 case @bot.auth.login(m.source, params[:botuser], params[:password])
\r
205 m.reply welcome(m.source)
\r
206 @bot.auth.set_changed
\r
208 m.reply "sorry, can't do"
\r
211 m.reply "couldn't login: #{e}"
\r
216 def auth_autologin(m, params)
\r
217 u = do_autologin(m.source)
\r
220 m.reply "I couldn't find anything to let you login automatically"
\r
222 m.reply welcome(m.source)
\r
226 def do_autologin(user)
\r
227 @bot.auth.autologin(user)
\r
230 def auth_whoami(m, params)
\r
233 # rep << m.source.nick << ", "
\r
236 rep << get_botusername_for(m.source).gsub(/^everyone$/, "no one that I know").gsub(/^owner$/, "my boss")
\r
240 def help(cmd, topic="")
\r
243 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
245 return "whoami: names the botuser you're linked to"
\r
249 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
250 when "set", "reset", "[re]set", "(re)set"
\r
251 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
253 return "permissions view [for <user>]: display the permissions for user <user>"
\r
255 return "topics: syntax, (re)set, view"
\r
260 return "user show <what> : shows info about the user; <what> can be any of autologin, login-by-mask, netmasks"
\r
261 when /^(en|dis)able/
\r
262 return "user enable|disable <what> : turns on or off <what> (autologin, login-by-mask)"
\r
264 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
266 return "user add|rm netmask <mask> : adds/removes netmask <mask> from the list of netmasks known to the botuser you're linked to"
\r
268 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
270 return "user tell <who> the password for <botuser> : contacts <who> in private to tell him/her the password for <botuser>"
\r
272 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
274 return "user list : lists all the botusers"
\r
276 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
278 return "user show, enable|disable, add|rm netmask, set, reset, tell, create, list, destroy"
\r
281 return "#{name}: login, whoami, permission syntax, permissions [re]set, permissions view, user"
\r
286 "sorry, I need more arguments to #{cmd}"
\r
289 def not_args(cmd, *stuff)
\r
290 "I can only #{cmd} these: #{stuff.join(', ')}"
\r
293 def set_prop(botuser, prop, val)
\r
294 k = prop.to_s.gsub("-","_")
\r
295 botuser.send( (k + "=").to_sym, val)
\r
298 def reset_prop(botuser, prop)
\r
299 k = prop.to_s.gsub("-","_")
\r
300 botuser.send( ("reset_"+k).to_sym)
\r
303 def ask_bool_prop(botuser, prop)
\r
304 k = prop.to_s.gsub("-","_")
\r
305 botuser.send( (k + "?").to_sym)
\r
308 def auth_manage_user(m, params)
\r
309 splits = params[:data]
\r
312 return auth_whoami(m, params) if cmd.nil?
\r
314 botuser = get_botuser_for(m.source)
\r
315 # By default, we do stuff on the botuser the irc user is bound to
\r
318 has_for = splits[-2] == "for"
\r
319 butarget = @bot.auth.get_botuser(splits[-1]) if has_for
\r
320 return m.reply("you can't mess with #{butarget.username}") if butarget == @bot.auth.botowner && botuser != butarget
\r
321 splits.slice!(-2,2) if has_for
\r
323 bools = [:autologin, :"login-by-mask"]
\r
324 can_set = [:password]
\r
325 can_addrm = [:netmasks]
\r
326 can_reset = bools + can_set + can_addrm
\r
327 can_show = can_reset + ["perms"]
\r
332 return "you can't see the properties of #{butarget.username}" if botuser != butarget and !botuser.permit?("auth::show::other")
\r
338 if botuser != butarget
\r
339 return m.reply("no way I'm telling you the master password!") if butarget == @bot.auth.botowner
\r
340 return m.reply("you can't ask for someone else's password")
\r
342 return m.reply("c'mon, you can't be asking me seriously to tell you the password in public!") if m.public?
\r
343 return m.reply("the password for #{butarget.username} is #{butarget.password}")
\r
345 props = splits[1..-1]
\r
352 next if k == :password
\r
356 str.last << "not" unless ask_bool_prop(butarget, k)
\r
357 str.last << " #{k}"
\r
360 if butarget.netmasks.empty?
\r
361 str.last << "no netmasks"
\r
363 str.last << butarget.netmasks.join(", ")
\r
367 return m.reply("#{butarget.username} #{str.join('; ')}")
\r
369 when :enable, :disable
\r
370 return m.reply("you can't change the default user") if butarget == @bot.auth.everyone and !botuser.permit?("auth::edit::other::default")
\r
371 return m.reply("you can't edit #{butarget.username}") if butarget != botuser and !botuser.permit?("auth::edit::other")
\r
373 return m.reply(need_args(cmd)) unless splits[1]
\r
376 splits[1..-1].each { |a|
\r
378 if bools.include?(arg)
\r
379 set_prop(butarget, arg, cmd.to_sym == :enable)
\r
386 m.reply "I ignored #{skipped.join(', ')} because " + not_args(cmd, *bools) unless skipped.empty?
\r
388 m.reply "I haven't changed anything"
\r
390 @bot.auth.set_changed
\r
391 return auth_manage_user(m, {:data => ["show"] + things + ["for", butarget.username] })
\r
395 return m.reply("you can't change the default user") if butarget == @bot.auth.everyone and !botuser.permit?("auth::edit::default")
\r
396 return m.reply("you can't edit #{butarget.username}") if butarget != botuser and !botuser.permit?("auth::edit::other")
\r
398 return m.reply(need_args(cmd)) unless splits[1]
\r
399 arg = splits[1].to_sym
\r
400 return m.reply(not_args(cmd, *can_set)) unless can_set.include?(arg)
\r
402 return m.reply(need_args([cmd, splits[1]].join(" "))) unless argarg
\r
403 if arg == :password && m.public?
\r
404 return m.reply("is that a joke? setting the password in public?")
\r
406 set_prop(butarget, arg, argarg)
\r
407 @bot.auth.set_changed
\r
408 auth_manage_user(m, {:data => ["show", arg, "for", butarget.username] })
\r
411 return m.reply("you can't change the default user") if butarget == @bot.auth.everyone and !botuser.permit?("auth::edit::default")
\r
412 return m.reply("you can't edit #{butarget.username}") if butarget != botuser and !botuser.permit?("auth::edit::other")
\r
414 return m.reply(need_args(cmd)) unless splits[1]
\r
417 splits[1..-1].each { |a|
\r
419 if can_reset.include?(arg)
\r
420 reset_prop(butarget, arg)
\r
427 m.reply "I ignored #{skipped.join(', ')} because " + not_args(cmd, *can_reset) unless skipped.empty?
\r
429 m.reply "I haven't changed anything"
\r
431 @bot.auth.set_changed
\r
432 @bot.say m.source, "the password for #{butarget.username} is now #{butarget.password}" if things.include?("password")
\r
433 return auth_manage_user(m, {:data => (["show"] + things - ["password"]) + ["for", butarget.username]})
\r
436 when :add, :rm, :remove, :del, :delete
\r
437 return m.reply("you can't change the default user") if butarget == @bot.auth.everyone and !botuser.permit?("auth::edit::default")
\r
438 return m.reply("you can't edit #{butarget.username}") if butarget != botuser and !botuser.permit?("auth::edit::other")
\r
441 if arg.nil? or arg !~ /netmasks?/ or splits[2].nil?
\r
442 return m.reply("I can only add/remove netmasks. See +help user add+ for more instructions")
\r
445 method = cmd.to_sym == :add ? :add_netmask : :delete_netmask
\r
449 splits[2..-1].each { |mask|
\r
451 butarget.send(method, mask.to_irc_netmask(:server => @bot.server))
\r
456 m.reply "I failed to #{cmd} #{failed.join(', ')}" unless failed.empty?
\r
457 @bot.auth.set_changed
\r
458 return auth_manage_user(m, {:data => ["show", "netmasks", "for", butarget.username] })
\r
461 m.reply "sorry, I don't know how to #{m.message}"
\r
465 def auth_tell_password(m, params)
\r
466 user = params[:user]
\r
468 botuser = @bot.auth.get_botuser(params[:botuser])
\r
470 return m.reply("coudln't find botuser #{params[:botuser]})")
\r
472 m.reply "I'm not telling the master password to anyway, pal" if botuser == @bot.auth.botowner
\r
473 msg = "the password for botuser #{botuser.username} is #{botuser.password}"
\r
475 @bot.say m.source, "I told #{user} that " + msg
\r
478 def auth_create_user(m, params)
\r
479 name = params[:name]
\r
480 password = params[:password]
\r
481 return m.reply("are you nuts, creating a botuser with a publicly known password?") if m.public? and not password.nil?
\r
483 bu = @bot.auth.create_botuser(name, password)
\r
484 @bot.auth.set_changed
\r
486 m.reply "failed to create #{name}: #{e}"
\r
487 debug e.inspect + "\n" + e.backtrace.join("\n")
\r
490 m.reply "created botuser #{bu.username}"
\r
493 def auth_list_users(m, params)
\r
494 # TODO name regexp to filter results
\r
495 list = @bot.auth.save_array.inject([]) { |list, x| list << x[:username] } - ['everyone', 'owner']
\r
496 if defined?(@destroy_q)
\r
498 @destroy_q.include?(x) ? x + " (queued for destruction)" : x
\r
501 return m.reply("I have no botusers other than the default ones") if list.empty?
\r
502 return m.reply("botuser#{'s' if list.length > 1}: #{list.join(', ')}")
\r
505 def auth_destroy_user(m, params)
\r
506 @destroy_q = [] unless defined?(@destroy_q)
\r
507 buname = params[:name]
\r
508 return m.reply("You can't destroy #{buname}") if ["everyone", "owner"].include?(buname)
\r
509 cancel = m.message.split[1] == 'cancel'
\r
510 password = params[:password]
\r
512 buser_array = @bot.auth.save_array
\r
513 buser_hash = buser_array.inject({}) { |h, u|
\r
514 h[u[:username]] = u
\r
518 return m.reply("no such botuser #{buname}") unless buser_hash.keys.include?(buname)
\r
521 if @destroy_q.include?(buname)
\r
522 @destroy_q.delete(buname)
\r
523 m.reply "#{buname} removed from the destruction queue"
\r
525 m.reply "#{buname} was not queued for destruction"
\r
531 if @destroy_q.include?(buname)
\r
532 rep = "#{buname} already queued for destruction"
\r
534 @destroy_q << buname
\r
535 rep = "#{buname} queued for destruction"
\r
537 return m.reply(rep + ", use #{Bold}user destroy #{buname} <password>#{Bold} to destroy it")
\r
540 return m.reply("#{buname} is not queued for destruction yet") unless @destroy_q.include?(buname)
\r
541 return m.reply("wrong password for #{buname}") unless buser_hash[buname][:password] == password
\r
542 buser_array.delete_if { |u|
\r
543 u[:username] == buname
\r
545 @destroy_q.delete(buname)
\r
546 @bot.auth.load_array(buser_array, true)
\r
547 @bot.auth.set_changed
\r
549 return m.reply("failed: #{e}")
\r
551 return m.reply("botuser #{buname} destroyed")
\r
556 def auth_copy_ren_user(m, params)
\r
557 source = Auth::BotUser.sanitize_username(params[:source])
\r
558 dest = Auth::BotUser.sanitize_username(params[:dest])
\r
559 return m.reply("please don't touch the default users") if (["everyone", "owner"] | [source, dest]).length < 4
\r
561 buser_array = @bot.auth.save_array
\r
562 buser_hash = buser_array.inject({}) { |h, u|
\r
563 h[u[:username]] = u
\r
567 return m.reply("no such botuser #{source}") unless buser_hash.keys.include?(source)
\r
568 return m.reply("botuser #{dest} exists already") if buser_hash.keys.include?(dest)
\r
570 copying = m.message.split[1] == "copy"
\r
574 buser_hash[source].each { |k, val|
\r
578 h = buser_hash[source]
\r
580 h[:username] = dest
\r
581 buser_array << h if copying
\r
583 @bot.auth.load_array(buser_array, true)
\r
584 @bot.auth.set_changed
\r
586 return m.reply("failed: #{e}")
\r
588 return m.reply("botuser #{source} copied to #{dest}") if copying
\r
589 return m.reply("botuser #{source} renamed to #{dest}")
\r
593 def auth_export(m, params)
\r
595 exportfile = "#{@bot.botclass}/new-auth.users"
\r
597 what = params[:things]
\r
599 has_to = what[-2] == "to"
\r
601 exportfile = "#{@bot.botclass}/#{what[-1]}"
\r
607 m.reply "selecting data to export ..."
\r
609 buser_array = @bot.auth.save_array
\r
610 buser_hash = buser_array.inject({}) { |h, u|
\r
611 h[u[:username]] = u
\r
616 we_want = buser_hash
\r
618 we_want = buser_hash.delete_if { |key, val|
\r
619 not what.include?(key)
\r
623 m.reply "preparing data for export ..."
\r
626 we_want.each { |k, val|
\r
633 yaml_hash[k][kk] = []
\r
635 yaml_hash[k][kk] << {
\r
636 :fullform => nm.fullform,
\r
637 :casemap => nm.casemap.to_s
\r
641 yaml_hash[k][kk] = v
\r
646 m.reply "failed to prepare data: #{e}"
\r
647 debug e.backtrace.dup.unshift(e.inspect).join("\n")
\r
651 m.reply "exporting to #{exportfile} ..."
\r
653 # m.reply yaml_hash.inspect
\r
654 File.open(exportfile, "w") do |file|
\r
655 file.puts YAML::dump(yaml_hash)
\r
658 m.reply "failed to export users: #{e}"
\r
659 debug e.backtrace.dup.unshift(e.inspect).join("\n")
\r
665 def auth_import(m, params)
\r
667 importfile = "#{@bot.botclass}/new-auth.users"
\r
669 what = params[:things]
\r
671 has_from = what[-2] == "from"
\r
673 importfile = "#{@bot.botclass}/#{what[-1]}"
\r
679 m.reply "reading #{importfile} ..."
\r
681 yaml_hash = YAML::load_file(importfile)
\r
683 m.reply "failed to import from: #{e}"
\r
684 debug e.backtrace.dup.unshift(e.inspect).join("\n")
\r
688 # m.reply yaml_hash.inspect
\r
690 m.reply "selecting data to import ..."
\r
693 we_want = yaml_hash
\r
695 we_want = yaml_hash.delete_if { |key, val|
\r
696 not what.include?(key)
\r
700 m.reply "parsing data from import ..."
\r
705 yaml_hash.each { |k, val|
\r
706 buser_hash[k] = { :username => k }
\r
710 buser_hash[k][kk] = []
\r
712 buser_hash[k][kk] << nm[:fullform].to_irc_netmask(:casemap => nm[:casemap].to_irc_casemap).to_irc_netmask(:server => @bot.server)
\r
715 buser_hash[k][kk] = v
\r
720 m.reply "failed to parse data: #{e}"
\r
721 debug e.backtrace.dup.unshift(e.inspect).join("\n")
\r
725 # m.reply buser_hash.inspect
\r
727 org_buser_array = @bot.auth.save_array
\r
728 org_buser_hash = org_buser_array.inject({}) { |h, u|
\r
729 h[u[:username]] = u
\r
733 # TODO we may want to do a(n optional) key-by-key merge
\r
735 org_buser_hash.merge!(buser_hash)
\r
736 new_buser_array = org_buser_hash.values
\r
737 @bot.auth.load_array(new_buser_array, true)
\r
738 @bot.auth.set_changed
\r
745 auth = AuthModule.new
\r
747 auth.map "user export *things",
\r
748 :action => 'auth_export',
\r
749 :defaults => { :things => ['all'] },
\r
750 :auth_path => ':manage:fedex:'
\r
752 auth.map "user import *things",
\r
753 :action => 'auth_import',
\r
754 :auth_path => ':manage:fedex:'
\r
756 auth.map "user create :name :password",
\r
757 :action => 'auth_create_user',
\r
758 :defaults => {:password => nil},
\r
759 :auth_path => ':manage:'
\r
761 auth.map "user [cancel] destroy :name :password",
\r
762 :action => 'auth_destroy_user',
\r
763 :defaults => { :password => nil },
\r
764 :auth_path => ':manage::destroy:'
\r
766 auth.map "user copy :source [to] :dest",
\r
767 :action => 'auth_copy_ren_user',
\r
768 :auth_path => ':manage:'
\r
770 auth.map "user rename :source [to] :dest",
\r
771 :action => 'auth_copy_ren_user',
\r
772 :auth_path => ':manage:'
\r
774 auth.default_auth("user::manage", false)
\r
776 auth.map "user tell :user the password for :botuser",
\r
777 :action => 'auth_tell_password',
\r
780 auth.map "user list",
\r
781 :action => 'auth_list_users',
\r
784 auth.map "user *data",
\r
785 :action => 'auth_manage_user'
\r
787 auth.default_auth("user", true)
\r
788 auth.default_auth("edit::other", false)
\r
791 :action => 'auth_whoami',
\r
792 :auth_path => '!*!'
\r
794 auth.map "auth :password",
\r
795 :action => 'auth_auth',
\r
797 :auth_path => '!login!'
\r
799 auth.map "login :botuser :password",
\r
800 :action => 'auth_login',
\r
802 :defaults => { :password => nil },
\r
803 :auth_path => '!login!'
\r
805 auth.map "login :botuser",
\r
806 :action => 'auth_login',
\r
807 :auth_path => '!login!'
\r
810 :action => 'auth_autologin',
\r
811 :auth_path => '!login!'
\r
813 auth.map "permissions set *args",
\r
814 :action => 'auth_edit_perm',
\r
815 :auth_path => ':edit::set:'
\r
817 auth.map "permissions reset *args",
\r
818 :action => 'auth_edit_perm',
\r
819 :auth_path => ':edit::reset:'
\r
821 auth.map "permissions view [for :user]",
\r
822 :action => 'auth_view_perm',
\r
825 auth.default_auth('*', false)
\r