Katkat

Evaluate Ordered Access-Control Rules

StripeSoftware Engineer · Technical Screen

Last updated

Implement an access-control evaluator that parses rule strings with conditions and returns the first matching decision (ACCEPT or BLOCK).

Problem Statement

Access-control systems often evaluate incoming requests against an ordered list of security rules using first-match semantics. You need to implement an engine that parses these rules and determines the final decision for an incoming request.

Each rule is provided as a string in one of two formats:

  • Unconditional rule: " ALWAYS"
  • Conditional rule: " WHERE AND AND ... AND "

`` is either "ACCEPT" or "BLOCK".

Each `` is formatted as , separated by single spaces:

  • ``: A string representing the attribute key in the request.
  • : One of `==`, `!=`, , >=.
  • ``: Either an integer (e.g., 500, -10) or an unquoted string token (e.g., US, mobile).

A rule matches if all of its clauses evaluate to True for the given request payload. If an attribute referenced in a clause is missing from the request, that clause evaluates to False. Numeric comparisons (``, >=) evaluate to False if the attribute in the request is not an integer.

Rules must be evaluated sequentially in the order given. The action of the first matching rule is returned. If no rule matches, return default_action.

Interface

def evaluate_access(
    rules: list[str],
    request: dict[str, int | str],
    default_action: str = "BLOCK"
) -> str:
    pass

Input & Output Format

  • rules: A list of rule strings.
  • request: A dictionary mapping attribute names (strings) to values (integers or strings).
  • default_action: A string, either "ACCEPT" or "BLOCK" (default: "BLOCK").
  • Return: "ACCEPT" or "BLOCK".

Constraints

  • 0 <= len(rules) <= 1,000
  • Each rule contains between 0 and 20 conditions joined by " AND ".
  • request contains at most 100 keys.
  • Keys and string values consist of alphanumeric characters and underscores.
  • Integers fit in standard 64-bit signed integers.

Examples

Example 1

rules = [
    "BLOCK WHERE risk_score > 85",
    "ACCEPT WHERE country == US AND amount  5000",
    "ACCEPT ALWAYS"
]
request = {"country": "US", "amount": 500, "risk_score": 20}
# Output: "ACCEPT"

Explanation: The first rule fails (20 > 85 is False). The second rule matches because country == "US" and `500 85", "ACCEPT WHERE country == US AND amount 5000" ] request = {"country": "CA", "amount": 2000, "risk_score": 10}

Output: "BLOCK"

*Explanation:* No rules match. The engine falls back to `default_action`, which is `"BLOCK"`.

## Edge Cases to Consider

- Missing request fields referenced in rule predicates.
- Comparing mismatched types (e.g., `<` operator applied to a string attribute).
- Empty `rules` list returning `default_action`.
- Negative numbers in numeric comparison values.

Hints

Hint 1

Separate the logic into two decoupled steps: parsing a rule string into a structured representation, and evaluating a parsed rule against a request context.

Hint 2

Be careful when parsing values from string rules: distinguish integer literals from string literals so type-specific comparisons behave correctly.

Hint 3

Ensure short-circuit evaluation: within a single rule, stop evaluating clauses on the first False; across rules, stop and return immediately on the first matching rule.

Follow-up questions

  • How would you optimize rule evaluation if millions of requests need to be checked against the same fixed set of rules?
  • How would you extend the rule syntax to support nested boolean expressions with OR operators and parentheses (e.g., `A AND (B OR C)`)?
  • If rules are updated dynamically by multiple concurrent services, how would you ensure thread-safe, zero-downtime rule set reloads?