Stringify Symbol values in the scope transpiler
What does this MR do and why?
Gitlab::PolicyStore::ScopeTranspiler compiles a structured policy_scope Hash into a Rego program that decides which projects a policy applies to. Before compiling, a private deep_stringify(hash) method normalises the input, but it only stringified Hash keys. Values passed through untouched, including Symbols.
Two places in the transpiler compare values against String literals: match_mode (source["match_mode"] == "any" ? "any" : "all") and excludes_type?(type) (item["type"] == type, called with "personal" and "archived"). A caller passing { match_mode: :any } silently compiled to match mode "all", and { projects: { excluding: [{ type: :personal }] } } compiled without the personal-project exclusion. There is no error, just a wrong Rego program: the policy applies to a different set of projects than the author asked for.
The same policy_scope is compiled both from a caller's in-memory Ruby Hash, where Symbols are natural, and from that value after a jsonb database round trip, where only Strings survive. The two compilations must agree, so the fix extracts a stringify_value(value) method from the case that used to live inline in deep_stringify, adds a when Symbol then value.to_s branch, and recurses through arrays with stringify_value so hashes nested inside arrays are normalised too, not just direct Hash items.
The gem is path-sourced inside the monorepo, so there is no version bump and no lockfile change.
How to set up and validate locally
- Open a rails console in your GDK with
gdk rails console - Compile a scope Hash that uses Symbol values, and the same scope after a JSON round trip, which is what comes back from jsonb
symbol_scope = { match_mode: :any, groups: { including: [{ id: 10 }] } }
json_scope = JSON.parse(JSON.generate(symbol_scope))
symbol_program = Gitlab::PolicyStore::ScopeTranspiler.new(symbol_scope, policy_name: "Demo").transpile
json_program = Gitlab::PolicyStore::ScopeTranspiler.new(json_scope, policy_name: "Demo").transpile
symbol_program == json_program # => true- Confirm the comparison prints
true, because before this change the Symbol form compiledmatch_modeto"all"while the String form compiled it to"any", so the two programs disagreed - Compile a scope that excludes personal projects by Symbol, and check the program mentions
personal
personal_exclusion_scope = { projects: { excluding: [{ type: :personal }] } }
program = Gitlab::PolicyStore::ScopeTranspiler.new(personal_exclusion_scope, policy_name: "Demo").transpile
program.include?("personal") # => true- Confirm this prints
true, because before this change the Symbol:personalnever matched the String comparison inexcludes_type?and the exclusion was dropped from the program - Compile the String-valued equivalent and confirm it produces the identical program, since the fix only changes how Symbols are handled
string_exclusion_scope = { projects: { excluding: [{ type: "personal" }] } }
Gitlab::PolicyStore::ScopeTranspiler.new(string_exclusion_scope, policy_name: "Demo").transpile == program # => trueReferences
- Extracted from !249133 (merged) (open)
- Related to https://gitlab.com/gitlab-org/gitlab/-/work_items/604367