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
58 # save current users and levels to files.
59 # levels are written to #{botclass}/levels.rbot
60 # users are written to #{botclass}/users.yaml
62 Dir.mkdir("#{@bot.botclass}") if(!File.exist?("#{@bot.botclass}"))
64 debug "Writing new users.yaml ..."
65 File.open("#{@bot.botclass}/users.yaml.new", 'w') do |file|
66 file.puts @users.to_yaml
68 debug "Officializing users.yaml ..."
69 File.rename("#{@bot.botclass}/users.yaml.new",
70 "#{@bot.botclass}/users.yaml")
72 error "failed to write configuration file users.yaml! #{$!}"
73 error "#{e.class}: #{e}"
74 error e.backtrace.join("\n")
77 debug "Writing new levels.rbot ..."
78 File.open("#{@bot.botclass}/levels.rbot.new", 'w') do |file|
79 @levels.each do |key, value|
80 file.puts "#{value} #{key}"
83 debug "Officializing levels.rbot ..."
84 File.rename("#{@bot.botclass}/levels.rbot.new",
85 "#{@bot.botclass}/levels.rbot")
87 error "failed to write configuration file levels.rbot! #{$!}"
88 error "#{e.class}: #{e}"
89 error e.backtrace.join("\n")
93 # command:: command user wishes to perform
94 # mask:: hostmask of user
95 # tell:: optional recipient for "insufficient auth" message
97 # returns true if user with hostmask +mask+ is permitted to perform
98 # +command+ optionally pass tell as the target for the "insufficient auth"
99 # message, if the user is not authorised
100 def allow?( command, mask, tell=nil )
101 auth = @users[matchingUser(mask)].level # Directly using @users[] is possible, because UserData has a default setting
102 if( auth >= @levels[command] )
105 debug "#{mask} is not allowed to perform #{command}"
106 @bot.say tell, "insufficient \"#{command}\" auth (have #{auth}, need #{@levels[command]})" if tell
111 # add user with hostmask matching +mask+ with initial auth level +level+
112 def useradd( username, level=@bot.config['auth.default_level'], password='', hostmask='*!*@*' )
113 @users[username] = Struct::UserData.new( level, password, [hostmask] ) if ! @users.has_key? username
116 # mask:: mask of user to remove
117 # remove user with mask +mask+
118 def userdel( username )
119 @users.delete( username ) if @users.has_key? username
122 def usermod( username, item, value=nil )
123 if @users.has_key?( username )
126 if Irc.ismask?( value )
127 @users[username].hostmasks = [ value ]
131 if Irc.ismask?( value )
132 @users[username].hostmasks += [ value ]
136 if Irc.ismask?( value )
137 @users[username].hostmasks -= [ value ]
141 @users[username].password = value
144 @users[username].level = value.to_i
147 debug "usermod: Tried to modify unknown item #{item}"
148 # @bot.say tell, "Unknown item #{item}" if tell
154 # command:: command to adjust
155 # level:: new auth level for the command
156 # set required auth level of +command+ to +level+
157 def setlevel(command, level)
158 @levels[command] = level
161 def matchingUser( mask )
164 @users.each { |user, data| # TODO Will get easier if YPaths are used...
165 if data.level > currentLevel
166 data.hostmasks.each { |hostmask|
167 if Irc.netmaskmatch( hostmask, mask )
169 currentLevel = data.level
177 def identify( mask, username, password )
178 return false unless @users.has_key?(username) && @users[username].password == password
179 @bot.auth.usermod( username, '+hostmask', mask )
183 # return all currently defined commands (for which auth is required) and
184 # their required authlevels
186 reply = 'Current levels are:'
187 @levels.sort.each { |key, value|
188 reply += " #{key}(#{value})"
193 # return all currently defined users and their authlevels
195 reply = 'Current users are:'
196 @users.sort.each { |key, value|
197 reply += " #{key}(#{value.level})"
202 def showdetails( username )
203 if @users.has_key? username
204 reply = "#{username}(#{@users[username].level}):"
205 @users[username].hostmasks.each { |hostmask|
206 reply += " #{hostmask}"
216 return 'setlevel <command> <level> => Sets required level for <command> to <level> (private addressing only)'
218 return 'useradd <username> => Add user <mask>, you still need to set him up correctly (private addressing only)'
220 return 'userdel <username> => Remove user <username> (private addressing only)'
222 return 'usermod <username> <item> <value> => Modify <username>s settings. Valid <item>s are: hostmask, (+|-)hostmask, password, level (private addressing only)'
224 return 'auth <masterpw> => Create a user with your hostmask and master password as bot master (private addressing only)'
226 return 'levels => list commands and their required levels (private addressing only)'
228 return 'users [<username>]=> list users and their levels or details about <username> (private addressing only)'
230 return 'whoami => Show as whom you are recognized (private addressing only)'
232 return 'identify <username> <password> => Identify your hostmask as belonging to <username> (private addressing only)'
234 return 'Auth module (User authentication) topics: setlevel, useradd, userdel, usermod, auth, levels, users, whoami, identify'
240 if(m.address? && m.private?)
242 when (/^setlevel\s+(\S+)\s+(\d+)$/)
243 if( @bot.auth.allow?( 'auth', m.source, m.replyto ) )
244 @bot.auth.setlevel( $1, $2.to_i )
245 m.reply "level for #$1 set to #$2"
247 when( /^useradd\s+(\S+)/ ) # FIXME Needs review!!! (\s+(\S+)(\s+(\S+)(\s+(\S+))?)?)? Should this part be added to make complete useradds possible?
248 if( @bot.auth.allow?( 'auth', m.source, m.replyto ) )
249 @bot.auth.useradd( $1 )
250 m.reply "added user #$1, please set him up correctly"
252 when( /^userdel\s+(\S+)/ )
253 if( @bot.auth.allow?( 'auth', m.source, m.replyto ) )
254 @bot.auth.userdel( $1 )
255 m.reply "user #$1 is gone"
257 when( /^usermod\s+(\S+)\s+(\S+)\s+(\S+)/ )
258 if( @bot.auth.allow?('auth', m.source, m.replyto ) )
259 if( @bot.auth.usermod( $1, $2, $3 ) )
260 m.reply "Set #$2 of #$1 to #$3"
262 m.reply "Failed to set #$2 of #$1 to #$3"
265 when( /^setpassword\s+(\S+)/ )
267 user = @bot.auth.matchingUser( m.source )
269 if @bot.auth.usermod(user, 'password', password)
270 m.reply "Your password has been set to #{password}"
272 m.reply "Couldn't set password"
275 m.reply 'You don\'t belong to any user.'
277 when (/^auth\s+(\S+)/)
278 if( $1 == @bot.config['auth.password'] )
279 if ! @users.has_key? 'master'
280 @bot.auth.useradd( 'master', 1000, @bot.config['auth.password'], m.source )
282 @bot.auth.usermod( 'master', '+hostmask', m.source )
284 m.reply 'Identified, security level maxed out'
286 m.reply 'Incorrect password'
288 when( /^identify\s+(\S+)\s+(\S+)/ )
289 if @bot.auth.identify( m.source, $1, $2 )
290 m.reply "Identified as #$1 (#{@users[$1].level})"
292 m.reply 'Incorrect username/password'
295 user = @bot.auth.matchingUser( m.source )
297 m.reply "I recognize you as #{user} (#{@users[user].level})"
299 m.reply 'You don\'t belong to any user.'
301 when( /^users\s+(\S+)/ )
302 m.reply @bot.auth.showdetails( $1 ) if( @bot.auth.allow?( 'auth', m.source, m.replyto ) )
304 m.reply @bot.auth.showlevels if( @bot.auth.allow?( 'config', m.source, m.replyto ) )
306 m.reply @bot.auth.showusers if( @bot.auth.allow?( 'users', m.source, m.replyto ) )