/[shmookey]/portconf/pyaaa/pyaaa.py


UCC Code Repository

Contents of /portconf/pyaaa/pyaaa.py

Parent Directory Parent Directory | Revision Log Revision Log


Revision 120 - (show annotations) (download) (as text)
Thu Jun 5 07:30:56 2008 UTC (13 years, 6 months ago) by shmookey
File MIME type: text/x-python
File size: 14230 byte(s)
general improvements, beginning stump of a configuration interface

1 from types import FunctionType
2 import pgdb, uuid, time
3 from Config import dbHost, dbName, dbPort, dbUser, dbPass, dbPref
4 import Log, auth
5
6 class NotLoggedIn (Exception): pass
7 class NotAllowed (Exception): pass
8 class DatabaseSettingsError (Exception): pass
9 class DatabaseError (Exception): pass
10 class DatabaseNotInitialised (Exception): pass
11 class UnrecognisedInstance (Exception): pass
12 class UnrecognisedClass (Exception): pass
13 class SessionExpired (Exception): pass
14 class InvalidSession (Exception): pass
15 class InvalidLogin (Exception): pass
16 class InconsistentDatabase (Exception): pass
17
18 ''' SafeObject is not what you think it is.
19 SafeObject provides an access-control-aware interface to any python module or class.
20 Its main use, perhaps its only use, is to help make secure interfaces to the system
21 less of a pain in the arse to build and maintain. By making sure your source file
22 only calls trusted functions through a SafeObject interface, you can be sure that all
23 accesses will be logged and only granted to authorized users. As well as password
24 authentication, SafeObject generates a ticket number on successful login that can be
25 stored in a cookie or elsewhere and used to resume a session.
26
27 SafeObject is implemented on top of the SafeBase class, which provides access to
28 the login and session interface. Additionally, SafeBase provides methods to
29 store and retrieve information about the entity it is guarding that the user
30 has access to.
31
32 SafeObject does not stop the programmer from using direct calls to the interface it
33 guards. SafeObject is only useful where the user can't make the interpreter execute
34 code.
35 '''
36
37 '''ACLs
38
39 An Access-Control List, or ACL, lists the values that a user or group is allowed
40 to pass to a particular argument of a particular function. Values are regular
41 expressions to be compiled by the python "re" module.
42 '''
43
44 class RemapType:
45 def __init__ (self, fn):
46 self.fn = fn
47
48 class SafeBase:
49 def __init__ (self):
50 self._username = ""
51 self._sessionid = ""
52 self._db = None
53 self._groups = []
54 self._uid = -1
55 self._recordCache = []
56 self._admin = False
57
58 def __GetDatabaseCursor (self):
59 if self._db == None:
60 try: self._db = pgdb.connect (host = dbHost + ":" + dbPort, user = dbUser, password = dbPass, database = dbName)
61 except: raise DatabaseSettingsError ()
62 return self._db.cursor ()
63
64 def _Login (self, username, password):
65 if not auth.Authenticate (username, password): raise InvalidLogin ()
66 cur = self.__GetDatabaseCursor ()
67 query = "SELECT admin FROM %s_users WHERE name = '%s'" % (dbPref, username)
68 try:
69 cur.execute (query)
70 except pgdb.DatabaseError, details:
71 Log.Message ("User %s triggered a database error in _Login (authenticated). Details: %s" % (self._username, details), "pyaaa.py", "ERROR")
72 raise DatabaseError ()
73 if cur.rowcount == 0:
74 Log.Message ("User %s does not exist." % username, "pyaaa.py", "ERROR")
75 raise InvalidLogin ()
76 result = cur.fetchone ()
77 self._admin = result[0]
78 self._username = username
79 self._sessionid = str(uuid.uuid4 ())
80 expiry = time.time () + 3600 # an hour
81 query = "INSERT INTO %s_sessions (username, sid, expiry) VALUES ('%s', '%s', %d)" % (dbPref, username, self._sessionid, expiry)
82 try:
83 cur.execute (query)
84 except pgdb.DatabaseError, details:
85 Log.Message ("User %s triggered a database error in _Login (authenticated). Details: %s" % (self._username, details), "pyaaa.py", "ERROR")
86 raise DatabaseError ()
87 self._db.commit ()
88 Log.Message ("User %s logs in." % username, "pyaaa.py", "INFO")
89 return self._sessionid
90
91 def _Logout (self):
92 cur = self.__GetDatabaseCursor ()
93 query = "DELETE FROM %s_sessions WHERE sid = '%s'" % (dbPref, self._sessionid)
94 try:
95 cur.execute (query)
96 except pgdb.DatabaseError, details:
97 Log.Message ("User %s triggered a database error in _Logout. Details: %s" % (self._username, details), "pyaaa.py", "ERROR")
98 raise DatabaseError ()
99 self._db.commit ()
100 self.username = ""
101 self.sessionid = ""
102 Log.Message ("User %s logs out." % username, "pyaaa.py", "INFO")
103
104 def _Resume (self, session):
105 cur = self.__GetDatabaseCursor ()
106 query = "SELECT id, username, expiry FROM %s_sessions WHERE sid = '%s'" % (dbPref, session)
107 try:
108 cur.execute (query)
109 except pgdb.DatabaseError, details:
110 Log.Message ("User %s triggered a database error in _Resume. Details: %s" % (self._username, details), "pyaaa.py", "ERROR")
111 raise DatabaseError ()
112 if cur.rowcount == 0:
113 Log.Message ("Session %s is invalid." % session, "pyaaa.py", "ERROR")
114 raise InvalidSession ()
115 results = cur.fetchall ()
116 for result in results:
117 if result[2] < time.time ():
118 delquery = "DELETE FROM %s_sessions WHERE id = %d" % (dbPref, result[0])
119 cur.execute (delquery)
120 self._db.commit ()
121 else:
122 expiry = time.time () + 3600 # an hour
123 upquery = "UPDATE %s_sessions SET expiry = %d WHERE id = %d" % (dbPref, expiry, result[0])
124 cur.execute (upquery)
125 self._db.commit ()
126 self._username = result[1]
127 adminquery = "SELECT admin FROM %s_users WHERE name = '%s'" % (dbPref, result[1])
128 cur.execute (adminquery)
129 adresult = cur.fetchone ()
130 self._admin = adresult [0]
131 self._sessionid = session
132 return self._username
133 Log.Message ("Session %s has expired." % session, "pyaaa.py", "ERROR")
134 raise SessionExpired ()
135
136 def _GetUID (self):
137 if self._uid == -1:
138 cur = self.__GetDatabaseCursor ()
139 query = "SELECT id FROM %s_users WHERE name = '%s'" % (dbPref, self._username)
140 try:
141 cur.execute (query)
142 except pgdb.DatabaseError, details:
143 Log.Message ("User %s triggered a database error in GetUID. Details: %s" % (self._username, details), "pyaaa.py", "ERROR")
144 raise DatabaseError ()
145 if cur.rowcount == 0:
146 Log.Message ("No matching UID for user %s" % self._username, "pyaaa.py", "ERROR")
147 raise InconsistentDatabase ()
148 result = cur.fetchone ()
149 self._uid = result[0]
150
151 return self._uid
152
153 def _GetGroups (self):
154 if self._groups == []:
155 cur = self.__GetDatabaseCursor ()
156 query = "SELECT gid FROM %s_memberships WHERE uid = %d" % (dbPref, self._GetUID ())
157 try:
158 cur.execute (query)
159 except pgdb.DatabaseError, details:
160 Log.Message ("User %s triggered a database error in GetGroups. Details: %s" % (self._username, details), "pyaaa.py", "ERROR")
161 raise DatabaseError ()
162 results = cur.fetchall ()
163 self._groups = [i[0] for i in results]
164
165 return self._groups
166
167 # Check to see if the current user has read access to a field.
168 # Now with a maximum of only two SQL queries!
169 def _IsFieldAllowed (self, tableName, recordID, fieldName):
170 cur = self.__GetDatabaseCursor ()
171
172 # First we'll get a list of all the rules relating directly to this user and directly to this table, and not any other record or field:
173 # This query makes an ordered list of rules to check, from most-specific to least-specific.
174 # User rules are always more specific than group rules.
175 #try:
176 query = """SELECT action FROM %s_permissions WHERE
177 (classid = '%s' or classid = '*') AND
178 (fieldid = '%s' or fieldid = '*') AND
179 (recordid = %d or recordid = 0) AND
180 gid = %d AND
181 isUser = 't'
182 ORDER BY classid DESC, recordid DESC, fieldid DESC""" % (dbPref, tableName, fieldName, recordID, self._GetUID () )
183 try:
184 cur.execute (query)
185 except pgdb.DatabaseError, details:
186 Log.Message ("User %s triggered a database error in IsFieldAllowed. Details: %s" % (self._username, details), "pyaaa.py", "ERROR")
187 raise DatabaseError ()
188 results = cur.fetchall ()
189 #except: raise DatabaseNotInitialised ()
190 for rule in results:
191 if rule[0] == "ALLOW": return True
192 else: return False
193 # Now do the same except with all the groups the user is a member of:
194 if len (self._GetGroups ()) == 0: return False
195 groupCheck = ""
196 for group in self._GetGroups (): groupCheck += "gid = " + str(group) + " OR "
197 groupCheck = groupCheck [:-3]
198 query = """SELECT action, classid, recordid, fieldid FROM %s_permissions WHERE
199 (classid = '%s' or classid = '*') AND
200 (fieldid = '%s' or fieldid = '*') AND
201 (recordid = %d or recordid = 0) AND
202 (%s) AND
203 isUser = 'f'
204 ORDER BY classid DESC, recordid DESC, fieldid DESC""" % (dbPref, tableName, fieldName, recordID, groupCheck )
205 try:
206 cur.execute (query)
207 except pgdb.DatabaseError, details:
208 Log.Message ("User %s triggered a database error in IsFieldAllowed. Details: %s" % (self._username, details), "pyaaa.py", "ERROR")
209 raise DatabaseError ()
210 results = cur.fetchall ()
211 # We can't use the normal 'for' iterator here because if we find a DENY rule we need to keep looking at the same level of specificness to see if there's an ALLOW to override it.
212 # Therefore: iterate through results, if we find one that says ALLOW then allow it, if we find anything else then keep looking at the same degree of specificness with different groups
213 # until we find an ALLOW to override it. Otherwise, deny it.
214 for i in range (0,len(results)):
215 if results[i][0] == "ALLOW": return True
216 else:
217 if i+1 == len(results): return False
218 j=i+1
219 while results[i][1:4] == results[j][1:4]:
220 if results[j][0] == "ALLOW": return True
221 j+1
222 return False
223 return False
224
225 def _GetInfo (self, className, instanceName, fieldsToRetrieve):
226 # first get the id of the object (switch in this case) from the pact_<objclass> table
227 # then check the permissions table to see if the current user can read this data
228 # then return the info
229
230 cur = self.__GetDatabaseCursor ()
231
232 # First get the ID
233 # Might as well get all the data in one query:
234 query = "SELECT id, " + ", ".join(fieldsToRetrieve) + " FROM " + dbPref + "_" + className + " WHERE name = '" + instanceName + "'"
235 try:
236 cur.execute (query)
237 except pgdb.DatabaseError, details:
238 Log.Message ("User %s triggered a database error in GetInfo. Details: %s" % (self._username, details), "pyaaa.py", "ERROR")
239 raise DatabaseError ()
240 if cur.rowcount == 0:
241 raise UnrecognisedInstance ()
242 results = cur.fetchone ()
243
244 id = results[0]
245
246 # Now check if we're allowed to give the user these values:
247 for field in fieldsToRetrieve:
248 if not self._IsFieldAllowed (className, id, field): raise NotAllowed ()
249
250 return results [1:]
251
252 def _SetMetaVar (self, name, value):
253 self._meta [name] = value
254
255 def _GetRecords (self, classType, fromCache = True):
256 if fromCache and not self._recordCache == []:
257 return self._recordCache
258
259 records = []
260 cur = self.__GetDatabaseCursor ()
261 query = "SELECT recordid FROM %s_permissions WHERE classid = '%s' AND gid = %d AND isUser = 't' AND action = 'ALLOW'" % (dbPref, classType, self._GetUID ())
262 try:
263 cur.execute (query)
264 except pgdb.DatabaseError, details:
265 Log.Message ("User %s triggered a database error in GetRecords. Details: %s" % (self._username, details), "pyaaa.py", "ERROR")
266 raise DatabaseError ()
267 results = cur.fetchall ()
268 records += [ int(r[0]) for r in results ]
269 groupCheck = ""
270 for group in self._GetGroups (): groupCheck += "gid = " + str(group) + " OR "
271 groupCheck = groupCheck [:-3]
272 query = "SELECT recordid FROM %s_permissions WHERE classid = '%s' AND (%s) AND isUser = 'f' AND action = 'ALLOW'" % (dbPref, classType, groupCheck)
273 try:
274 cur.execute (query)
275 except pgdb.DatabaseError, details:
276 Log.Message ("User %s triggered a database error in GetRecords. Details: %s" % (self._username, details), "pyaaa.py", "ERROR")
277 raise DatabaseError ()
278 results = cur.fetchall ()
279 records += [ int(r[0]) for r in results ]
280
281 ids = ""
282 for id in records: ids += "id = %d OR " % id
283 ids = ids [:-3]
284 query = "SELECT name FROM %s_%s WHERE %s" % (dbPref, classType, ids)
285 cur.execute (query)
286 results = cur.fetchall ()
287 self._recordCache = [ r[0] for r in results ]
288
289 return self._recordCache
290
291 def _GetRecordSummary (self, classType):
292
293 cur = self.__GetDatabaseCursor ()
294 query = "SELECT * FROM %s_%s" % (dbPref, classType)
295 try:
296 cur.execute (query)
297 except pgdb.DatabaseError, details:
298 Log.Message ("User %s triggered a database error in GetRecordSummary. Details: %s" % (self._username, details), "pyaaa.py", "ERROR")
299 raise DatabaseError ()
300 return cur.fetchall ()
301
302 class SafeObject (SafeBase):
303 def __init__ (self, BaseObject, InstanceName):
304 SafeBase.__init__ (self)
305 self._base = BaseObject
306 self._name = InstanceName
307 for key, value in BaseObject.__class__.__dict__.iteritems ():
308 if type (value) == FunctionType:
309 self.__dict__ [key] = (lambda k: lambda *args, **kwargs: self._SafeExecute (k, args, kwargs))(key)
310 def _IsAllowed (self, fn):
311 return True
312 def _SafeExecute (self, fn, args, kwargs):
313 if self._username == "":
314 raise NotLoggedIn ()
315 if not self._IsAllowed (fn):
316 raise NotAllowed ()
317 resolvedArgs = kwargs
318 # Find any remapped items and retrieve their value.
319 for key, value in kwargs.iteritems ():
320 if value.__class__ == RemapType:
321 resolvedArgs [key] = value.fn ()
322 Log.Message ("User %s runs %s.%s (instance name: %s)" % (self._username, self._base.__class__, fn, self._name), "pyaaa.py", "INFO")
323 return self._base.__class__.__dict__ [fn] (self._base, *args, **resolvedArgs)
324
325 # Remap a class method so that some or all of its arguments are retrieved from the database.
326 # The new remapped method WILL override supplied arguments.
327 # Arguments to be remapped should be provided as a dictionary linking argument names to a column name in the database.
328 # Only keyword arguments can be remapped.
329 def _Remap (self, fnName, newMappings):
330 newArguments = {}
331 for ArgName, ArgColumn in newMappings.iteritems():
332 newArguments[ArgName] = RemapType (lambda ArgColumn=ArgColumn: self._GetInfo ([ArgColumn])[0])
333 self.__dict__ [fnName] = (lambda mappedArgs: lambda *args, **kwargs: self._SafeExecute (fnName, args, kwargs.update(mappedArgs) or kwargs))(newArguments)
334
335 def _GetInfo (self, fieldsToRetrieve):
336 return SafeBase._GetInfo (self, self._base.__class__.__name__, self._name, fieldsToRetrieve)
337
338 def _GetOtherInstances (self):
339 return SafeBase._GetRecords (self, self._base.__class__.__name__)
340
341 class SafeDataSource (SafeBase):
342 def __init__ (self, dataSourceName):
343 self.dsName = dataSourceName
344 def GetData (self): pass

Managed by UCC Webmasters ViewVC Help
Powered by ViewVC 1.1.26