Merge tag 'release_3_0_12' into branch moonshot-fr-3.0.12-upgrade.
[freeradius.git] / src / modules / rlm_python / prepaid.py
1 #! /usr/bin/env python
2 #
3 # Example Python module for prepaid usage using MySQL
4
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 2 of the License, or
8 # (at your option) any later version.
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software
15 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
16 #
17 # Copyright 2002 Miguel A.L. Paraz <mparaz@mparaz.com>
18 # Copyright 2002 Imperium Technology, Inc.
19 #
20 # $Id$
21
22 import radiusd
23 import MySQLdb
24
25 # Configuration
26 configDb = 'python'                     # Database name
27 configHost = 'localhost'                # Database host
28 configUser = 'python'                   # Database user and password
29 configPasswd = 'python'
30
31 # xxx Database
32
33 # Globals
34 dbHandle = None
35
36 def log(level, s):
37   """Log function."""
38   radiusd.radlog(level, 'prepaid.py: ' + s)
39
40 def instantiate(p):
41   """Module Instantiation.  0 for success, -1 for failure.
42   p is a dummy variable here."""
43   global dbHandle
44
45   p = p
46
47   try:
48     dbHandle = MySQLdb.connect(db=configDb, host=configHost,
49                                user=configUser, passwd=configPasswd)
50
51   except MySQLdb.OperationalError, e:
52     # Report the error and return -1 for failure.
53     # xxx A more advanced module would retry the database.
54     log(radiusd.L_ERR, str(e))
55     return -1
56
57   log(radiusd.L_INFO, 'db connection: ' + str(dbHandle))
58
59   return 0
60
61
62 def authorize(authData):
63   """Authorization and authentication are done in one step."""
64
65   # Extract the data we need.
66   userName = None
67   userPasswd = None
68
69   for t in authData:
70     if t[0] == 'User-Name':
71       userName = t[1]
72     elif t[0] == 'Password':
73       userPasswd = t[1]
74
75   # Build and log the SQL statement
76   # radiusd puts double quotes (") around the string representation of
77   # the RADIUS packet.
78   sql = 'select passwd, maxseconds from users where username = ' +  userName
79
80   log(radiusd.L_DBG, sql)
81
82   # Get a cursor
83   # xxx Or should this be one cursor all throughout?
84   try:
85     dbCursor = dbHandle.cursor()
86   except MySQLdb.OperationalError, e:
87     log(radiusd.L_ERR, str(e))
88     return radiusd.RLM_MODULE_FAIL
89
90   # Execute the SQL statement
91   try:
92     dbCursor.execute(sql)
93   except MySQLdb.OperationalError, e:
94     log(radiusd.L_ERR, str(e))
95     dbCursor.close()
96     return radiusd.RLM_MODULE_FAIL
97
98   # Get the result. (passwd, maxseconds)
99   result = dbCursor.fetchone()
100   if not result:
101     # User not found
102     log(radiusd.L_INFO, 'user not found: ' + userName)
103     dbCursor.close()
104     return radiusd.RLM_MODULE_NOTFOUND
105
106
107
108   # Compare passwords
109   # Ignore the quotes around userPasswd.
110   if result[0] != userPasswd[1:-1]:
111     log(radiusd.L_DBG, 'user password mismatch: ' + userName)
112     return radiusd.RLM_MODULE_REJECT
113
114   maxSeconds = result[1]
115
116   # Compute their session limit
117
118   # Build and log the SQL statement
119   sql = 'select sum(seconds) from sessions where username = ' + userName
120
121   log(radiusd.L_DBG, sql)
122
123   # Execute the SQL statement
124   try:
125     dbCursor.execute(sql)
126   except MySQLdb.OperationalError, e:
127     log(radiusd.L_ERR, str(e))
128     dbCursor.close()
129     return radiusd.RLM_MODULE_FAIL
130
131   # Get the result. (sum,)
132   result = dbCursor.fetchone()
133   if (not result) or (not result[0]):
134     # No usage yet
135     secondsUsed = 0
136   else:
137     secondsUsed = result[0]
138
139   # Done with cursor
140   dbCursor.close()
141
142   # Note that MySQL returns the result of SUM() as a float.
143   sessionTimeout = maxSeconds - int(secondsUsed)
144
145   if sessionTimeout <= 0:
146     # No more time, reject outright
147     log(radiusd.L_INFO, 'user out of time: ' + userName)
148     return radiusd.RLM_MODULE_REJECT
149
150   # Log the success
151   log(radiusd.L_DBG, 'user accepted: %s, %d seconds' %
152       (userName, sessionTimeout))
153
154   # We are adding to the RADIUS packet
155   # Note that the session timeout integer must be converted to string.
156   # We need to set an Auth-Type.
157
158   return (radiusd.RLM_MODULE_UPDATED,
159           (('Session-Timeout', str(sessionTimeout)),),
160           (('Auth-Type', 'python'),))
161   # If you want to use different operators
162   # you can do
163   # return (radiusd.RLM_MODULE_UPDATED,
164   #         (
165   #            ('Session-Timeout', ':=', str(sessionTimeout)),
166   #            ('Some-other-option', '-=', Value'),
167   #         ),
168   #         (
169   #            ('Auth-Type', ':=', 'python'),
170   #         ),
171   #        )
172
173 def authenticate(p):
174   p = p
175   return radiusd.RLM_MODULE_OK
176
177
178 def preacct(p):
179   p = p
180   return radiusd.RLM_MODULE_OK
181
182
183 def accounting(acctData):
184   """Accounting."""
185   # Extract the data we need.
186
187   userName = None
188   acctSessionTime = None
189   acctStatusType = None
190
191   # xxx A dict would make this nice.
192   for t in acctData:
193     if t[0] == 'User-Name':
194       userName = t[1]
195     elif t[0] == 'Acct-Session-Time':
196       acctSessionTime = t[1]
197     elif t[0] == 'Acct-Status-Type':
198       acctStatusType = t[1]
199
200
201   # We will not deal with Start for now.
202   # We may later, for simultaneous checks and the like.
203   if acctStatusType == 'Start':
204     return radiusd.RLM_MODULE_OK
205
206   # Build and log the SQL statement
207   # radiusd puts double quotes (") around the string representation of
208   # the RADIUS packet.
209   #
210   # xxx This is simplistic as it does not record the time, etc.
211   #
212   sql = 'insert into sessions (username, seconds) values (%s, %d)' % \
213         (userName, int(acctSessionTime))
214
215   log(radiusd.L_DBG, sql)
216
217   # Get a cursor
218   # xxx Or should this be one cursor all throughout?
219   try:
220     dbCursor = dbHandle.cursor()
221   except MySQLdb.OperationalError, e:
222     log(radiusd.L_ERR, str(e))
223     return radiusd.RLM_MODULE_FAIL
224
225   # Execute the SQL statement
226   try:
227     dbCursor.execute(sql)
228   except MySQLdb.OperationalError, e:
229     log(radiusd.L_ERR, str(e))
230     dbCursor.close()
231     return radiusd.RLM_MODULE_FAIL
232
233
234   return radiusd.RLM_MODULE_OK
235
236
237 def detach():
238   """Detach and clean up."""
239   # Shut down the database connection.
240   global dbHandle
241   log(radiusd.L_DBG, 'closing database handle: ' + str(dbHandle))
242   dbHandle.close()
243
244   return radiusd.RLM_MODULE_OK
245
246
247
248 # Test the modules
249 if __name__ == '__main__':
250   instantiate(None)
251   print authorize((('User-Name', '"map"'), ('User-Password', '"abc"')))