From 7bd5147258d0e4b394cfb3c9ff0d1920086f428d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 1 Jan 2026 10:47:40 +0000 Subject: [PATCH 1/5] Initial plan From c295747610fa42347fe63b65c1c59c627f820ddb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 1 Jan 2026 10:55:27 +0000 Subject: [PATCH 2/5] Add all missing matcher functions (keyMatch, keyGet, keyGet2, keyMatch3, keyMatch4, ipMatch, globMatch) Co-authored-by: hsluoyz <3787410+hsluoyz@users.noreply.github.com> --- README.md | 14 +- lib/casbin/enforcer.ex | 305 ++++++++++++++++++++++- test/enforcer/matcher_functions_test.exs | 177 +++++++++++++ 3 files changed, 488 insertions(+), 8 deletions(-) create mode 100644 test/enforcer/matcher_functions_test.exs diff --git a/README.md b/README.md index 2ac7892..4ae5612 100644 --- a/README.md +++ b/README.md @@ -339,14 +339,14 @@ Casbin-Ex supports the following access control models: Implement all [matchers' functions](https://casbin.org/docs/function): - [x] regexMatch -- [ ] keyMatch -- [ ] keyGet +- [x] keyMatch +- [x] keyGet - [x] keyMatch2 -- [ ] keyGet2 -- [ ] keyMatch3 -- [ ] keyMatch4 -- [ ] ipMatch -- [ ] globMatch +- [x] keyGet2 +- [x] keyMatch3 +- [x] keyMatch4 +- [x] ipMatch +- [x] globMatch ## Testing diff --git a/lib/casbin/enforcer.ex b/lib/casbin/enforcer.ex index 3fa1149..3653383 100644 --- a/lib/casbin/enforcer.ex +++ b/lib/casbin/enforcer.ex @@ -963,6 +963,70 @@ defmodule Casbin.Enforcer do @doc """ Returns `true` if `key1` matches the pattern of `key2`. + `key2` can contain a `*` wildcard. + + ## Examples + + iex> Enforcer.key_match?("/foo/bar", "/foo/*") + true + iex> Enforcer.key_match?("/foo/bar", "/foo*") + true + iex> Enforcer.key_match?("/foo", "/foo") + true + """ + @spec key_match?(String.t(), String.t()) :: boolean() + def key_match?(key1, key2) do + case String.split(key2, "*", parts: 2) do + [prefix, _] -> + if String.length(key1) >= String.length(prefix) do + String.starts_with?(key1, prefix) + else + key1 == prefix + end + + [_] -> + key1 == key2 + end + end + + @doc """ + Returns the matched part of `key1` based on pattern `key2`. + + For example, `/foo/bar/foo` matches `/foo/*` and returns `bar/foo`. + + ## Examples + + iex> Enforcer.key_get("/foo/bar", "/foo/*") + "bar" + iex> Enforcer.key_get("/foo/bar", "/foo*") + "/bar" + """ + @spec key_get(String.t(), String.t()) :: String.t() + def key_get(key1, key2) do + i = :binary.match(key2, "*") + + case i do + :nomatch -> + "" + + {pos, _} -> + if String.length(key1) > pos do + prefix = String.slice(key2, 0, pos) + + if String.starts_with?(key1, prefix) do + String.slice(key1, pos, String.length(key1) - pos) + else + "" + end + else + "" + end + end + end + + @doc """ + Returns `true` if `key1` matches the pattern of `key2`. + Returns `false` otherwise. `key_match2?/2` can handle three types of path / patterns : @@ -996,6 +1060,238 @@ defmodule Casbin.Enforcer do end end + @doc """ + Returns the value matched by the path variable in `key2`. + + For example, `/resource1` matches `/:resource` and if `path_var` is `"resource"`, + then `"resource1"` will be returned. + + ## Examples + + iex> Enforcer.key_get2("/resource1", "/:resource", "resource") + "resource1" + iex> Enforcer.key_get2("/myid/using/myresid", "/:id/using/:resId", "id") + "myid" + iex> Enforcer.key_get2("/myid/using/myresid", "/:id/using/:resId", "resId") + "myresid" + """ + @spec key_get2(String.t(), String.t(), String.t()) :: String.t() + def key_get2(key1, key2, path_var) do + key2 = String.replace(key2, "/*", "/.*") + + with {:ok, r1} <- Regex.compile(":[^/]+"), + keys <- Regex.scan(r1, key2) |> Enum.map(fn [k] -> k end), + key2 <- Regex.replace(r1, key2, "([^/]+)"), + {:ok, r2} <- Regex.compile("^" <> key2 <> "$"), + matches <- Regex.run(r2, key1) do + if matches && length(matches) > 1 do + values = Enum.drop(matches, 1) + + keys + |> Enum.zip(values) + |> Enum.find_value("", fn {k, v} -> + if String.slice(k, 1..-1//1) == path_var, do: v + end) + else + "" + end + else + _ -> "" + end + end + + @doc """ + Returns `true` if `key1` matches the pattern of `key2`. + + Similar to `key_match2?/2` but uses `{resource}` syntax instead of `:resource`. + + ## Examples + + iex> Enforcer.key_match3?("/foo/bar", "/foo/*") + true + iex> Enforcer.key_match3?("/resource1", "/{resource}") + true + iex> Enforcer.key_match3?("/myid/using/myresid", "/{id}/using/{resId}") + true + """ + @spec key_match3?(String.t(), String.t()) :: boolean() + def key_match3?(key1, key2) do + key2 = String.replace(key2, "/*", "/.*") + + with {:ok, r1} <- Regex.compile("\\{[^/]+\\}"), + match <- Regex.replace(r1, key2, "[^/]+"), + {:ok, r2} <- Regex.compile("^" <> match <> "$") do + Regex.match?(r2, key1) + else + _ -> false + end + end + + @doc """ + Returns `true` if `key1` matches the pattern of `key2`. + + Similar to `key_match3?/2` but enforces that repeated parameter names must have + the same value. For example, `/parent/123/child/123` matches `/parent/{id}/child/{id}` + but `/parent/123/child/456` does not. + + ## Examples + + iex> Enforcer.key_match4?("/parent/123/child/123", "/parent/{id}/child/{id}") + true + iex> Enforcer.key_match4?("/parent/123/child/456", "/parent/{id}/child/{id}") + false + """ + @spec key_match4?(String.t(), String.t()) :: boolean() + def key_match4?(key1, key2) do + key2 = String.replace(key2, "/*", "/.*") + + with {:ok, r1} <- Regex.compile("\\{([^/]+)\\}"), + tokens <- Regex.scan(r1, key2) |> Enum.map(fn [_, token] -> token end), + key2_pattern <- Regex.replace(r1, key2, "([^/]+)"), + {:ok, r2} <- Regex.compile("^" <> key2_pattern <> "$"), + matches <- Regex.run(r2, key1) do + if matches do + values = Enum.drop(matches, 1) + + if length(tokens) != length(values) do + false + else + # Build a map of token -> value and check for conflicts + tokens + |> Enum.zip(values) + |> Enum.reduce_while(%{}, fn {token, value}, acc -> + case Map.get(acc, token) do + nil -> {:cont, Map.put(acc, token, value)} + ^value -> {:cont, acc} + _ -> {:halt, :mismatch} + end + end) + |> case do + :mismatch -> false + _ -> true + end + end + else + false + end + else + _ -> false + end + end + + @doc """ + Returns `true` if IP address `ip1` matches the pattern of `ip2`. + + `ip2` can be an IP address or a CIDR pattern. + + ## Examples + + iex> Enforcer.ip_match?("192.168.2.123", "192.168.2.0/24") + true + iex> Enforcer.ip_match?("192.168.2.123", "192.168.2.123") + true + iex> Enforcer.ip_match?("192.168.2.123", "192.168.3.0/24") + false + """ + @spec ip_match?(String.t(), String.t()) :: boolean() + def ip_match?(ip1, ip2) do + case parse_ip(ip1) do + {:ok, ip1_tuple} -> + case parse_cidr(ip2) do + {:ok, network, prefix_len} -> + ip_in_cidr?(ip1_tuple, network, prefix_len) + + :error -> + case parse_ip(ip2) do + {:ok, ip2_tuple} -> ip1_tuple == ip2_tuple + :error -> false + end + end + + :error -> + false + end + end + + # Helper function to parse IP address + defp parse_ip(ip_string) do + case :inet.parse_address(String.to_charlist(ip_string)) do + {:ok, ip_tuple} -> {:ok, ip_tuple} + {:error, _} -> :error + end + end + + # Helper function to parse CIDR notation + defp parse_cidr(cidr_string) do + case String.split(cidr_string, "/") do + [ip_str, prefix_str] -> + with {:ok, ip_tuple} <- parse_ip(ip_str), + {prefix_len, ""} <- Integer.parse(prefix_str) do + {:ok, ip_tuple, prefix_len} + else + _ -> :error + end + + _ -> + :error + end + end + + # Check if an IP is within a CIDR range + defp ip_in_cidr?(ip_tuple, network_tuple, prefix_len) do + ip_bits = ip_to_bits(ip_tuple) + network_bits = ip_to_bits(network_tuple) + + String.slice(ip_bits, 0, prefix_len) == String.slice(network_bits, 0, prefix_len) + end + + # Convert IP tuple to binary string representation + defp ip_to_bits({a, b, c, d}) do + <> + |> :binary.bin_to_list() + |> Enum.map(&Integer.to_string(&1, 2)) + |> Enum.map(&String.pad_leading(&1, 8, "0")) + |> Enum.join() + end + + defp ip_to_bits({a, b, c, d, e, f, g, h}) do + <> + |> :binary.bin_to_list() + |> Enum.map(&Integer.to_string(&1, 2)) + |> Enum.map(&String.pad_leading(&1, 8, "0")) + |> Enum.join() + end + + @doc """ + Returns `true` if `key1` matches the glob pattern `key2`. + + Uses standard glob pattern matching with `*` and `**` wildcards. + + ## Examples + + iex> Enforcer.glob_match?("/foo/bar", "/foo/*") + true + iex> Enforcer.glob_match?("/foo", "/foo") + true + """ + @spec glob_match?(String.t(), String.t()) :: boolean() + def glob_match?(key1, key2) do + # Convert glob pattern to regex pattern + # This is a simplified implementation - for production use consider a dedicated glob library + pattern = + key2 + |> String.replace(".", "\\.") + |> String.replace("**", "") + |> String.replace("*", "[^/]*") + |> String.replace("", ".*") + |> then(&("^" <> &1 <> "$")) + + case Regex.compile(pattern) do + {:ok, regex} -> Regex.match?(regex, key1) + {:error, _} -> false + end + end + # # Helpers # @@ -1003,7 +1299,14 @@ defmodule Casbin.Enforcer do defp init_env do %{ regexMatch: ®ex_match?/2, - keyMatch2: &key_match2?/2 + keyMatch: &key_match?/2, + keyGet: &key_get/2, + keyMatch2: &key_match2?/2, + keyGet2: &key_get2/3, + keyMatch3: &key_match3?/2, + keyMatch4: &key_match4?/2, + ipMatch: &ip_match?/2, + globMatch: &glob_match?/2 } end end diff --git a/test/enforcer/matcher_functions_test.exs b/test/enforcer/matcher_functions_test.exs new file mode 100644 index 0000000..eec8307 --- /dev/null +++ b/test/enforcer/matcher_functions_test.exs @@ -0,0 +1,177 @@ +defmodule Casbin.Enforcer.MatcherFunctionsTest do + use ExUnit.Case, async: true + alias Casbin.Enforcer + + describe "key_match?/2" do + @test_cases [ + {"/foo", "/foo", true}, + {"/foo", "/foo*", true}, + {"/foo", "/foo/*", false}, + {"/foo/bar", "/foo", false}, + {"/foo/bar", "/foo*", true}, + {"/foo/bar", "/foo/*", true}, + {"/foobar", "/foo", false}, + {"/foobar", "/foo*", true}, + {"/foobar", "/foo/*", false} + ] + + Enum.each(@test_cases, fn {key1, key2, expected} -> + test "key_match?(#{inspect(key1)}, #{inspect(key2)}) returns #{expected}" do + assert Enforcer.key_match?(unquote(key1), unquote(key2)) === unquote(expected) + end + end) + end + + describe "key_get/2" do + @test_cases [ + {"/foo", "/foo", ""}, + {"/foo", "/foo*", ""}, + {"/foo", "/foo/*", ""}, + {"/foo/bar", "/foo", ""}, + {"/foo/bar", "/foo*", "/bar"}, + {"/foo/bar", "/foo/*", "bar"}, + {"/foobar", "/foo", ""}, + {"/foobar", "/foo*", "bar"}, + {"/foobar", "/foo/*", ""} + ] + + Enum.each(@test_cases, fn {key1, key2, expected} -> + test "key_get(#{inspect(key1)}, #{inspect(key2)}) returns #{inspect(expected)}" do + assert Enforcer.key_get(unquote(key1), unquote(key2)) === unquote(expected) + end + end) + end + + describe "key_get2/3" do + @test_cases [ + {"/foo", "/foo", "id", ""}, + {"/foo", "/foo*", "id", ""}, + {"/foo", "/foo/*", "id", ""}, + {"/foo/bar", "/foo", "id", ""}, + {"/foo/bar", "/foo*", "id", ""}, + {"/foo/bar", "/foo/*", "id", ""}, + {"/foobar", "/foo", "id", ""}, + {"/foobar", "/foo*", "id", ""}, + {"/foobar", "/foo/*", "id", ""}, + {"/", "/:resource", "resource", ""}, + {"/resource1", "/:resource", "resource", "resource1"}, + {"/myid", "/:id/using/:resId", "id", ""}, + {"/myid/using/myresid", "/:id/using/:resId", "id", "myid"}, + {"/myid/using/myresid", "/:id/using/:resId", "resId", "myresid"}, + {"/proxy/myid", "/proxy/:id/*", "id", ""}, + {"/proxy/myid/", "/proxy/:id/*", "id", "myid"}, + {"/proxy/myid/res", "/proxy/:id/*", "id", "myid"}, + {"/proxy/myid/res/res2", "/proxy/:id/*", "id", "myid"}, + {"/proxy/myid/res/res2/res3", "/proxy/:id/*", "id", "myid"}, + {"/proxy/", "/proxy/:id/*", "id", ""}, + {"/alice", "/:id", "id", "alice"}, + {"/alice/all", "/:id/all", "id", "alice"}, + {"/alice", "/:id/all", "id", ""}, + {"/alice/all", "/:id", "id", ""} + ] + + Enum.each(@test_cases, fn {key1, key2, path_var, expected} -> + test "key_get2(#{inspect(key1)}, #{inspect(key2)}, #{inspect(path_var)}) returns #{inspect(expected)}" do + assert Enforcer.key_get2(unquote(key1), unquote(key2), unquote(path_var)) === + unquote(expected) + end + end) + end + + describe "key_match3?/2" do + @test_cases [ + {"/foo", "/foo", true}, + {"/foo", "/foo*", true}, + {"/foo", "/foo/*", false}, + {"/foo/bar", "/foo", false}, + {"/foo/bar", "/foo*", false}, + {"/foo/bar", "/foo/*", true}, + {"/foobar", "/foo", false}, + {"/foobar", "/foo*", false}, + {"/foobar", "/foo/*", false}, + {"/", "/{resource}", false}, + {"/resource1", "/{resource}", true}, + {"/myid", "/{id}/using/{resId}", false}, + {"/myid/using/myresid", "/{id}/using/{resId}", true}, + {"/proxy/myid", "/proxy/{id}/*", false}, + {"/proxy/myid/", "/proxy/{id}/*", true}, + {"/proxy/myid/res", "/proxy/{id}/*", true}, + {"/proxy/myid/res/res2", "/proxy/{id}/*", true}, + {"/proxy/myid/res/res2/res3", "/proxy/{id}/*", true}, + {"/proxy/", "/proxy/{id}/*", false} + ] + + Enum.each(@test_cases, fn {key1, key2, expected} -> + test "key_match3?(#{inspect(key1)}, #{inspect(key2)}) returns #{expected}" do + assert Enforcer.key_match3?(unquote(key1), unquote(key2)) === unquote(expected) + end + end) + end + + describe "key_match4?/2" do + @test_cases [ + {"/parent/123/child/123", "/parent/{id}/child/{id}", true}, + {"/parent/123/child/456", "/parent/{id}/child/{id}", false}, + {"/parent/123/child/123", "/parent/{id}/child/{another_id}", true}, + {"/parent/123/child/456", "/parent/{id}/child/{another_id}", true}, + {"/parent/123/child/123/book/123", "/parent/{id}/child/{id}/book/{id}", true}, + {"/parent/123/child/123/book/456", "/parent/{id}/child/{id}/book/{id}", false}, + {"/parent/123/child/456/book/123", "/parent/{id}/child/{id}/book/{id}", false}, + {"/parent/123/child/456/book/", "/parent/{id}/child/{id}/book/{id}", false}, + {"/parent/123/child/456", "/parent/{id}/child/{id}/book/{id}", false} + ] + + Enum.each(@test_cases, fn {key1, key2, expected} -> + test "key_match4?(#{inspect(key1)}, #{inspect(key2)}) returns #{expected}" do + assert Enforcer.key_match4?(unquote(key1), unquote(key2)) === unquote(expected) + end + end) + end + + describe "ip_match?/2" do + @test_cases [ + {"192.168.2.123", "192.168.2.0/24", true}, + {"192.168.2.123", "192.168.3.0/24", false}, + {"192.168.2.123", "192.168.2.0/16", true}, + {"192.168.2.123", "192.168.2.123", true}, + {"192.168.2.123", "192.168.2.123/32", true}, + {"10.0.0.11", "10.0.0.0/8", true}, + {"11.0.0.123", "10.0.0.0/8", false} + ] + + Enum.each(@test_cases, fn {ip1, ip2, expected} -> + test "ip_match?(#{inspect(ip1)}, #{inspect(ip2)}) returns #{expected}" do + assert Enforcer.ip_match?(unquote(ip1), unquote(ip2)) === unquote(expected) + end + end) + end + + describe "glob_match?/2" do + @test_cases [ + {"/foo", "/foo", true}, + {"/foo", "/foo*", true}, + {"/foo", "/foo/*", false}, + {"/foo/bar", "/foo", false}, + {"/foo/bar", "/foo*", false}, + {"/foo/bar", "/foo/*", true}, + {"/foobar", "/foo", false}, + {"/foobar", "/foo*", true}, + {"/foobar", "/foo/*", false}, + {"/foo", "*/foo", true}, + {"/foo", "*/foo*", true}, + {"/foo", "*/foo/*", false}, + {"/foo/bar", "*/foo", false}, + {"/foo/bar", "*/foo*", false}, + {"/foo/bar", "*/foo/*", true}, + {"/foobar", "*/foo", false}, + {"/foobar", "*/foo*", true}, + {"/foobar", "*/foo/*", false} + ] + + Enum.each(@test_cases, fn {key1, key2, expected} -> + test "glob_match?(#{inspect(key1)}, #{inspect(key2)}) returns #{expected}" do + assert Enforcer.glob_match?(unquote(key1), unquote(key2)) === unquote(expected) + end + end) + end +end From b3ba7fb86d7897a2c3fe33f7a1b8f2940aeaec5f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 1 Jan 2026 10:57:13 +0000 Subject: [PATCH 3/5] Add integration tests and test data for keyMatch and keyMatch3 functions Co-authored-by: hsluoyz <3787410+hsluoyz@users.noreply.github.com> --- test/data/keymatch.conf | 11 ++++++++++ test/data/keymatch.csv | 3 +++ test/data/keymatch3.conf | 11 ++++++++++ test/data/keymatch3.csv | 3 +++ test/enforcer/keymatch3_test.exs | 36 ++++++++++++++++++++++++++++++++ test/enforcer/keymatch_test.exs | 36 ++++++++++++++++++++++++++++++++ 6 files changed, 100 insertions(+) create mode 100644 test/data/keymatch.conf create mode 100644 test/data/keymatch.csv create mode 100644 test/data/keymatch3.conf create mode 100644 test/data/keymatch3.csv create mode 100644 test/enforcer/keymatch3_test.exs create mode 100644 test/enforcer/keymatch_test.exs diff --git a/test/data/keymatch.conf b/test/data/keymatch.conf new file mode 100644 index 0000000..9046b35 --- /dev/null +++ b/test/data/keymatch.conf @@ -0,0 +1,11 @@ +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = r.sub == p.sub && keyMatch(r.obj, p.obj) && r.act == p.act diff --git a/test/data/keymatch.csv b/test/data/keymatch.csv new file mode 100644 index 0000000..390db53 --- /dev/null +++ b/test/data/keymatch.csv @@ -0,0 +1,3 @@ +p, alice, /alice_data*, GET +p, alice, /alice_data/*, POST +p, bob, /bob_data/*, GET diff --git a/test/data/keymatch3.conf b/test/data/keymatch3.conf new file mode 100644 index 0000000..fd47864 --- /dev/null +++ b/test/data/keymatch3.conf @@ -0,0 +1,11 @@ +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = r.sub == p.sub && keyMatch3(r.obj, p.obj) && r.act == p.act diff --git a/test/data/keymatch3.csv b/test/data/keymatch3.csv new file mode 100644 index 0000000..92fe05b --- /dev/null +++ b/test/data/keymatch3.csv @@ -0,0 +1,3 @@ +p, alice, /alice_data/{resource}, GET +p, alice, /alice_data2/{id}/using/{resId}, GET +p, bob, /bob_data/{id}*, GET diff --git a/test/enforcer/keymatch3_test.exs b/test/enforcer/keymatch3_test.exs new file mode 100644 index 0000000..0666838 --- /dev/null +++ b/test/enforcer/keymatch3_test.exs @@ -0,0 +1,36 @@ +defmodule Casbin.Enforcer.KeyMatch3Test do + use ExUnit.Case, async: true + alias Casbin.Enforcer + + @cfile "../data/keymatch3.conf" |> Path.expand(__DIR__) + @pfile "../data/keymatch3.csv" |> Path.expand(__DIR__) + + setup do + {:ok, e} = Enforcer.init(@cfile) + + e = + e + |> Enforcer.load_policies!(@pfile) + + {:ok, e: e} + end + + describe "allow?/2 with keyMatch3" do + @test_cases [ + {["alice", "/alice_data/resource1", "GET"], true}, + {["alice", "/alice_data2/myid/using/myresid", "GET"], true}, + {["alice", "/alice_data2/1/using/2", "GET"], true}, + {["alice", "/alice_data2/1/using/2", "POST"], false}, + {["bob", "/bob_data/123", "GET"], true}, + {["bob", "/bob_data/123/extra", "GET"], true}, + {["alice", "/alice_data/", "GET"], false}, + {["bob", "/alice_data/resource1", "GET"], false} + ] + + Enum.each(@test_cases, fn {req, res} -> + test "response `#{res}` for request #{inspect(req)}", %{e: e} do + assert e |> Enforcer.allow?(unquote(req)) === unquote(res) + end + end) + end +end diff --git a/test/enforcer/keymatch_test.exs b/test/enforcer/keymatch_test.exs new file mode 100644 index 0000000..c9f383d --- /dev/null +++ b/test/enforcer/keymatch_test.exs @@ -0,0 +1,36 @@ +defmodule Casbin.Enforcer.KeyMatchTest do + use ExUnit.Case, async: true + alias Casbin.Enforcer + + @cfile "../data/keymatch.conf" |> Path.expand(__DIR__) + @pfile "../data/keymatch.csv" |> Path.expand(__DIR__) + + setup do + {:ok, e} = Enforcer.init(@cfile) + + e = + e + |> Enforcer.load_policies!(@pfile) + + {:ok, e: e} + end + + describe "allow?/2 with keyMatch" do + @test_cases [ + {["alice", "/alice_data/resource1", "GET"], true}, + {["alice", "/alice_data", "GET"], true}, + {["alice", "/alice_data/", "POST"], true}, + {["alice", "/alice_data/resource1", "POST"], true}, + {["alice", "/alice_data", "POST"], false}, + {["bob", "/bob_data/resource1", "GET"], true}, + {["bob", "/bob_data/", "GET"], true}, + {["bob", "/alice_data", "GET"], false} + ] + + Enum.each(@test_cases, fn {req, res} -> + test "response `#{res}` for request #{inspect(req)}", %{e: e} do + assert e |> Enforcer.allow?(unquote(req)) === unquote(res) + end + end) + end +end From 678937f1e05682483f4ff38f4d70be25388b1feb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 1 Jan 2026 10:59:02 +0000 Subject: [PATCH 4/5] Fix IPv6 handling and improve glob_match implementation based on code review Co-authored-by: hsluoyz <3787410+hsluoyz@users.noreply.github.com> --- lib/casbin/enforcer.ex | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/casbin/enforcer.ex b/lib/casbin/enforcer.ex index 3653383..b96e5e0 100644 --- a/lib/casbin/enforcer.ex +++ b/lib/casbin/enforcer.ex @@ -1255,10 +1255,10 @@ defmodule Casbin.Enforcer do end defp ip_to_bits({a, b, c, d, e, f, g, h}) do - <> - |> :binary.bin_to_list() + # Convert each 16-bit segment to binary representation + [a, b, c, d, e, f, g, h] |> Enum.map(&Integer.to_string(&1, 2)) - |> Enum.map(&String.pad_leading(&1, 8, "0")) + |> Enum.map(&String.pad_leading(&1, 16, "0")) |> Enum.join() end @@ -1266,6 +1266,8 @@ defmodule Casbin.Enforcer do Returns `true` if `key1` matches the glob pattern `key2`. Uses standard glob pattern matching with `*` and `**` wildcards. + - `*` matches any characters except `/` + - `**` matches any characters including `/` ## Examples @@ -1277,13 +1279,13 @@ defmodule Casbin.Enforcer do @spec glob_match?(String.t(), String.t()) :: boolean() def glob_match?(key1, key2) do # Convert glob pattern to regex pattern - # This is a simplified implementation - for production use consider a dedicated glob library + # Process in order: escape dots, handle **, then handle * pattern = key2 |> String.replace(".", "\\.") - |> String.replace("**", "") + |> String.replace("**", "\x00") # Use null byte as temporary placeholder |> String.replace("*", "[^/]*") - |> String.replace("", ".*") + |> String.replace("\x00", ".*") # Replace placeholder with .* |> then(&("^" <> &1 <> "$")) case Regex.compile(pattern) do From 0d93487e0f020bf3837fc2ff79a9919513ccc03c Mon Sep 17 00:00:00 2001 From: Yang Luo Date: Thu, 1 Jan 2026 19:01:11 +0800 Subject: [PATCH 5/5] Remove TODO section for matchers functions Removed the TODO section for matchers functions from the README. --- README.md | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/README.md b/README.md index 4ae5612..1d25ede 100644 --- a/README.md +++ b/README.md @@ -333,21 +333,6 @@ Casbin-Ex supports the following access control models: 10. **Deny-override**: both allow and deny authorizations are supported, deny overrides the allow 11. **Priority**: the policy rules can be prioritized like firewall rules -## TODO - -### Matchers Functions - -Implement all [matchers' functions](https://casbin.org/docs/function): -- [x] regexMatch -- [x] keyMatch -- [x] keyGet -- [x] keyMatch2 -- [x] keyGet2 -- [x] keyMatch3 -- [x] keyMatch4 -- [x] ipMatch -- [x] globMatch - ## Testing ### Using with Ecto.Adapters.SQL.Sandbox