# HG changeset patch
# User Laurent Wouters <lwouters@cenotelie.fr>
# Date 1524241077 -7200
# Fri Apr 20 18:17:57 2018 +0200
# Node ID f22be300dee138245d51ff7ff6c72f7b40a54b9e
# Parent a6f0758c32df019a997d1b22ea10c390359c2e85
[rql] Introduces the new security model for RQL statements
This changes introduces the definition of a security model an its evaluation for
RQL statements. This change enables the production of a security model that can
be built and put in cache and then evaluated multiple time for differnt users
with different arguments for the corresponding statement. In this new model,
security checks are injected in rewritten RQL queries and the injected nodes can
be selectively activated upon SQL generation depending on the current security
evaluation for the current user.
# User Laurent Wouters <lwouters@cenotelie.fr>
# Date 1524241077 -7200
# Fri Apr 20 18:17:57 2018 +0200
# Node ID f22be300dee138245d51ff7ff6c72f7b40a54b9e
# Parent a6f0758c32df019a997d1b22ea10c390359c2e85
[rql] Introduces the new security model for RQL statements
This changes introduces the definition of a security model an its evaluation for
RQL statements. This change enables the production of a security model that can
be built and put in cache and then evaluated multiple time for differnt users
with different arguments for the corresponding statement. In this new model,
security checks are injected in rewritten RQL queries and the injected nodes can
be selectively activated upon SQL generation depending on the current security
evaluation for the current user.
@@ -0,0 +1,828 @@
1 +# copyright 2003-2016 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 + 19 +""" 20 +This module defines the security model and security checking plan for a RQL statement 21 +""" 22 +from cubicweb.rqlrewrite import ArgumentValue 23 +from rql.nodes import Exists, Not, Relation, VariableRef 24 +from rql.stmts import Select, Union 25 + 26 +READ_ONLY_RTYPES = {'eid', 'has_text', 'is', 'is_instance_of', 'identity'} 27 + 28 + 29 +class SecurityModel: 30 + """ 31 + Defines the security model of a RQL SELECT statement 32 + This security model keeps track of the entity and relation schemas that are relevant. 33 + When the security permissions applied to these entities and relations are modified, 34 + the security model must be invalidated 35 + """ 36 + 37 + def __init__(self, statement): 38 + """ 39 + Initializes an empty security model 40 + :param statement: The original RQL statement 41 + """ 42 + self.statement = statement 43 + # alternative re-written statements 44 + self.statements = [] 45 + # The children security models for sub-queries 46 + self.children = [] 47 + # The map of the relevant entity types to their cached security constraints 48 + self.entity_types = {} 49 + # The map of the relevant relation types to their cached security constraints 50 + self.relation_types = {} 51 + # The map of variables of interest 52 + self.variables = {} 53 + # Map of solution to their constraints 54 + self.solutions = [] 55 + # The argument injectors 56 + self.injectors = [] 57 + 58 + def uses_entity_or_relation(self, name): 59 + """ 60 + Determines whether this security model relies on the security constraints of an entity or relation with the specified name 61 + :param name: The name of an entity or relation 62 + :return: Whether the name corresponds to an entity or relation used in this security model 63 + """ 64 + return self.entity_types.has_key(name) or self.relation_types.has_key(name) 65 + 66 + def resolve_entity_security(self, schema, entity_name): 67 + """ 68 + Resolves the security constraints of an entity type 69 + :param schema: The current schema 70 + :param entity_name: The name of the entity 71 + :return: The corresponding EntitySecurity 72 + """ 73 + if self.entity_types.has_key(entity_name): 74 + return self.entity_types[entity_name] 75 + eschema = schema.eschema(entity_name) 76 + groups = eschema.get_groups('read') 77 + expressions = eschema.get_rqlexprs('read') 78 + # Create the constraint 79 + constraints = EntitySecurity(entity_name, eschema) 80 + constraints.groups.extend(groups) 81 + for exp in expressions: 82 + constraints.expressions.append(EntitySecurityExpression(exp)) 83 + self.entity_types[entity_name] = constraints 84 + return constraints 85 + 86 + def resolve_relation_security(self, schema, relation_name, get_left_type, get_right_type): 87 + """ 88 + Resolves the security constraints of a relation 89 + :param schema: The current schema 90 + :param relation_name: The name of the relation 91 + :param get_left_type: Getter for the name of the type on the relation's left 92 + :param get_right_type: Getter for the name of the type on the relation's right 93 + :return: The list of corresponding RelationDefinitionSecurity 94 + """ 95 + if not self.relation_types.has_key(relation_name): 96 + self.relation_types[relation_name] = RelationSecurity(relation_name, schema.rschema(relation_name)) 97 + constraints = self.relation_types[relation_name] 98 + 99 + if constraints.schema.final: 100 + lhs = get_left_type() 101 + if lhs is not None: 102 + entity_schema = schema.eschema(lhs) 103 + relation_definition = entity_schema.rdef(constraints.schema) 104 + return [constraints.resolve_case(relation_definition)] 105 + else: 106 + return [constraints.resolve_case(relation_definition) 107 + for types, relation_definition in constraints.schema.rdefs.items() 108 + ] 109 + else: 110 + lhs = get_left_type() 111 + rhs = get_right_type() 112 + if lhs is not None and rhs is not None: 113 + left_type = schema.eschema(lhs) 114 + right_type = schema.eschema(rhs) 115 + relation_definition = constraints.schema.rdef(left_type, right_type) 116 + return [constraints.resolve_case(relation_definition)] 117 + else: 118 + left_type = schema.eschema(lhs) if lhs is not None else None 119 + right_type = schema.eschema(rhs) if rhs is not None else None 120 + return [ 121 + constraints.resolve_case(relation_definition) 122 + for types, relation_definition in constraints.schema.rdefs.items() 123 + if (left_type is None or left_type == types[0]) and (right_type is None or right_type == types[1]) 124 + ] 125 + 126 + def inject_constraints(self, connection, args): 127 + """ 128 + Injects the found RQL constraints into the statement using the specified RQL rewriter 129 + :param connection: The current connection 130 + :param args: The current arguments 131 + :return: The set of variables with no invariant 132 + """ 133 + # Performs injection in children models 134 + result = set() 135 + for child in self.children: 136 + sub = child.inject_constraints(connection, args) 137 + result = result.union(sub) 138 + 139 + # Bail-out if this is not a SELECT statement 140 + if not isinstance(self.statement, Select): 141 + return result 142 + 143 + no_invariant = set() 144 + injectables, restricted = self._get_injectables() 145 + if len(restricted) > 0: 146 + # Rewrite the statement with the injectable constraints 147 + rewritten, new_args = connection.rql_rewriter.insert_local_checks(self.statement, args, injectables, 148 + restricted, no_invariant) 149 + # New arguments ? 150 + if len(new_args) > len(args): 151 + # new arguments generated ... 152 + for name, value in new_args.items(): 153 + if name in args: 154 + continue 155 + if name == "login": 156 + self.injectors.append(ArgumentLoginInjector()) 157 + elif isinstance(value, ArgumentValue) and value.type_hint == "CWUser": 158 + self.injectors.append(ArgumentUserEIDInjector(name)) 159 + else: 160 + self.injectors.append(ArgumentValueInjector(name, value)) 161 + if len(rewritten) > 0: 162 + self.statement = rewritten[0] 163 + self.statements.extend(rewritten[1:]) 164 + # Recompute the solutions ... 165 + connection.repo.vreg.solutions(connection, self.statement.parent, new_args) 166 + return result.union(no_invariant) 167 + 168 + def _get_injectables(self): 169 + """ 170 + Gets all the RQL expressions that have to be injected (not all will be activated), and a set of the restricted variables 171 + :return: A tuple (injectables, restricted) where 172 + injectables is a map (constraints |-> [solution]) 173 + with constraints is a list [constraint1, constraint2, ...] where each constraint is a tuple (variable, expressions) 174 + where expressions is a tuple where each member is an RQL expression 175 + and restricted is the set of variable names that have applicable RQL expressions 176 + """ 177 + injectables = {} 178 + restricted = set() 179 + for sc in self.solutions: 180 + sc.get_injectables(injectables, restricted) 181 + return injectables, restricted 182 + 183 + def evaluate(self, connection, arguments, security=True): 184 + """ 185 + Evaluates this security model and its children against the current user and arguments 186 + :param connection: The current connection 187 + :param arguments: The current arguments 188 + :param security: Whether the security is active 189 + :return: The evaluation result 190 + """ 191 + # Check that security is on 192 + if not connection.read_security or not security: 193 + return SecurityModelEvaluation(self, connection, self.variables, arguments) 194 + return self._evaluate_tree(connection, arguments) 195 + 196 + def _evaluate_tree(self, connection, arguments): 197 + """ 198 + Performs the evaluation of this security model and its children against the current user and arguments 199 + :param connection: The current connection 200 + :param arguments: The current arguments 201 + :return: The evaluation result 202 + """ 203 + result = SecurityModelEvaluation(self, connection, self.variables, arguments) 204 + for injector in self.injectors: 205 + injector.inject(connection, arguments) 206 + for child in self.children: 207 + sub_result = child._evaluate_tree(connection, arguments) 208 + result.children.append(sub_result) 209 + result.problems.extend(sub_result.problems) 210 + result.activate_expressions(sub_result.rql_expressions) 211 + self._evaluate_content(result) 212 + return result 213 + 214 + def _evaluate_content(self, result): 215 + """ 216 + Evaluates this security model only against the current user and arguments 217 + :param result: The evaluation result to build 218 + :return: Nothing 219 + """ 220 + # First, evaluate the constraints for each solution 221 + for sc in self.solutions: 222 + sc.evaluate(result) 223 + # Check that all arguments are allowed 224 + for argument in result.arguments.values(): 225 + if not argument.is_authorised and len(argument.solutions) == 0: 226 + result.problems.append("No read access on %r with eid %i." % (argument.name, argument.value)) 227 + # Checks that there are still allowed typing solutions 228 + if isinstance(self.statement, Select) and len(result.solutions) == 0: 229 + result.problems.append("No remaining authorised typing solution") 230 + 231 + 232 +class ArgumentInjector: 233 + """ 234 + Represents an entity able to inject query arguments upon evaluation of a security model 235 + """ 236 + 237 + def __init__(self, name): 238 + """ 239 + Initializes this injector 240 + :param name: The name of the argument to be injected 241 + """ 242 + self.name = name 243 + 244 + def inject(self, connection, arguments): 245 + """ 246 + Executes this injection job 247 + :param connection: The current connection 248 + :param arguments: The target arguments dictionary 249 + :return: Nothing 250 + """ 251 + raise NotImplemented 252 + 253 + 254 +class ArgumentLoginInjector(ArgumentInjector): 255 + """ 256 + Injects the login of the current user 257 + """ 258 + 259 + def __init__(self): 260 + """ 261 + Initializes this injector 262 + """ 263 + ArgumentInjector.__init__(self, "login") 264 + 265 + def inject(self, connection, arguments): 266 + arguments[self.name] = connection.user.name 267 + 268 + 269 +class ArgumentUserEIDInjector(ArgumentInjector): 270 + """ 271 + Injects the EID of the current user 272 + """ 273 + 274 + def __init__(self, name): 275 + """ 276 + Initializes this injector 277 + :param name: The name of the argument to be injected 278 + """ 279 + ArgumentInjector.__init__(self, name) 280 + 281 + def inject(self, connection, arguments): 282 + arguments[self.name] = connection.user.eid 283 + 284 + 285 +class ArgumentValueInjector(ArgumentInjector): 286 + """ 287 + Injects the EID of the current user 288 + """ 289 + 290 + def __init__(self, name, value): 291 + """ 292 + Initializes this injector 293 + :param name: The name of the argument to be injected 294 + :param value: The value for the argument 295 + """ 296 + ArgumentInjector.__init__(self, name) 297 + self.value = value.value if isinstance(value, ArgumentValue) else value 298 + 299 + def inject(self, connection, arguments): 300 + arguments[self.name] = self.value 301 + 302 + 303 +class EntitySecurity: 304 + """ 305 + Defines the security constraints specific to an entity type 306 + """ 307 + 308 + def __init__(self, type_name, schema): 309 + """ 310 + Initializes this constraint 311 + :param type_name: The type's name 312 + :param schema: The entity's schema 313 + """ 314 + self.type_name = type_name 315 + self.schema = schema 316 + # List of security groups 317 + # The requesting user must be in one of them to satisfy the constraint 318 + self.groups = [] 319 + # Or, one of these expressions must be satisfied 320 + self.expressions = [] 321 + 322 + def get_injectables(self): 323 + """ 324 + Gets the injectable RQL expressions associated to this entity type 325 + :return: A tuple with the expressions as members, or None is there is none 326 + """ 327 + if len(self.expressions) == 0: 328 + return None 329 + return tuple(e.expression for e in self.expressions) 330 + 331 + def evaluate(self, evaluation, solution, variable): 332 + """ 333 + Evaluates the security constraints for a variable of this type 334 + :param evaluation: The current evaluation 335 + :param solution: The current typing solution 336 + :param variable: The name of the variable that is of this type 337 + :return None if the constraints cannot be verified, True if they unconditionally are, or the list of expressions to be activated 338 + """ 339 + if self.type_name == "Password": 340 + evaluation.problems.append("Password selection is not allowed (%s)" % variable) 341 + return None 342 + # Check the groups 343 + for g in self.groups: 344 + if evaluation.check_group(g): 345 + if evaluation.arguments.has_key(variable): 346 + argument = evaluation.arguments[variable] 347 + argument.allow_in(solution) 348 + return True 349 + # Not in groups ... do we have RQL expressions? 350 + if len(self.expressions) == 0: 351 + return None 352 + # Is this an argument for which we can evaluate the expressions? 353 + if not evaluation.arguments.has_key(variable): 354 + # Not an arguments, activate the expressions 355 + return set(self.expressions) 356 + argument = evaluation.arguments[variable] 357 + # Authorized by the transaction => OK 358 + if argument.is_authorised: 359 + return True 360 + # No, try to evaluate the expressions: 361 + for ec in self.expressions: 362 + if ec.evaluate(evaluation, variable): 363 + # Found a matching expression => OK 364 + argument.allow_in(solution) 365 + return True 366 + # all failed ... 367 + return None 368 + 369 + 370 +class EntitySecurityExpression: 371 + """ 372 + Represents a RQL expression for checking the security of a specific type of entity 373 + """ 374 + 375 + def __init__(self, expression): 376 + """ 377 + Initializes this constraint 378 + :param expression: The encapsulated expression 379 + """ 380 + self.expression = expression 381 + self.expression.injection_marker = object() 382 + 383 + def evaluate(self, evaluation, variable): 384 + """ 385 + Evaluates the security constraints for this expression 386 + :param evaluation: The current evaluation 387 + :param variable: The name of the variable that is of this type 388 + :return Whether the expression is verified 389 + """ 390 + return self.expression.check(evaluation.connection, evaluation.arguments[variable].value) 391 + 392 + def is_node_activated(self, node): 393 + """ 394 + Gets whether the specified injected RQL node is to be activated by this expression 395 + (it corresponds to this expression) 396 + :param node: The injected RQL node 397 + :return: Whether the node shall be activated 398 + """ 399 + return self.expression.injection_marker == node.injection_marker 400 + 401 + 402 +class RelationSecurity: 403 + """ 404 + Defines the security constraints specific to a relation 405 + """ 406 + 407 + def __init__(self, relation_name, schema): 408 + """ 409 + Initializes this constraint 410 + :param relation_name: The relation's name 411 + :param schema: The relation's schema 412 + """ 413 + self.relation_name = relation_name 414 + self.schema = schema 415 + # The different typing cases 416 + self.cases = {} 417 + 418 + def resolve_case(self, definition): 419 + """ 420 + Resolves the constraints for a specific definition of this relation 421 + :param definition: The definition to look for 422 + :return: The constraints 423 + """ 424 + if self.cases.has_key(definition): 425 + return self.cases[definition] 426 + result = RelationDefinitionSecurity(definition) 427 + self.cases[definition] = result 428 + return result 429 + 430 + 431 +class RelationDefinitionSecurity: 432 + """ 433 + Defines the security constraints specific to a typing case for a relation 434 + """ 435 + 436 + def __init__(self, definition): 437 + """ 438 + Initializes this structure 439 + :param definition: The relation definition for the typing cases 440 + """ 441 + self.definition = definition 442 + # The various typing cases for this relation definition 443 + self.typing = (definition.subject.type, definition.object.type) 444 + # List of security groups 445 + # The requesting user must be in one of them to satisfy the constraint 446 + self.groups = definition.get_groups('read') 447 + 448 + def evaluate(self, evaluation): 449 + """ 450 + Evaluates the security constraints for this relation definition 451 + :param evaluation: The current evaluation 452 + :return True if all constraints are satisfied 453 + """ 454 + for g in self.groups: 455 + if evaluation.check_group(g): 456 + return True 457 + evaluation.problems.append("Cannot read relation") 458 + return False 459 + 460 + 461 +class SecurityModelArgument: 462 + """ 463 + Represents the data for the security model about an argument for a RQL statement 464 + """ 465 + 466 + def __init__(self, name, value, is_authorised): 467 + """ 468 + Initializes this argument 469 + :param name: The argument's name 470 + :param value: The argument's value 471 + :param is_authorised: Whether the value pointed to by the argument has been explicitly authorised 472 + """ 473 + self.name = name 474 + self.value = value 475 + self.is_authorised = is_authorised 476 + # The typing solutions 477 + self.solutions = [] 478 + 479 + def allow_in(self, solution): 480 + """ 481 + Allows access to the value represented by this argument in the specified typing solution 482 + :param solution: A typing solution 483 + :return: Nothing 484 + """ 485 + if not solution in self.solutions: 486 + self.solutions.append(solution) 487 + 488 + 489 +class SecurityModelEvaluation: 490 + """ 491 + Represents an evaluation of a security model 492 + """ 493 + 494 + def __init__(self, model, connection, variables, arguments): 495 + """ 496 + Initializes this structure 497 + :param model: The evaluated security model 498 + :param connection: The current connection 499 + :param variables: The variables of interest in the statement 500 + :param arguments: The arguments passed for the evaluation 501 + """ 502 + self.model = model 503 + self.connection = connection 504 + # The current user 505 + self.user = connection.user 506 + # The children evaluation 507 + self.children = [] 508 + # The evaluation arguments 509 + self.arguments = {} 510 + # The allowed typing solutions 511 + self.solutions = [] 512 + # The activated RQL expressions 513 + self.rql_expressions = set() 514 + # The security problems raised during the evaluation 515 + self.problems = [] 516 + # The cached group checks 517 + self.groups = {} 518 + # Prepare the evaluation arguments 519 + for var, node in variables.items(): 520 + if node is not None: 521 + eid = int(node.eval(arguments)) 522 + self.arguments[var] = SecurityModelArgument(var, eid, connection.added_in_transaction(eid)) 523 + 524 + def check_group(self, group): 525 + """ 526 + Checks whether the current user belongs to the specified group 527 + :param group: The group to check 528 + :return: Whether the current user belongs to the specified group 529 + """ 530 + if self.groups.has_key(group): 531 + return self.groups[group] 532 + result = self.user.is_in_group(group) 533 + self.groups[group] = result 534 + return result 535 + 536 + def get_evaluation_for(self, statement): 537 + """ 538 + Retrieves the security evaluation specific to the specified statement 539 + :param statement: The RQL statement 540 + :return: The evaluation 541 + """ 542 + if statement == self.model.statement or statement in self.model.statements: 543 + return self 544 + for child in self.children: 545 + result = child.get_evaluation_for(statement) 546 + if result is not None: 547 + return result 548 + return None 549 + 550 + def activate_expressions(self, expressions): 551 + """ 552 + Activates the specified expressions in this evaluation 553 + :param expressions: The set of expressions to activate 554 + :return: Nothing 555 + """ 556 + self.rql_expressions = self.rql_expressions.union(expressions) 557 + 558 + def is_node_activated(self, node): 559 + """ 560 + Gets whether the specified injected RQL node is to be activated by this evaluation 561 + :param node: The injected RQL node 562 + :return: Whether the node shall be activated 563 + """ 564 + for expression in self.rql_expressions: 565 + if expression.is_node_activated(node): 566 + return True 567 + return False 568 + 569 + 570 +class SolutionConstraints: 571 + """ 572 + Defines the security constraints for a typing solution of a RQL SELECT statement 573 + """ 574 + 575 + def __init__(self, solution): 576 + """ 577 + Initializes empty constraints for the specified solution 578 + :param solution: The typing solution being constrained 579 + """ 580 + self.solution = solution 581 + # Map of variables to their constraints 582 + self.variables = {} 583 + # The constraints on the relations for this typing solution 584 + self.relations = {} 585 + 586 + def get_injectables(self, injectables, restricted): 587 + """ 588 + Gets the injectable RQL expressions associated for this solution 589 + :param injectables: The resulting dictionary (constraints |-> [solution]) to fill 590 + :param restricted: The list of the restricted variables (with applicable expressions) 591 + :return: Nothing 592 + """ 593 + my_constraints = [] 594 + for vc in self.variables.values(): 595 + constraints = vc.get_injectables() 596 + if constraints is None: 597 + continue 598 + # Register the restricted variable 599 + restricted.add(constraints[0]) 600 + my_constraints.append(constraints) 601 + my_constraints = tuple(my_constraints) 602 + # Register the constraints for this solution 603 + injectables.setdefault(my_constraints, []).append(self.solution) 604 + 605 + def evaluate(self, evaluation): 606 + """ 607 + Evaluates the security constraints for this solution 608 + :param evaluation: The current evaluation 609 + :return Nothing 610 + """ 611 + activated = set() 612 + for var, vc in self.variables.items(): 613 + r = vc.evaluate(evaluation, self.solution) 614 + if r is None: 615 + # This solution is dis-allowed 616 + return 617 + elif isinstance(r, set): 618 + # this is conditioned to expressions being activated 619 + activated = activated.union(r) 620 + elif r: 621 + # all sub-constrained reported matched => continue 622 + continue 623 + else: 624 + # should not happen 625 + return 626 + 627 + for relation, rc in self.relations.items(): 628 + if not rc.evaluate(evaluation): 629 + # This solution is dis-allowed 630 + return 631 + 632 + # Looks like this solution is allowed (provision the expressions if any) 633 + evaluation.solutions.append(self.solution) 634 + evaluation.activate_expressions(activated) 635 + 636 + 637 +class VariableConstraints: 638 + """ 639 + Defines the security constraints specific to a variable in a typing solution of a RQL SELECT statement 640 + """ 641 + 642 + def __init__(self, name, is_eid_evaluable, security): 643 + """ 644 + Initializes this constraint 645 + :param name: The name of the constrained variable 646 + :param is_eid_evaluable: Whether the variable is evaluable (at execution time) to an EID 647 + In that case, the associated RQL expressions may be statically checked, 648 + instead of being injected within the final SQL statement. 649 + :param security: The security constraint for the variable's type 650 + """ 651 + self.name = name 652 + self.is_eid_evaluable = is_eid_evaluable 653 + self.security = security 654 + 655 + def get_injectables(self): 656 + """ 657 + Gets the injectable RQL expressions associated to this variable, if any 658 + :return: A tuple (name, expressions) where 659 + name is the variable's name and 660 + expressions is the tuple of RQL expression if there are appropriate expression, 661 + None otherwise 662 + """ 663 + if self.is_eid_evaluable: 664 + return None 665 + expressions = self.security.get_injectables() 666 + if expressions is None: 667 + return None 668 + return self.name, expressions 669 + 670 + def evaluate(self, evaluation, solution): 671 + """ 672 + Evaluates the security constraints for this variable 673 + :param evaluation: The current evaluation 674 + :param solution: The current typing solution 675 + :return None if the constraints cannot be verified, True if they unconditionally are, or the list of expressions to be activated 676 + """ 677 + return self.security.evaluate(evaluation, solution, self.name) 678 + 679 + 680 +class RelationConstraints: 681 + """ 682 + Represents the set of constraints on a specific relation for a solution 683 + """ 684 + 685 + def __init__(self, security): 686 + """ 687 + Initializes this structure 688 + :param security: The security constraints for the associated relation 689 + """ 690 + self.security = security 691 + 692 + def evaluate(self, evaluation): 693 + """ 694 + Evaluates the security constraints for this relation 695 + :param evaluation: The current evaluation 696 + :return True if all constraints are satisfied 697 + """ 698 + for s in self.security: 699 + if not s.evaluate(evaluation): 700 + return False 701 + return True 702 + 703 + 704 +def build_security_model(connection, statement): 705 + """ 706 + Builds the security model of a RQL statement 707 + :param connection: The current connection 708 + :param statement: The RQL statement 709 + :return: The security model 710 + """ 711 + with connection.security_enabled(read=False): 712 + schema = connection.repo.schema 713 + return build_security_model_for(statement, schema, None) 714 + 715 + 716 +def build_security_model_for(statement, schema, root_model): 717 + """ 718 + Builds the security model of a RQL statement 719 + :param statement: The RQL statement 720 + :param schema: The current schema 721 + :param root_model: The root security model, if any (use to resolve the schema-related data) 722 + :return: The security model 723 + """ 724 + if isinstance(statement, Union): 725 + return _build_security_model_for_union(statement, schema, root_model) 726 + elif isinstance(statement, Select): 727 + return _build_security_model_for_select(statement, schema, root_model) 728 + return SecurityModel(statement) 729 + 730 + 731 +def _build_security_model_for_union(union, schema, root_model): 732 + """ 733 + Builds the security model of a UNION statement 734 + :param union: The RQL UNION statement 735 + :param schema: The current schema 736 + :param root_model: The root security model, if any (use to resolve the schema-related data) 737 + :return: The security model 738 + """ 739 + union_model = SecurityModel(union) 740 + # Setup the root model if required 741 + root_model = root_model if root_model is not None else union_model 742 + for select in union.children[:]: 743 + union_model.children.append(_build_security_model_for_select(select, schema, root_model)) 744 + return union_model 745 + 746 + 747 +def _build_security_model_for_select(select, schema, root_model): 748 + """ 749 + Builds the security model of a SELECT statement 750 + :param select: The RQL SELECT statement 751 + :param schema: The current schema 752 + :param root_model: The root security model, if any (use to resolve the schema-related data) 753 + :return: The security model 754 + """ 755 + model = SecurityModel(select) 756 + # Setup the root model if required 757 + root_model = root_model if root_model is not None else model 758 + # Build the children models for the sub queries 759 + for subquery in select.with_: 760 + model.children.append(build_security_model_for(subquery.query, schema, root_model)) 761 + 762 + # Filter out the variables that are not of interest from a security standpoint 763 + for var_name in select.defined_vars: 764 + # don't insert security on variable only referenced by 'NOT X relation Y' or 765 + # 'NOT EXISTS(X relation Y)' 766 + var_info = select.defined_vars[var_name].stinfo 767 + if var_info['selected'] or ( 768 + len([r for r in var_info['relations'] 769 + if (not schema.rschema(r.r_type).final 770 + and ((isinstance(r.parent, Exists) and r.parent.neged(strict=True)) 771 + or isinstance(r.parent, Not)))]) 772 + != 773 + len(var_info['relations'])): 774 + model.variables[var_name] = var_info['constnode'] 775 + 776 + for solution in select.solutions: 777 + # Register the solution and the associated constraints 778 + solution_constraints = SolutionConstraints(solution) 779 + model.solutions.append(solution_constraints) 780 + # Look for the constraints on the variables 781 + for var_name in model.variables.keys(): 782 + type_name = solution[var_name] 783 + eschema = schema.eschema(type_name) 784 + if eschema.final and type_name != "Password": 785 + # this is a primitive type (and not password), do nothing 786 + # if the type is Password, register the variable constraint for later reporting 787 + continue 788 + is_eid_evaluable = model.variables[var_name] is not None 789 + solution_constraints.variables[var_name] = VariableConstraints( 790 + var_name, 791 + is_eid_evaluable, 792 + root_model.resolve_entity_security(schema, type_name)) 793 + # Look for the constraints on the relations 794 + if select.where is not None: 795 + for relation_node in select.where.iget_nodes(Relation): 796 + if relation_node.r_type in READ_ONLY_RTYPES: 797 + # This relation si read-only => do nothing 798 + continue 799 + solution_constraints.relations[relation_node.r_type] = RelationConstraints( 800 + root_model.resolve_relation_security( 801 + schema, 802 + relation_node.r_type, 803 + lambda rn=relation_node, s=solution: _get_relation_left_type(rn, s), 804 + lambda rn=relation_node, s=solution: _get_relation_right_type(rn, s))) 805 + 806 + return model 807 + 808 + 809 +def _get_relation_left_type(relation_node, solution): 810 + """ 811 + Gets the type of the left-hand side of a Relation RQL node 812 + :param relation_node: A Relation RQL node 813 + :param solution: The current solution 814 + :return: The type of the left-hand side (or None) if it cannot be known 815 + """ 816 + node = relation_node.children[0] 817 + return solution[node.name] if isinstance(node, VariableRef) else None 818 + 819 + 820 +def _get_relation_right_type(relation_node, solution): 821 + """ 822 + Gets the type of the right-hand side of a Relation RQL node 823 + :param relation_node: A Relation RQL node 824 + :param solution: The current solution 825 + :return: The type of the right-hand side (or None) if it cannot be known 826 + """ 827 + node = relation_node.children[1].children[0] 828 + return solution[node.name] if isinstance(node, VariableRef) else None
@@ -249,11 +249,11 @@
829 raise NotImplementedError(self) 830 831 # RQL query api ############################################################ 832 833 def syntax_tree_search(self, cnx, union, 834 - args=None, cachekey=None, debug=0): 835 + args=None, cachekey=None, security=None, debug=0): 836 """return result from this source for a rql query (actually from a rql 837 syntax tree and a solution dictionary mapping each used variable to a 838 possible type). If cachekey is given, the query necessary to fetch the 839 results (but not the results themselves) may be cached using this key. 840 """
@@ -501,11 +501,11 @@
841 return authentifier.authenticate(cnx, login, **kwargs) 842 except AuthenticationError: 843 continue 844 raise AuthenticationError() 845 846 - def syntax_tree_search(self, cnx, union, args=None, cachekey=None): 847 + def syntax_tree_search(self, cnx, union, args=None, cachekey=None, security=None, debug=0): 848 """return result from this source for a rql query (actually from 849 a rql syntax tree and a solution dictionary mapping each used 850 variable to a possible type). If cachekey is given, the query 851 necessary to fetch the results (but not the results themselves) 852 may be cached using this key.
@@ -513,19 +513,19 @@
853 assert dbg_st_search(self.uri, union, args, cachekey) 854 # remember number of actually selected term (sql generation may append some) 855 if cachekey is None: 856 self.no_cache += 1 857 # generate sql query if we are able to do so (not supported types...) 858 - sql, qargs, cbs = self._rql_sqlgen.generate(union, args) 859 + sql, qargs, cbs = self._rql_sqlgen.generate(union, args, security) 860 else: 861 # sql may be cached 862 try: 863 sql, qargs, cbs = self._cache[cachekey] 864 self.cache_hit += 1 865 except KeyError: 866 self.cache_miss += 1 867 - sql, qargs, cbs = self._rql_sqlgen.generate(union, args) 868 + sql, qargs, cbs = self._rql_sqlgen.generate(union, args, security) 869 self._cache[cachekey] = sql, qargs, cbs 870 args = self.merge_args(args, qargs) 871 assert isinstance(sql, string_types), repr(sql) 872 cursor = cnx.system_sql(sql, args) 873 results = self.process_result(cursor, cnx, cbs)
@@ -736,12 +736,16 @@
874 self.union_sql = self.noparen_union_sql 875 self._lock = threading.Lock() 876 if attrmap is None: 877 attrmap = {} 878 self.attr_map = attrmap 879 + self._args = None 880 + self._query_attrs = None 881 + self._state = None 882 + self._security = None 883 884 - def generate(self, union, args=None): 885 + def generate(self, union, args=None, security=None): 886 """return SQL queries and a variable dictionary from a RQL syntax tree 887 888 :partrqls: a list of couple (rqlst, solutions) 889 :args: optional dictionary with values of substitutions used in the query 890
@@ -751,10 +755,11 @@
891 args = {} 892 self._lock.acquire() 893 self._args = args 894 self._query_attrs = {} 895 self._state = None 896 + self._security = security 897 # self._not_scope_offset = 0 898 try: 899 # union query for each rqlst / solution 900 sql = self.union_sql(union) 901 # we are done
@@ -810,10 +815,15 @@
902 elif vscope is not scope: 903 scope = common_parent(scope, vscope).scope 904 if scope is None: 905 scope = select 906 scope.add_restriction(restr) 907 + 908 + # get the security evaluation for this select statement 909 + security = self._security.get_evaluation_for(select) if self._security is not None else None 910 + # get the activate RQL security constraints: 911 + activated = list(security.rql_expressions) if security is not None else [] 912 # remember selection, it may be changed and have to be restored 913 origselection = select.selection[:] 914 # check if the query will have union subquery, if it need sort term 915 # selection (union or distinct query) and wrapping (union with groups) 916 needwrap = False
@@ -856,11 +866,11 @@
917 needalias = True 918 self._in_wrapping_query = False 919 self._state = state 920 try: 921 sql = self._solutions_sql(select, sols, distinct, 922 - needalias or needwrap) 923 + needalias or needwrap, activated) 924 # generate groups / having before wrapping query selection to get 925 # correct column aliases 926 self._in_wrapping_query = needwrap 927 if groups: 928 # no constant should be inserted in GROUP BY else the backend
@@ -926,11 +936,11 @@
929 update_source_cb_stack(state, select, vref, stack) 930 state.subquery_source_cb[selectidx] = stack 931 except KeyError: 932 continue 933 934 - def _solutions_sql(self, select, solutions, distinct, needalias): 935 + def _solutions_sql(self, select, solutions, distinct, needalias, activated): 936 sqls = [] 937 for solution in solutions: 938 self._state.reset(solution) 939 # visit restriction subtree 940 if select.where is not None:
@@ -1010,19 +1020,43 @@
941 return csql 942 return 'NOT (%s)' % csql 943 944 def visit_exists(self, exists): 945 """generate SQL name for a exists subquery""" 946 + if hasattr(exists, "injection_marker"): 947 + if not self._is_injectable_activated(exists): 948 + # this is an injectable that has not been activated 949 + return '' 950 sqls = [] 951 for dummy in self._state.iter_exists_sols(exists): 952 sql = self._visit_exists(exists) 953 if sql: 954 sqls.append(sql) 955 if not sqls: 956 return '' 957 return 'EXISTS(%s)' % ' UNION '.join(sqls) 958 959 + def _is_injectable_activated(self, node): 960 + """ 961 + Determines whether the specified injectable RQL node is activated for SQL generation 962 + :param node: An injectable RQL node 963 + :return: Whether the injectable is activated 964 + """ 965 + if self._security is None: 966 + # not security => do not activate 967 + return False 968 + # find the parent select node 969 + select = node 970 + while not isinstance(select, Select): 971 + select = select.parent 972 + # get the security evaluation for this select statement 973 + security = self._security.get_evaluation_for(select) 974 + if security is None: 975 + # no security for this query => do not activate 976 + return False 977 + return security.is_node_activated(node) 978 + 979 def _visit_exists(self, exists): 980 self._state.push_scope(exists) 981 restriction = exists.children[0].accept(self) 982 restrictions, tables = self._state.pop_scope() 983 if restriction: