]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - lib/rbot/auth.rb
Lots of fixes all around, in preparation for the new auth coremodule
[user/henk/code/ruby/rbot.git] / lib / rbot / auth.rb
1 module Irc
2
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)
11     return false
12   end
13
14   # check if a string is an actual IRC hostmask
15   def Irc.ismask?(mask)
16     mask =~ /^.+!.+@.+$/
17   end
18
19   Struct.new( 'UserData', :level, :password, :hostmasks )
20
21   # User-level authentication to allow/disallow access to bot commands based
22   # on hostmask and userlevel.
23   class IrcAuth
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' )
30
31     # create a new IrcAuth instance.
32     # bot:: associated bot class
33     def initialize(bot)
34       @bot = bot
35       @users = Hash.new do
36         Struct::UserData.new(@bot.config['auth.default_level'], '', [])
37       end
38       @levels = Hash.new(0)
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)
45         }
46       end
47       if(File.exist?("#{@bot.botclass}/levels.rbot"))
48         IO.foreach("#{@bot.botclass}/levels.rbot") do |line|
49           if(line =~ /\s*(\d+)\s*(\S+)/)
50             level = $1.to_i
51             command = $2
52             @levels[command] = level
53           end
54         end
55       end
56       if @levels.length < 1
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"
58       end
59     end
60
61     # save current users and levels to files.
62     # levels are written to #{botclass}/levels.rbot
63     # users are written to #{botclass}/users.yaml
64     def save
65       Dir.mkdir("#{@bot.botclass}") if(!File.exist?("#{@bot.botclass}"))
66       begin
67         debug "Writing new users.yaml ..."
68         File.open("#{@bot.botclass}/users.yaml.new", 'w') do |file|
69           file.puts @users.to_yaml
70         end
71         debug "Officializing users.yaml ..."
72         File.rename("#{@bot.botclass}/users.yaml.new",
73                     "#{@bot.botclass}/users.yaml")
74       rescue
75         error "failed to write configuration file users.yaml! #{$!}"
76         error "#{e.class}: #{e}"
77         error e.backtrace.join("\n")
78       end
79       begin
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}"
84           end
85         end
86         debug "Officializing levels.rbot ..."
87         File.rename("#{@bot.botclass}/levels.rbot.new",
88                     "#{@bot.botclass}/levels.rbot")
89       rescue
90         error "failed to write configuration file levels.rbot! #{$!}"
91         error "#{e.class}: #{e}"
92         error e.backtrace.join("\n")
93       end
94     end
95
96     # command:: command user wishes to perform
97     # mask::    hostmask of user
98     # tell::    optional recipient for "insufficient auth" message
99     #
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] )
106           return true
107         else
108           debug "#{mask} is not allowed to perform #{command}"
109           @bot.say tell, "insufficient \"#{command}\" auth (have #{auth}, need #{@levels[command]})" if tell
110           return false
111         end
112     end
113
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
117     end
118
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
123     end
124
125     def usermod( username, item, value=nil )
126       if @users.has_key?( username )
127         case item
128           when 'hostmask'
129             if Irc.ismask?( value )
130               @users[username].hostmasks = [ value ]
131               return true
132             end
133           when '+hostmask'
134             if Irc.ismask?( value )
135               @users[username].hostmasks += [ value ]
136               return true
137             end
138           when '-hostmask'
139             if Irc.ismask?( value )
140               @users[username].hostmasks -= [ value ]
141               return true
142             end
143           when 'password'
144               @users[username].password = value
145               return true
146           when 'level'
147               @users[username].level = value.to_i
148               return true
149           else
150             debug "usermod: Tried to modify unknown item #{item}"
151             # @bot.say tell, "Unknown item #{item}" if tell
152         end
153       end
154       return false
155     end
156
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
162     end
163
164     def matchingUser( mask )
165       currentUser = nil
166       currentLevel = 0
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 )
171               currentUser = user
172               currentLevel = data.level
173             end
174           }
175         end
176       }
177       currentUser
178     end
179
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 )
183       return true
184     end
185
186     # return all currently defined commands (for which auth is required) and
187     # their required authlevels
188     def showlevels
189       reply = 'Current levels are:'
190       @levels.sort.each { |key, value|
191         reply += " #{key}(#{value})"
192       }
193       reply
194     end
195
196     # return all currently defined users and their authlevels
197     def showusers
198       reply = 'Current users are:'
199       @users.sort.each { |key, value|
200         reply += " #{key}(#{value.level})"
201       }
202       reply
203     end
204
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}"
210         }
211       end
212       reply
213     end
214
215     # module help
216     def help(topic='')
217       case topic
218         when 'setlevel'
219           return 'setlevel <command> <level> => Sets required level for <command> to <level> (private addressing only)'
220         when 'useradd'
221           return 'useradd <username> => Add user <mask>, you still need to set him up correctly (private addressing only)'
222         when 'userdel'
223           return 'userdel <username> => Remove user <username> (private addressing only)'
224         when 'usermod'
225           return 'usermod <username> <item> <value> => Modify <username>s settings. Valid <item>s are: hostmask, (+|-)hostmask, password, level (private addressing only)'
226         when 'auth'
227           return 'auth <masterpw> => Create a user with your hostmask and master password as bot master (private addressing only)'
228         when 'levels'
229           return 'levels => list commands and their required levels (private addressing only)'
230         when 'users'
231           return 'users [<username>]=> list users and their levels or details about <username> (private addressing only)'
232         when 'whoami'
233           return 'whoami => Show as whom you are recognized (private addressing only)'
234         when 'identify'
235           return 'identify <username> <password> => Identify your hostmask as belonging to <username> (private addressing only)'
236         else
237           return 'Auth module (User authentication) topics: setlevel, useradd, userdel, usermod, auth, levels, users, whoami, identify'
238       end
239     end
240
241     # privmsg handler
242     def privmsg(m)
243      if(m.address? && m.private?)
244       case m.message
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"
249           end
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"
254           end
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"
259           end
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"
264             else
265               m.reply "Failed to set #$2 of #$1 to #$3"
266             end
267           end
268         when( /^setpassword\s+(\S+)/ )
269           password = $1
270           user = @bot.auth.matchingUser( m.source )
271           if user
272             if @bot.auth.usermod(user, 'password', password)
273               m.reply "Your password has been set to #{password}"
274             else
275               m.reply "Couldn't set password"
276             end
277           else
278             m.reply 'You don\'t belong to any user.'
279           end
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 )
284             else
285               @bot.auth.usermod( 'master', '+hostmask', m.source )
286             end
287             m.reply 'Identified, security level maxed out'
288           else
289             m.reply 'Incorrect password'
290           end
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})"
294           else
295             m.reply 'Incorrect username/password'
296           end
297         when( 'whoami' )
298           user = @bot.auth.matchingUser( m.source )
299           if user
300             m.reply "I recognize you as #{user} (#{@users[user].level})"
301           else
302             m.reply 'You don\'t belong to any user.'
303           end
304         when( /^users\s+(\S+)/ )
305           m.reply @bot.auth.showdetails( $1 ) if( @bot.auth.allow?( 'auth', m.source, m.replyto ) )
306         when ( 'levels' )
307           m.reply @bot.auth.showlevels if( @bot.auth.allow?( 'config', m.source, m.replyto ) )
308         when ( 'users' )
309           m.reply @bot.auth.showusers if( @bot.auth.allow?( 'users', m.source, m.replyto ) )
310       end
311      end
312     end
313   end
314 end