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.pretty_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].pretty_inspect}"
\r
30 def load_array(key=:default, forced=false)
\r
31 debug "loading botusers (#{key}): #{@registry[key].pretty_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 "permission 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 topics: show, enable|disable, add|rm netmask, set, reset, tell, create, list, destroy"
\r
281 return "auth <masterpassword>: log in as the bot owner; other commands: login, whoami, permission syntax, permissions [re]set, permissions view, user"
\r
283 return "auth commands: auth, login, whoami, permission[s], user"
\r
288 "sorry, I need more arguments to #{cmd}"
\r
291 def not_args(cmd, *stuff)
\r
292 "I can only #{cmd} these: #{stuff.join(', ')}"
\r
295 def set_prop(botuser, prop, val)
\r
296 k = prop.to_s.gsub("-","_")
\r
297 botuser.send( (k + "=").to_sym, val)
\r
300 def reset_prop(botuser, prop)
\r
301 k = prop.to_s.gsub("-","_")
\r
302 botuser.send( ("reset_"+k).to_sym)
\r
305 def ask_bool_prop(botuser, prop)
\r
306 k = prop.to_s.gsub("-","_")
\r
307 botuser.send( (k + "?").to_sym)
\r
310 def auth_manage_user(m, params)
\r
311 splits = params[:data]
\r
314 return auth_whoami(m, params) if cmd.nil?
\r
316 botuser = get_botuser_for(m.source)
\r
317 # By default, we do stuff on the botuser the irc user is bound to
\r
320 has_for = splits[-2] == "for"
\r
321 butarget = @bot.auth.get_botuser(splits[-1]) if has_for
\r
322 return m.reply("you can't mess with #{butarget.username}") if butarget == @bot.auth.botowner && botuser != butarget
\r
323 splits.slice!(-2,2) if has_for
\r
325 bools = [:autologin, :"login-by-mask"]
\r
326 can_set = [:password]
\r
327 can_addrm = [:netmasks]
\r
328 can_reset = bools + can_set + can_addrm
\r
329 can_show = can_reset + ["perms"]
\r
334 return "you can't see the properties of #{butarget.username}" if botuser != butarget and !botuser.permit?("auth::show::other")
\r
340 if botuser != butarget
\r
341 return m.reply("no way I'm telling you the master password!") if butarget == @bot.auth.botowner
\r
342 return m.reply("you can't ask for someone else's password")
\r
344 return m.reply("c'mon, you can't be asking me seriously to tell you the password in public!") if m.public?
\r
345 return m.reply("the password for #{butarget.username} is #{butarget.password}")
\r
347 props = splits[1..-1]
\r
354 next if k == :password
\r
358 str.last << "not" unless ask_bool_prop(butarget, k)
\r
359 str.last << " #{k}"
\r
362 if butarget.netmasks.empty?
\r
363 str.last << "no netmasks"
\r
365 str.last << butarget.netmasks.join(", ")
\r
369 return m.reply("#{butarget.username} #{str.join('; ')}")
\r
371 when :enable, :disable
\r
372 return m.reply("you can't change the default user") if butarget == @bot.auth.everyone and !botuser.permit?("auth::edit::other::default")
\r
373 return m.reply("you can't edit #{butarget.username}") if butarget != botuser and !botuser.permit?("auth::edit::other")
\r
375 return m.reply(need_args(cmd)) unless splits[1]
\r
378 splits[1..-1].each { |a|
\r
380 if bools.include?(arg)
\r
381 set_prop(butarget, arg, cmd.to_sym == :enable)
\r
388 m.reply "I ignored #{skipped.join(', ')} because " + not_args(cmd, *bools) unless skipped.empty?
\r
390 m.reply "I haven't changed anything"
\r
392 @bot.auth.set_changed
\r
393 return auth_manage_user(m, {:data => ["show"] + things + ["for", butarget.username] })
\r
397 return m.reply("you can't change the default user") if butarget == @bot.auth.everyone and !botuser.permit?("auth::edit::default")
\r
398 return m.reply("you can't edit #{butarget.username}") if butarget != botuser and !botuser.permit?("auth::edit::other")
\r
400 return m.reply(need_args(cmd)) unless splits[1]
\r
401 arg = splits[1].to_sym
\r
402 return m.reply(not_args(cmd, *can_set)) unless can_set.include?(arg)
\r
404 return m.reply(need_args([cmd, splits[1]].join(" "))) unless argarg
\r
405 if arg == :password && m.public?
\r
406 return m.reply("is that a joke? setting the password in public?")
\r
408 set_prop(butarget, arg, argarg)
\r
409 @bot.auth.set_changed
\r
410 auth_manage_user(m, {:data => ["show", arg, "for", butarget.username] })
\r
413 return m.reply("you can't change the default user") if butarget == @bot.auth.everyone and !botuser.permit?("auth::edit::default")
\r
414 return m.reply("you can't edit #{butarget.username}") if butarget != botuser and !botuser.permit?("auth::edit::other")
\r
416 return m.reply(need_args(cmd)) unless splits[1]
\r
419 splits[1..-1].each { |a|
\r
421 if can_reset.include?(arg)
\r
422 reset_prop(butarget, arg)
\r
429 m.reply "I ignored #{skipped.join(', ')} because " + not_args(cmd, *can_reset) unless skipped.empty?
\r
431 m.reply "I haven't changed anything"
\r
433 @bot.auth.set_changed
\r
434 @bot.say m.source, "the password for #{butarget.username} is now #{butarget.password}" if things.include?("password")
\r
435 return auth_manage_user(m, {:data => (["show"] + things - ["password"]) + ["for", butarget.username]})
\r
438 when :add, :rm, :remove, :del, :delete
\r
439 return m.reply("you can't change the default user") if butarget == @bot.auth.everyone and !botuser.permit?("auth::edit::default")
\r
440 return m.reply("you can't edit #{butarget.username}") if butarget != botuser and !botuser.permit?("auth::edit::other")
\r
443 if arg.nil? or arg !~ /netmasks?/ or splits[2].nil?
\r
444 return m.reply("I can only add/remove netmasks. See +help user add+ for more instructions")
\r
447 method = cmd.to_sym == :add ? :add_netmask : :delete_netmask
\r
451 splits[2..-1].each { |mask|
\r
453 butarget.send(method, mask.to_irc_netmask(:server => @bot.server))
\r
458 m.reply "I failed to #{cmd} #{failed.join(', ')}" unless failed.empty?
\r
459 @bot.auth.set_changed
\r
460 return auth_manage_user(m, {:data => ["show", "netmasks", "for", butarget.username] })
\r
463 m.reply "sorry, I don't know how to #{m.message}"
\r
467 def auth_tell_password(m, params)
\r
468 user = params[:user]
\r
470 botuser = @bot.auth.get_botuser(params[:botuser])
\r
472 return m.reply("coudln't find botuser #{params[:botuser]})")
\r
474 m.reply "I'm not telling the master password to anyway, pal" if botuser == @bot.auth.botowner
\r
475 msg = "the password for botuser #{botuser.username} is #{botuser.password}"
\r
477 @bot.say m.source, "I told #{user} that " + msg
\r
480 def auth_create_user(m, params)
\r
481 name = params[:name]
\r
482 password = params[:password]
\r
483 return m.reply("are you nuts, creating a botuser with a publicly known password?") if m.public? and not password.nil?
\r
485 bu = @bot.auth.create_botuser(name, password)
\r
486 @bot.auth.set_changed
\r
488 m.reply "failed to create #{name}: #{e}"
\r
489 debug e.inspect + "\n" + e.backtrace.join("\n")
\r
492 m.reply "created botuser #{bu.username}"
\r
495 def auth_list_users(m, params)
\r
496 # TODO name regexp to filter results
\r
497 list = @bot.auth.save_array.inject([]) { |list, x| list << x[:username] } - ['everyone', 'owner']
\r
498 if defined?(@destroy_q)
\r
500 @destroy_q.include?(x) ? x + " (queued for destruction)" : x
\r
503 return m.reply("I have no botusers other than the default ones") if list.empty?
\r
504 return m.reply("botuser#{'s' if list.length > 1}: #{list.join(', ')}")
\r
507 def auth_destroy_user(m, params)
\r
508 @destroy_q = [] unless defined?(@destroy_q)
\r
509 buname = params[:name]
\r
510 return m.reply("You can't destroy #{buname}") if ["everyone", "owner"].include?(buname)
\r
511 cancel = m.message.split[1] == 'cancel'
\r
512 password = params[:password]
\r
514 buser_array = @bot.auth.save_array
\r
515 buser_hash = buser_array.inject({}) { |h, u|
\r
516 h[u[:username]] = u
\r
520 return m.reply("no such botuser #{buname}") unless buser_hash.keys.include?(buname)
\r
523 if @destroy_q.include?(buname)
\r
524 @destroy_q.delete(buname)
\r
525 m.reply "#{buname} removed from the destruction queue"
\r
527 m.reply "#{buname} was not queued for destruction"
\r
533 if @destroy_q.include?(buname)
\r
534 rep = "#{buname} already queued for destruction"
\r
536 @destroy_q << buname
\r
537 rep = "#{buname} queued for destruction"
\r
539 return m.reply(rep + ", use #{Bold}user destroy #{buname} <password>#{Bold} to destroy it")
\r
542 return m.reply("#{buname} is not queued for destruction yet") unless @destroy_q.include?(buname)
\r
543 return m.reply("wrong password for #{buname}") unless buser_hash[buname][:password] == password
\r
544 buser_array.delete_if { |u|
\r
545 u[:username] == buname
\r
547 @destroy_q.delete(buname)
\r
548 @bot.auth.load_array(buser_array, true)
\r
549 @bot.auth.set_changed
\r
551 return m.reply("failed: #{e}")
\r
553 return m.reply("botuser #{buname} destroyed")
\r
558 def auth_copy_ren_user(m, params)
\r
559 source = Auth::BotUser.sanitize_username(params[:source])
\r
560 dest = Auth::BotUser.sanitize_username(params[:dest])
\r
561 return m.reply("please don't touch the default users") if (["everyone", "owner"] | [source, dest]).length < 4
\r
563 buser_array = @bot.auth.save_array
\r
564 buser_hash = buser_array.inject({}) { |h, u|
\r
565 h[u[:username]] = u
\r
569 return m.reply("no such botuser #{source}") unless buser_hash.keys.include?(source)
\r
570 return m.reply("botuser #{dest} exists already") if buser_hash.keys.include?(dest)
\r
572 copying = m.message.split[1] == "copy"
\r
576 buser_hash[source].each { |k, val|
\r
580 h = buser_hash[source]
\r
582 h[:username] = dest
\r
583 buser_array << h if copying
\r
585 @bot.auth.load_array(buser_array, true)
\r
586 @bot.auth.set_changed
\r
588 return m.reply("failed: #{e}")
\r
590 return m.reply("botuser #{source} copied to #{dest}") if copying
\r
591 return m.reply("botuser #{source} renamed to #{dest}")
\r
595 def auth_export(m, params)
\r
597 exportfile = "#{@bot.botclass}/new-auth.users"
\r
599 what = params[:things]
\r
601 has_to = what[-2] == "to"
\r
603 exportfile = "#{@bot.botclass}/#{what[-1]}"
\r
609 m.reply "selecting data to export ..."
\r
611 buser_array = @bot.auth.save_array
\r
612 buser_hash = buser_array.inject({}) { |h, u|
\r
613 h[u[:username]] = u
\r
618 we_want = buser_hash
\r
620 we_want = buser_hash.delete_if { |key, val|
\r
621 not what.include?(key)
\r
625 m.reply "preparing data for export ..."
\r
628 we_want.each { |k, val|
\r
635 yaml_hash[k][kk] = []
\r
637 yaml_hash[k][kk] << {
\r
638 :fullform => nm.fullform,
\r
639 :casemap => nm.casemap.to_s
\r
643 yaml_hash[k][kk] = v
\r
648 m.reply "failed to prepare data: #{e}"
\r
649 debug e.backtrace.dup.unshift(e.inspect).join("\n")
\r
653 m.reply "exporting to #{exportfile} ..."
\r
655 # m.reply yaml_hash.inspect
\r
656 File.open(exportfile, "w") do |file|
\r
657 file.puts YAML::dump(yaml_hash)
\r
660 m.reply "failed to export users: #{e}"
\r
661 debug e.backtrace.dup.unshift(e.inspect).join("\n")
\r
667 def auth_import(m, params)
\r
669 importfile = "#{@bot.botclass}/new-auth.users"
\r
671 what = params[:things]
\r
673 has_from = what[-2] == "from"
\r
675 importfile = "#{@bot.botclass}/#{what[-1]}"
\r
681 m.reply "reading #{importfile} ..."
\r
683 yaml_hash = YAML::load_file(importfile)
\r
685 m.reply "failed to import from: #{e}"
\r
686 debug e.backtrace.dup.unshift(e.inspect).join("\n")
\r
690 # m.reply yaml_hash.inspect
\r
692 m.reply "selecting data to import ..."
\r
695 we_want = yaml_hash
\r
697 we_want = yaml_hash.delete_if { |key, val|
\r
698 not what.include?(key)
\r
702 m.reply "parsing data from import ..."
\r
707 yaml_hash.each { |k, val|
\r
708 buser_hash[k] = { :username => k }
\r
712 buser_hash[k][kk] = []
\r
714 buser_hash[k][kk] << nm[:fullform].to_irc_netmask(:casemap => nm[:casemap].to_irc_casemap).to_irc_netmask(:server => @bot.server)
\r
717 buser_hash[k][kk] = v
\r
722 m.reply "failed to parse data: #{e}"
\r
723 debug e.backtrace.dup.unshift(e.inspect).join("\n")
\r
727 # m.reply buser_hash.inspect
\r
729 org_buser_array = @bot.auth.save_array
\r
730 org_buser_hash = org_buser_array.inject({}) { |h, u|
\r
731 h[u[:username]] = u
\r
735 # TODO we may want to do a(n optional) key-by-key merge
\r
737 org_buser_hash.merge!(buser_hash)
\r
738 new_buser_array = org_buser_hash.values
\r
739 @bot.auth.load_array(new_buser_array, true)
\r
740 @bot.auth.set_changed
\r
747 auth = AuthModule.new
\r
749 auth.map "user export *things",
\r
750 :action => 'auth_export',
\r
751 :defaults => { :things => ['all'] },
\r
752 :auth_path => ':manage:fedex:'
\r
754 auth.map "user import *things",
\r
755 :action => 'auth_import',
\r
756 :auth_path => ':manage:fedex:'
\r
758 auth.map "user create :name :password",
\r
759 :action => 'auth_create_user',
\r
760 :defaults => {:password => nil},
\r
761 :auth_path => ':manage:'
\r
763 auth.map "user [cancel] destroy :name :password",
\r
764 :action => 'auth_destroy_user',
\r
765 :defaults => { :password => nil },
\r
766 :auth_path => ':manage::destroy:'
\r
768 auth.map "user copy :source [to] :dest",
\r
769 :action => 'auth_copy_ren_user',
\r
770 :auth_path => ':manage:'
\r
772 auth.map "user rename :source [to] :dest",
\r
773 :action => 'auth_copy_ren_user',
\r
774 :auth_path => ':manage:'
\r
776 auth.default_auth("user::manage", false)
\r
778 auth.map "user tell :user the password for :botuser",
\r
779 :action => 'auth_tell_password',
\r
782 auth.map "user list",
\r
783 :action => 'auth_list_users',
\r
786 auth.map "user *data",
\r
787 :action => 'auth_manage_user'
\r
789 auth.default_auth("user", true)
\r
790 auth.default_auth("edit::other", false)
\r
793 :action => 'auth_whoami',
\r
794 :auth_path => '!*!'
\r
796 auth.map "auth :password",
\r
797 :action => 'auth_auth',
\r
799 :auth_path => '!login!'
\r
801 auth.map "login :botuser :password",
\r
802 :action => 'auth_login',
\r
804 :defaults => { :password => nil },
\r
805 :auth_path => '!login!'
\r
807 auth.map "login :botuser",
\r
808 :action => 'auth_login',
\r
809 :auth_path => '!login!'
\r
812 :action => 'auth_autologin',
\r
813 :auth_path => '!login!'
\r
815 auth.map "permissions set *args",
\r
816 :action => 'auth_edit_perm',
\r
817 :auth_path => ':edit::set:'
\r
819 auth.map "permissions reset *args",
\r
820 :action => 'auth_edit_perm',
\r
821 :auth_path => ':edit::reset:'
\r
823 auth.map "permissions view [for :user]",
\r
824 :action => 'auth_view_perm',
\r
827 auth.default_auth('*', false)
\r