def insert_pending(self): """pending_keys hold variable referenced by U has_<action>_permission X relation. Once the snippet introducing this has been inserted and solutions recomputed, we have to insert snippet defined for <action> of entity types taken by X """ stmt = self.current_statement() while self.pending_keys: key, action = self.pending_keys.pop() try: varname = self.rewritten[key] except KeyError: try: varname = self.revvarmap[key[-1]][0] except KeyError: # variable isn't used anywhere else, we can't insert security raise Unauthorized() ptypes = stmt.defined_vars[varname].stinfo['possibletypes'] if len(ptypes) > 1: # XXX dunno how to handle this self.session.error( 'cant check security of %s, ambigous type for %s in %s', stmt, varname, key[0]) # key[0] == the rql expression raise Unauthorized() etype = next(iter(ptypes)) eschema = self.schema.eschema(etype) if not eschema.has_perm(self.session, action): rqlexprs = eschema.get_rqlexprs(action) if not rqlexprs: raise Unauthorized() self.insert_snippets([({varname: 'X'}, rqlexprs)])
def _test(self, func): self.assertEqual(func(Unauthorized()), 'You are not allowed to perform this operation') self.assertEqual(func(Unauthorized('a')), 'a') self.assertEqual(func(Unauthorized('a', 'b')), 'You are not allowed to perform a operation on b') self.assertEqual(func(Unauthorized('a', 'b', 'c')), 'a b c')
def check_no_password_selected(rqlst): """check that Password entities are not selected""" for solution in rqlst.solutions: for var, etype in solution.items(): if etype == 'Password': raise Unauthorized('Password selection is not allowed (%s)' % var)
def insert_varmap_snippets(self, varmap, rqlexprs, varexistsmap): try: self.init_from_varmap(varmap, varexistsmap) except VariableFromSubQuery as ex: # variable may have been moved to a newly inserted subquery # we should insert snippet in that subquery subquery = self.select.aliases[ex.variable].query assert len(subquery.children) == 1, subquery subselect = subquery.children[0] RQLRewriter(self.session).rewrite(subselect, [(varmap, rqlexprs)], self.kwargs) return self._insert_scope = None previous = None inserted = False for rqlexpr in rqlexprs: self.current_expr = rqlexpr if varexistsmap is None: try: new = self.insert_snippet(varmap, rqlexpr.snippet_rqlst, previous) except Unsupported: continue inserted = True if new is not None and self._insert_scope is None: self.exists_snippet[rqlexpr] = new previous = previous or new else: # called to reintroduce snippet due to ambiguity creation, # so skip snippets which are not introducing this ambiguity exists = varexistsmap[varmap] if self.exists_snippet.get(rqlexpr) is exists: self.insert_snippet(varmap, rqlexpr.snippet_rqlst, exists) if varexistsmap is None and not inserted: # no rql expression found matching rql solutions. User has no access right raise Unauthorized() # XXX may also be because of bad constraints in schema definition
def get_local_checks(cnx, rqlst, solution): """Check that the given user has credentials to access data read by the query and return a dict defining necessary "local checks" (i.e. rql expression in read permission defined in the schema) where no group grants him the permission. Returned dictionary's keys are variable names and values the rql expressions for this variable (with the given solution). Raise :exc:`Unauthorized` if access is known to be defined, i.e. if there is no matching group and no local permissions. """ DBG = (server.DEBUG & server.DBG_SEC) and 'read' in server._SECURITY_CAPS schema = cnx.repo.schema user = cnx.user localchecks = {} # iterate on defined_vars and not on solutions to ignore column aliases for varname in rqlst.defined_vars: eschema = schema.eschema(solution[varname]) if eschema.final: continue if not user.matching_groups(eschema.get_groups('read')): erqlexprs = eschema.get_rqlexprs('read') if not erqlexprs: ex = Unauthorized('read', solution[varname]) ex.var = varname if DBG: print('check_read_access: %s %s %s %s' % (varname, eschema, user.groups, eschema.get_groups('read'))) raise ex # don't insert security on variable only referenced by 'NOT X relation Y' or # 'NOT EXISTS(X relation Y)' varinfo = rqlst.defined_vars[varname].stinfo if varinfo['selected'] or (len([ r for r in varinfo['relations'] if (not schema.rschema(r.r_type).final and ( (isinstance(r.parent, Exists) and r.parent.neged( strict=True)) or isinstance(r.parent, Not))) ]) != len(varinfo['relations'])): localchecks[varname] = erqlexprs return localchecks
def check_entity_attributes(cnx, entity, action, editedattrs=None): eid = entity.eid eschema = entity.e_schema # ._cw_skip_security_attributes is there to bypass security for attributes # set by hooks by modifying the entity's dictionary if editedattrs is None: editedattrs = entity.cw_edited dontcheck = editedattrs.skip_security etypechecked = False for attr in editedattrs: if attr in dontcheck: continue rdef = eschema.rdef(attr, takefirst=True) if rdef.final: # non final relation are checked by standard hooks perms = rdef.permissions.get(action) # comparison below works because the default update perm is: # # ('managers', ERQLExpression(Any X WHERE U has_update_permission X, # X eid %(x)s, U eid %(u)s)) # # is deserialized in this order (groups first), and ERQLExpression # implements comparison by rql expression. if perms == buildobjs.DEFAULT_ATTRPERMS[action]: # The default rule is to delegate to the entity # rule. This needs to be checked only once. if not etypechecked: entity.cw_check_perm(action) etypechecked = True continue if perms == (): # That means an immutable attribute; as an optimization, avoid # going through check_perm. raise Unauthorized(action, str(rdef)) rdef.check_perm(cnx, action, eid=eid) if action == 'add' and not etypechecked: # think about cnx.create_entity('Foo') # the standard metadata were inserted by a hook # with a bypass ... we conceptually need to check # the eid attribute at *creation* time entity.cw_check_perm(action)
def check_relations_read_access(cnx, select, args): """Raise :exc:`Unauthorized` if the given user doesn't have credentials to read relations used in the given syntax tree """ # use `term_etype` since we've to deal with rewritten constants here, # when used as an external source by another repository. # XXX what about local read security w/ those rewritten constants... # XXX constants can also happen in some queries generated by req.find() DBG = (server.DEBUG & server.DBG_SEC) and 'read' in server._SECURITY_CAPS schema = cnx.repo.schema user = cnx.user if select.where is not None: for rel in select.where.iget_nodes(Relation): for solution in select.solutions: # XXX has_text may have specific perm ? if rel.r_type in READ_ONLY_RTYPES: continue rschema = schema.rschema(rel.r_type) if rschema.final: eschema = schema.eschema( term_etype(cnx, rel.children[0], solution, args)) rdef = eschema.rdef(rschema) else: rdef = rschema.rdef( term_etype(cnx, rel.children[0], solution, args), term_etype(cnx, rel.children[1].children[0], solution, args)) if not user.matching_groups(rdef.get_groups('read')): if DBG: print('check_read_access: %s %s does not match %s' % (rdef, user.groups, rdef.get_groups('read'))) # XXX rqlexpr not allowed raise Unauthorized('read', rel.r_type) if DBG: print('check_read_access: %s %s matches %s' % (rdef, user.groups, rdef.get_groups('read')))
def __call__(self): raise Unauthorized()
def core_handle(self, req): """method called by the main publisher to process <req> relative path should return a string containing the resulting page or raise a `NotFound` exception :type req: `web.Request` :param req: the request object :rtype: str :return: the result of the pusblished url """ path = req.relative_path(False) # don't log form values they may contains sensitive information self.debug('publish "%s" (%s, form params: %s)', path, req.session.sessionid, list(req.form)) # remove user callbacks on a new request (except for json controllers # to avoid callbacks being unregistered before they could be called) tstart = process_time() commited = False try: # standard processing of the request try: # apply CORS sanity checks cors.process_request(req, self.vreg.config) ctrlid, rset = self.url_resolver.process(req, path) try: controller = self.vreg['controllers'].select(ctrlid, req, appli=self) except NoSelectableObject: raise Unauthorized(req._('not authorized')) req.update_search_state() result = controller.publish(rset=rset) except cors.CORSPreflight: # Return directly an empty 200 req.status_out = 200 result = b'' except Redirect as ex: # Redirect may be raised by edit controller when everything went # fine, so attempt to commit result = self.redirect_handler(req, ex) if req.cnx: txuuid = req.cnx.commit() commited = True if txuuid is not None: req.data['last_undoable_transaction'] = txuuid # error case except NotFound as ex: result = self.notfound_content(req) req.status_out = ex.status except ValidationError as ex: result = self.validation_error_handler(req, ex) except RemoteCallFailed as ex: result = self.ajax_error_handler(req, ex) except Unauthorized as ex: req.data['errmsg'] = req._( 'You\'re not authorized to access this page. ' 'If you think you should, please contact the site administrator.' ) req.status_out = http_client.FORBIDDEN result = self.error_handler(req, ex, tb=False) except Forbidden as ex: req.data['errmsg'] = req._( 'This action is forbidden. ' 'If you think it should be allowed, please contact the site administrator.' ) req.status_out = http_client.FORBIDDEN result = self.error_handler(req, ex, tb=False) except (BadRQLQuery, RequestError) as ex: result = self.error_handler(req, ex, tb=False) # pass through exception except DirectResponse: if req.cnx: req.cnx.commit() raise except (AuthenticationError, LogOut): # the rollback is handled in the finally raise # Last defense line except BaseException as ex: req.status_out = http_client.INTERNAL_SERVER_ERROR result = self.error_handler(req, ex, tb=True) finally: if req.cnx and not commited: try: req.cnx.rollback() except Exception: pass # ignore rollback error at this point self.add_undo_link_to_msg(req) self.debug('query %s executed in %s sec', path, process_time() - tstart) return result
def _check_permissions(self, rqlst): """Return a dict defining "local checks", i.e. RQLExpression defined in the schema that should be inserted in the original query, together with a set of variable names which requires some security to be inserted. Solutions where a variable has a type which the user can't definitly read are removed, else if the user *may* read it (i.e. if an rql expression is defined for the "read" permission of the related type), the local checks dict is updated. The local checks dict has entries for each different local check necessary, with associated solutions as value, a local check being defined by a list of 2-uple (variable name, rql expressions) for each variable which has to be checked. Solutions which don't require local checks will be associated to the empty tuple key. Note rqlst should not have been simplified at this point. """ cnx = self.cnx msgs = [] # dict(varname: eid), allowing to check rql expression for variables # which have a known eid varkwargs = {} if not cnx.transaction_data.get('security-rqlst-cache'): for var in rqlst.defined_vars.values(): if var.stinfo['constnode'] is not None: eid = var.stinfo['constnode'].eval(self.args) varkwargs[var.name] = int(eid) # dictionary of variables restricted for security reason localchecks = {} restricted_vars = set() newsolutions = [] for solution in rqlst.solutions: try: localcheck = get_local_checks(cnx, rqlst, solution) except Unauthorized as ex: msg = 'remove %s from solutions since %s has no %s access to %s' msg %= (solution, cnx.user.login, ex.args[0], ex.args[1]) msgs.append(msg) LOGGER.info(msg) else: newsolutions.append(solution) # try to benefit of rqlexpr.check cache for entities which # are specified by eid in query'args for varname, eid in varkwargs.items(): try: rqlexprs = localcheck.pop(varname) except KeyError: continue # if entity has been added in the current transaction, the # user can read it whatever rql expressions are associated # to its type if cnx.added_in_transaction(eid): continue for rqlexpr in rqlexprs: if rqlexpr.check(cnx, eid): break else: raise Unauthorized( 'No read access on %r with eid %i.' % (var, eid)) # mark variables protected by an rql expression restricted_vars.update(localcheck) # turn local check into a dict key localcheck = tuple(sorted(localcheck.items())) localchecks.setdefault(localcheck, []).append(solution) # raise Unautorized exception if the user can't access to any solution if not newsolutions: raise Unauthorized('\n'.join(msgs)) # if there is some message, solutions have been modified and must be # reconsidered by the syntax treee if msgs: rqlst.set_possible_types(newsolutions) return localchecks, restricted_vars
def ensure_ro_rql(self, rql): """raise an exception if the given rql is not a select query""" first = rql.split(None, 1)[0].lower() if first in ('insert', 'set', 'delete'): raise Unauthorized(self._('only select queries are authorized'))