|
1 # copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
|
2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr |
|
3 # |
|
4 # This file is part of CubicWeb. |
|
5 # |
|
6 # CubicWeb is free software: you can redistribute it and/or modify it under the |
|
7 # terms of the GNU Lesser General Public License as published by the Free |
|
8 # Software Foundation, either version 2.1 of the License, or (at your option) |
|
9 # any later version. |
|
10 # |
|
11 # CubicWeb is distributed in the hope that it will be useful, but WITHOUT |
|
12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
|
13 # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more |
|
14 # details. |
|
15 # |
|
16 # You should have received a copy of the GNU Lesser General Public License along |
|
17 # with CubicWeb. If not, see <http://www.gnu.org/licenses/>. |
|
18 """Security hooks: check permissions to add/delete/update entities according to |
|
19 the connected user |
|
20 """ |
|
21 |
|
22 __docformat__ = "restructuredtext en" |
|
23 from warnings import warn |
|
24 |
|
25 from logilab.common.registry import objectify_predicate |
|
26 |
|
27 from yams import buildobjs |
|
28 |
|
29 from cubicweb import Unauthorized |
|
30 from cubicweb.server import BEFORE_ADD_RELATIONS, ON_COMMIT_ADD_RELATIONS, hook |
|
31 |
|
32 |
|
33 |
|
34 def check_entity_attributes(cnx, entity, action, editedattrs=None): |
|
35 eid = entity.eid |
|
36 eschema = entity.e_schema |
|
37 if action == 'delete': |
|
38 eschema.check_perm(session, action, eid=eid) |
|
39 return |
|
40 # ._cw_skip_security_attributes is there to bypass security for attributes |
|
41 # set by hooks by modifying the entity's dictionary |
|
42 if editedattrs is None: |
|
43 editedattrs = entity.cw_edited |
|
44 dontcheck = editedattrs.skip_security |
|
45 etypechecked = False |
|
46 for attr in editedattrs: |
|
47 if attr in dontcheck: |
|
48 continue |
|
49 rdef = eschema.rdef(attr, takefirst=True) |
|
50 if rdef.final: # non final relation are checked by standard hooks |
|
51 perms = rdef.permissions.get(action) |
|
52 # comparison below works because the default update perm is: |
|
53 # |
|
54 # ('managers', ERQLExpression(Any X WHERE U has_update_permission X, |
|
55 # X eid %(x)s, U eid %(u)s)) |
|
56 # |
|
57 # is deserialized in this order (groups first), and ERQLExpression |
|
58 # implements comparison by rql expression. |
|
59 if perms == buildobjs.DEFAULT_ATTRPERMS[action]: |
|
60 # The default rule is to delegate to the entity |
|
61 # rule. This needs to be checked only once. |
|
62 if not etypechecked: |
|
63 entity.cw_check_perm(action) |
|
64 etypechecked = True |
|
65 continue |
|
66 if perms == (): |
|
67 # That means an immutable attribute; as an optimization, avoid |
|
68 # going through check_perm. |
|
69 raise Unauthorized(action, str(rdef)) |
|
70 rdef.check_perm(cnx, action, eid=eid) |
|
71 |
|
72 if action == 'add' and not etypechecked: |
|
73 # think about cnx.create_entity('Foo') |
|
74 # the standard metadata were inserted by a hook |
|
75 # with a bypass ... we conceptually need to check |
|
76 # the eid attribute at *creation* time |
|
77 entity.cw_check_perm(action) |
|
78 |
|
79 |
|
80 class CheckEntityPermissionOp(hook.DataOperationMixIn, hook.LateOperation): |
|
81 def precommit_event(self): |
|
82 cnx = self.cnx |
|
83 for eid, action, edited in self.get_data(): |
|
84 entity = cnx.entity_from_eid(eid) |
|
85 check_entity_attributes(cnx, entity, action, edited) |
|
86 |
|
87 |
|
88 class CheckRelationPermissionOp(hook.DataOperationMixIn, hook.LateOperation): |
|
89 def precommit_event(self): |
|
90 cnx = self.cnx |
|
91 for action, rschema, eidfrom, eidto in self.get_data(): |
|
92 rdef = rschema.rdef(cnx.entity_metas(eidfrom)['type'], |
|
93 cnx.entity_metas(eidto)['type']) |
|
94 rdef.check_perm(cnx, action, fromeid=eidfrom, toeid=eidto) |
|
95 |
|
96 |
|
97 @objectify_predicate |
|
98 def write_security_enabled(cls, req, **kwargs): |
|
99 if req is None or not req.write_security: |
|
100 return 0 |
|
101 return 1 |
|
102 |
|
103 class SecurityHook(hook.Hook): |
|
104 __abstract__ = True |
|
105 category = 'security' |
|
106 __select__ = hook.Hook.__select__ & write_security_enabled() |
|
107 |
|
108 |
|
109 class AfterAddEntitySecurityHook(SecurityHook): |
|
110 __regid__ = 'securityafteraddentity' |
|
111 events = ('after_add_entity',) |
|
112 |
|
113 def __call__(self): |
|
114 CheckEntityPermissionOp.get_instance(self._cw).add_data( |
|
115 (self.entity.eid, 'add', self.entity.cw_edited) ) |
|
116 |
|
117 |
|
118 class AfterUpdateEntitySecurityHook(SecurityHook): |
|
119 __regid__ = 'securityafterupdateentity' |
|
120 events = ('after_update_entity',) |
|
121 |
|
122 def __call__(self): |
|
123 # save back editedattrs in case the entity is reedited later in the |
|
124 # same transaction, which will lead to cw_edited being |
|
125 # overwritten |
|
126 action = 'add' if self._cw.added_in_transaction(self.entity.eid) else 'update' |
|
127 CheckEntityPermissionOp.get_instance(self._cw).add_data( |
|
128 (self.entity.eid, action, self.entity.cw_edited) ) |
|
129 |
|
130 |
|
131 class BeforeDelEntitySecurityHook(SecurityHook): |
|
132 __regid__ = 'securitybeforedelentity' |
|
133 events = ('before_delete_entity',) |
|
134 |
|
135 def __call__(self): |
|
136 self.entity.cw_check_perm('delete') |
|
137 |
|
138 |
|
139 def skip_inlined_relation_security(cnx, rschema, eid): |
|
140 """return True if security for the given inlined relation should be skipped, |
|
141 in case where the relation has been set through modification of |
|
142 `entity.cw_edited` in a hook |
|
143 """ |
|
144 assert rschema.inlined |
|
145 try: |
|
146 entity = cnx.entity_cache(eid) |
|
147 except KeyError: |
|
148 return False |
|
149 edited = getattr(entity, 'cw_edited', None) |
|
150 if edited is None: |
|
151 return False |
|
152 return rschema.type in edited.skip_security |
|
153 |
|
154 |
|
155 class BeforeAddRelationSecurityHook(SecurityHook): |
|
156 __regid__ = 'securitybeforeaddrelation' |
|
157 events = ('before_add_relation',) |
|
158 |
|
159 def __call__(self): |
|
160 if self.rtype in BEFORE_ADD_RELATIONS: |
|
161 nocheck = self._cw.transaction_data.get('skip-security', ()) |
|
162 if (self.eidfrom, self.rtype, self.eidto) in nocheck: |
|
163 return |
|
164 rschema = self._cw.repo.schema[self.rtype] |
|
165 if rschema.inlined and skip_inlined_relation_security( |
|
166 self._cw, rschema, self.eidfrom): |
|
167 return |
|
168 rdef = rschema.rdef(self._cw.entity_metas(self.eidfrom)['type'], |
|
169 self._cw.entity_metas(self.eidto)['type']) |
|
170 rdef.check_perm(self._cw, 'add', fromeid=self.eidfrom, toeid=self.eidto) |
|
171 |
|
172 |
|
173 class AfterAddRelationSecurityHook(SecurityHook): |
|
174 __regid__ = 'securityafteraddrelation' |
|
175 events = ('after_add_relation',) |
|
176 |
|
177 def __call__(self): |
|
178 if self.rtype not in BEFORE_ADD_RELATIONS: |
|
179 nocheck = self._cw.transaction_data.get('skip-security', ()) |
|
180 if (self.eidfrom, self.rtype, self.eidto) in nocheck: |
|
181 return |
|
182 rschema = self._cw.repo.schema[self.rtype] |
|
183 if rschema.inlined and skip_inlined_relation_security( |
|
184 self._cw, rschema, self.eidfrom): |
|
185 return |
|
186 if self.rtype in ON_COMMIT_ADD_RELATIONS: |
|
187 CheckRelationPermissionOp.get_instance(self._cw).add_data( |
|
188 ('add', rschema, self.eidfrom, self.eidto) ) |
|
189 else: |
|
190 rdef = rschema.rdef(self._cw.entity_metas(self.eidfrom)['type'], |
|
191 self._cw.entity_metas(self.eidto)['type']) |
|
192 rdef.check_perm(self._cw, 'add', fromeid=self.eidfrom, toeid=self.eidto) |
|
193 |
|
194 |
|
195 class BeforeDeleteRelationSecurityHook(SecurityHook): |
|
196 __regid__ = 'securitybeforedelrelation' |
|
197 events = ('before_delete_relation',) |
|
198 |
|
199 def __call__(self): |
|
200 nocheck = self._cw.transaction_data.get('skip-security', ()) |
|
201 if (self.eidfrom, self.rtype, self.eidto) in nocheck: |
|
202 return |
|
203 rschema = self._cw.repo.schema[self.rtype] |
|
204 if rschema.inlined and skip_inlined_relation_security( |
|
205 self._cw, rschema, self.eidfrom): |
|
206 return |
|
207 rdef = rschema.rdef(self._cw.entity_metas(self.eidfrom)['type'], |
|
208 self._cw.entity_metas(self.eidto)['type']) |
|
209 rdef.check_perm(self._cw, 'delete', fromeid=self.eidfrom, toeid=self.eidto) |