3 # globmask:: glob to test with
4 # netmask:: netmask to test against
5 # Compare a netmask with a standard IRC glob, e.g foo!bar@baz.com would
6 # match *!*@baz.com, foo!*@*, *!bar@*, etc.
7 def Irc.netmaskmatch( globmask, netmask )
8 regmask = Regexp.escape( globmask )
9 regmask.gsub!( /\\\*/, '.*' )
10 return true if(netmask =~ /#{regmask}/i)
14 # check if a string is an actual IRC hostmask
19 Struct.new( 'UserData', :level, :password, :hostmasks )
21 # User-level authentication to allow/disallow access to bot commands based
22 # on hostmask and userlevel.
24 BotConfig.register BotConfigStringValue.new( 'auth.password',
25 :default => 'rbotauth', :wizard => true,
26 :desc => 'Your password for maxing your auth with the bot (used to associate new hostmasks with your owner-status etc)' )
27 BotConfig.register BotConfigIntegerValue.new( 'auth.default_level',
28 :default => 10, :wizard => true,
29 :desc => 'The default level for new/unknown users' )
31 # create a new IrcAuth instance.
32 # bot:: associated bot class
36 Struct::UserData.new(@bot.config['auth.default_level'], '', [])
39 @currentUsers = Hash.new( nil )
40 if( File.exist?( "#{@bot.botclass}/users.yaml" ) )
41 File.open( "#{@bot.botclass}/users.yaml" ) { |file|
42 # work around YAML not maintaining the default proc
43 @loadedusers = YAML::parse(file).transform
44 @users.update(@loadedusers)
47 if(File.exist?("#{@bot.botclass}/levels.rbot"))
48 IO.foreach("#{@bot.botclass}/levels.rbot") do |line|
49 if(line =~ /\s*(\d+)\s*(\S+)/)
52 @levels[command] = level
57 raise RuntimeError, "No valid levels.rbot found! If you really want a free-for-all bot and this isn't the result of a previous error, write a proper levels.rbot"
61 # save current users and levels to files.
62 # levels are written to #{botclass}/levels.rbot
63 # users are written to #{botclass}/users.yaml
65 Dir.mkdir("#{@bot.botclass}") if(!File.exist?("#{@bot.botclass}"))
67 debug "Writing new users.yaml ..."
68 File.open("#{@bot.botclass}/users.yaml.new", 'w') do |file|
69 file.puts @users.to_yaml
71 debug "Officializing users.yaml ..."
72 File.rename("#{@bot.botclass}/users.yaml.new",
73 "#{@bot.botclass}/users.yaml")
75 error "failed to write configuration file users.yaml! #{$!}"
76 error "#{e.class}: #{e}"
77 error e.backtrace.join("\n")
80 debug "Writing new levels.rbot ..."
81 File.open("#{@bot.botclass}/levels.rbot.new", 'w') do |file|
82 @levels.each do |key, value|
83 file.puts "#{value} #{key}"
86 debug "Officializing levels.rbot ..."
87 File.rename("#{@bot.botclass}/levels.rbot.new",
88 "#{@bot.botclass}/levels.rbot")
90 error "failed to write configuration file levels.rbot! #{$!}"
91 error "#{e.class}: #{e}"
92 error e.backtrace.join("\n")
96 # command:: command user wishes to perform
97 # mask:: hostmask of user
98 # tell:: optional recipient for "insufficient auth" message
100 # returns true if user with hostmask +mask+ is permitted to perform
101 # +command+ optionally pass tell as the target for the "insufficient auth"
102 # message, if the user is not authorised
103 def allow?( command, mask, tell=nil )
104 auth = @users[matchingUser(mask)].level # Directly using @users[] is possible, because UserData has a default setting
105 if( auth >= @levels[command] )
108 debug "#{mask} is not allowed to perform #{command}"
109 @bot.say tell, "insufficient \"#{command}\" auth (have #{auth}, need #{@levels[command]})" if tell
114 # add user with hostmask matching +mask+ with initial auth level +level+
115 def useradd( username, level=@bot.config['auth.default_level'], password='', hostmask='*!*@*' )
116 @users[username] = Struct::UserData.new( level, password, [hostmask] ) if ! @users.has_key? username
119 # mask:: mask of user to remove
120 # remove user with mask +mask+
121 def userdel( username )
122 @users.delete( username ) if @users.has_key? username
125 def usermod( username, item, value=nil )
126 if @users.has_key?( username )
129 if Irc.ismask?( value )
130 @users[username].hostmasks = [ value ]
134 if Irc.ismask?( value )
135 @users[username].hostmasks += [ value ]
139 if Irc.ismask?( value )
140 @users[username].hostmasks -= [ value ]
144 @users[username].password = value
147 @users[username].level = value.to_i
150 debug "usermod: Tried to modify unknown item #{item}"
151 # @bot.say tell, "Unknown item #{item}" if tell
157 # command:: command to adjust
158 # level:: new auth level for the command
159 # set required auth level of +command+ to +level+
160 def setlevel(command, level)
161 @levels[command] = level
164 def matchingUser( mask )
167 @users.each { |user, data| # TODO Will get easier if YPaths are used...
168 if data.level > currentLevel
169 data.hostmasks.each { |hostmask|
170 if Irc.netmaskmatch( hostmask, mask )
172 currentLevel = data.level
180 def identify( mask, username, password )
181 return false unless @users.has_key?(username) && @users[username].password == password
182 @bot.auth.usermod( username, '+hostmask', mask )
186 # return all currently defined commands (for which auth is required) and
187 # their required authlevels
189 reply = 'Current levels are:'
190 @levels.sort.each { |key, value|
191 reply += " #{key}(#{value})"
196 # return all currently defined users and their authlevels
198 reply = 'Current users are:'
199 @users.sort.each { |key, value|
200 reply += " #{key}(#{value.level})"
205 def showdetails( username )
206 if @users.has_key? username
207 reply = "#{username}(#{@users[username].level}):"
208 @users[username].hostmasks.each { |hostmask|
209 reply += " #{hostmask}"
219 return 'setlevel <command> <level> => Sets required level for <command> to <level> (private addressing only)'
221 return 'useradd <username> => Add user <mask>, you still need to set him up correctly (private addressing only)'
223 return 'userdel <username> => Remove user <username> (private addressing only)'
225 return 'usermod <username> <item> <value> => Modify <username>s settings. Valid <item>s are: hostmask, (+|-)hostmask, password, level (private addressing only)'
227 return 'auth <masterpw> => Create a user with your hostmask and master password as bot master (private addressing only)'
229 return 'levels => list commands and their required levels (private addressing only)'
231 return 'users [<username>]=> list users and their levels or details about <username> (private addressing only)'
233 return 'whoami => Show as whom you are recognized (private addressing only)'
235 return 'identify <username> <password> => Identify your hostmask as belonging to <username> (private addressing only)'
237 return 'Auth module (User authentication) topics: setlevel, useradd, userdel, usermod, auth, levels, users, whoami, identify'
243 if(m.address? && m.private?)
245 when (/^setlevel\s+(\S+)\s+(\d+)$/)
246 if( @bot.auth.allow?( 'auth', m.source, m.replyto ) )
247 @bot.auth.setlevel( $1, $2.to_i )
248 m.reply "level for #$1 set to #$2"
250 when( /^useradd\s+(\S+)/ ) # FIXME Needs review!!! (\s+(\S+)(\s+(\S+)(\s+(\S+))?)?)? Should this part be added to make complete useradds possible?
251 if( @bot.auth.allow?( 'auth', m.source, m.replyto ) )
252 @bot.auth.useradd( $1 )
253 m.reply "added user #$1, please set him up correctly"
255 when( /^userdel\s+(\S+)/ )
256 if( @bot.auth.allow?( 'auth', m.source, m.replyto ) )
257 @bot.auth.userdel( $1 )
258 m.reply "user #$1 is gone"
260 when( /^usermod\s+(\S+)\s+(\S+)\s+(\S+)/ )
261 if( @bot.auth.allow?('auth', m.source, m.replyto ) )
262 if( @bot.auth.usermod( $1, $2, $3 ) )
263 m.reply "Set #$2 of #$1 to #$3"
265 m.reply "Failed to set #$2 of #$1 to #$3"
268 when( /^setpassword\s+(\S+)/ )
270 user = @bot.auth.matchingUser( m.source )
272 if @bot.auth.usermod(user, 'password', password)
273 m.reply "Your password has been set to #{password}"
275 m.reply "Couldn't set password"
278 m.reply 'You don\'t belong to any user.'
280 when (/^auth\s+(\S+)/)
281 if( $1 == @bot.config['auth.password'] )
282 if ! @users.has_key? 'master'
283 @bot.auth.useradd( 'master', 1000, @bot.config['auth.password'], m.source )
285 @bot.auth.usermod( 'master', '+hostmask', m.source )
287 m.reply 'Identified, security level maxed out'
289 m.reply 'Incorrect password'
291 when( /^identify\s+(\S+)\s+(\S+)/ )
292 if @bot.auth.identify( m.source, $1, $2 )
293 m.reply "Identified as #$1 (#{@users[$1].level})"
295 m.reply 'Incorrect username/password'
298 user = @bot.auth.matchingUser( m.source )
300 m.reply "I recognize you as #{user} (#{@users[user].level})"
302 m.reply 'You don\'t belong to any user.'
304 when( /^users\s+(\S+)/ )
305 m.reply @bot.auth.showdetails( $1 ) if( @bot.auth.allow?( 'auth', m.source, m.replyto ) )
307 m.reply @bot.auth.showlevels if( @bot.auth.allow?( 'config', m.source, m.replyto ) )
309 m.reply @bot.auth.showusers if( @bot.auth.allow?( 'users', m.source, m.replyto ) )