Commit | Line | Data |
---|---|---|
920dae64 AT |
1 | """Bastionification utility. |
2 | ||
3 | A bastion (for another object -- the 'original') is an object that has | |
4 | the same methods as the original but does not give access to its | |
5 | instance variables. Bastions have a number of uses, but the most | |
6 | obvious one is to provide code executing in restricted mode with a | |
7 | safe interface to an object implemented in unrestricted mode. | |
8 | ||
9 | The bastionification routine has an optional second argument which is | |
10 | a filter function. Only those methods for which the filter method | |
11 | (called with the method name as argument) returns true are accessible. | |
12 | The default filter method returns true unless the method name begins | |
13 | with an underscore. | |
14 | ||
15 | There are a number of possible implementations of bastions. We use a | |
16 | 'lazy' approach where the bastion's __getattr__() discipline does all | |
17 | the work for a particular method the first time it is used. This is | |
18 | usually fastest, especially if the user doesn't call all available | |
19 | methods. The retrieved methods are stored as instance variables of | |
20 | the bastion, so the overhead is only occurred on the first use of each | |
21 | method. | |
22 | ||
23 | Detail: the bastion class has a __repr__() discipline which includes | |
24 | the repr() of the original object. This is precomputed when the | |
25 | bastion is created. | |
26 | ||
27 | """ | |
28 | ||
29 | __all__ = ["BastionClass", "Bastion"] | |
30 | ||
31 | from types import MethodType | |
32 | ||
33 | ||
34 | class BastionClass: | |
35 | ||
36 | """Helper class used by the Bastion() function. | |
37 | ||
38 | You could subclass this and pass the subclass as the bastionclass | |
39 | argument to the Bastion() function, as long as the constructor has | |
40 | the same signature (a get() function and a name for the object). | |
41 | ||
42 | """ | |
43 | ||
44 | def __init__(self, get, name): | |
45 | """Constructor. | |
46 | ||
47 | Arguments: | |
48 | ||
49 | get - a function that gets the attribute value (by name) | |
50 | name - a human-readable name for the original object | |
51 | (suggestion: use repr(object)) | |
52 | ||
53 | """ | |
54 | self._get_ = get | |
55 | self._name_ = name | |
56 | ||
57 | def __repr__(self): | |
58 | """Return a representation string. | |
59 | ||
60 | This includes the name passed in to the constructor, so that | |
61 | if you print the bastion during debugging, at least you have | |
62 | some idea of what it is. | |
63 | ||
64 | """ | |
65 | return "<Bastion for %s>" % self._name_ | |
66 | ||
67 | def __getattr__(self, name): | |
68 | """Get an as-yet undefined attribute value. | |
69 | ||
70 | This calls the get() function that was passed to the | |
71 | constructor. The result is stored as an instance variable so | |
72 | that the next time the same attribute is requested, | |
73 | __getattr__() won't be invoked. | |
74 | ||
75 | If the get() function raises an exception, this is simply | |
76 | passed on -- exceptions are not cached. | |
77 | ||
78 | """ | |
79 | attribute = self._get_(name) | |
80 | self.__dict__[name] = attribute | |
81 | return attribute | |
82 | ||
83 | ||
84 | def Bastion(object, filter = lambda name: name[:1] != '_', | |
85 | name=None, bastionclass=BastionClass): | |
86 | """Create a bastion for an object, using an optional filter. | |
87 | ||
88 | See the Bastion module's documentation for background. | |
89 | ||
90 | Arguments: | |
91 | ||
92 | object - the original object | |
93 | filter - a predicate that decides whether a function name is OK; | |
94 | by default all names are OK that don't start with '_' | |
95 | name - the name of the object; default repr(object) | |
96 | bastionclass - class used to create the bastion; default BastionClass | |
97 | ||
98 | """ | |
99 | ||
100 | raise RuntimeError, "This code is not secure in Python 2.2 and 2.3" | |
101 | ||
102 | # Note: we define *two* ad-hoc functions here, get1 and get2. | |
103 | # Both are intended to be called in the same way: get(name). | |
104 | # It is clear that the real work (getting the attribute | |
105 | # from the object and calling the filter) is done in get1. | |
106 | # Why can't we pass get1 to the bastion? Because the user | |
107 | # would be able to override the filter argument! With get2, | |
108 | # overriding the default argument is no security loophole: | |
109 | # all it does is call it. | |
110 | # Also notice that we can't place the object and filter as | |
111 | # instance variables on the bastion object itself, since | |
112 | # the user has full access to all instance variables! | |
113 | ||
114 | def get1(name, object=object, filter=filter): | |
115 | """Internal function for Bastion(). See source comments.""" | |
116 | if filter(name): | |
117 | attribute = getattr(object, name) | |
118 | if type(attribute) == MethodType: | |
119 | return attribute | |
120 | raise AttributeError, name | |
121 | ||
122 | def get2(name, get1=get1): | |
123 | """Internal function for Bastion(). See source comments.""" | |
124 | return get1(name) | |
125 | ||
126 | if name is None: | |
127 | name = repr(object) | |
128 | return bastionclass(get2, name) | |
129 | ||
130 | ||
131 | def _test(): | |
132 | """Test the Bastion() function.""" | |
133 | class Original: | |
134 | def __init__(self): | |
135 | self.sum = 0 | |
136 | def add(self, n): | |
137 | self._add(n) | |
138 | def _add(self, n): | |
139 | self.sum = self.sum + n | |
140 | def total(self): | |
141 | return self.sum | |
142 | o = Original() | |
143 | b = Bastion(o) | |
144 | testcode = """if 1: | |
145 | b.add(81) | |
146 | b.add(18) | |
147 | print "b.total() =", b.total() | |
148 | try: | |
149 | print "b.sum =", b.sum, | |
150 | except: | |
151 | print "inaccessible" | |
152 | else: | |
153 | print "accessible" | |
154 | try: | |
155 | print "b._add =", b._add, | |
156 | except: | |
157 | print "inaccessible" | |
158 | else: | |
159 | print "accessible" | |
160 | try: | |
161 | print "b._get_.func_defaults =", map(type, b._get_.func_defaults), | |
162 | except: | |
163 | print "inaccessible" | |
164 | else: | |
165 | print "accessible" | |
166 | \n""" | |
167 | exec testcode | |
168 | print '='*20, "Using rexec:", '='*20 | |
169 | import rexec | |
170 | r = rexec.RExec() | |
171 | m = r.add_module('__main__') | |
172 | m.b = b | |
173 | r.r_exec(testcode) | |
174 | ||
175 | ||
176 | if __name__ == '__main__': | |
177 | _test() |