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

  1. Open a rails console in your GDK with gdk rails console
  2. 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
  1. Confirm the comparison prints true, because before this change the Symbol form compiled match_mode to "all" while the String form compiled it to "any", so the two programs disagreed
  2. 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
  1. Confirm this prints true, because before this change the Symbol :personal never matched the String comparison in excludes_type? and the exclusion was dropped from the program
  2. 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 # => true

References

Merge request reports

Loading
Loading