diff --git a/.credo.exs b/.credo.exs new file mode 100644 index 0000000..baa1ea2 --- /dev/null +++ b/.credo.exs @@ -0,0 +1,217 @@ +# This file contains the configuration for Credo and you are probably reading +# this after creating it with `mix credo.gen.config`. +# +# If you find anything wrong or unclear in this file, please report an +# issue on GitHub: https://github.com/rrrene/credo/issues +# +%{ + # + # You can have as many configs as you like in the `configs:` field. + configs: [ + %{ + # + # Run any config using `mix credo -C `. If no config name is given + # "default" is used. + # + name: "default", + # + # These are the files included in the analysis: + files: %{ + # + # You can give explicit globs or simply directories. + # In the latter case `**/*.{ex,exs}` will be used. + # + included: [ + "lib/", + "src/", + "test/", + "web/", + "apps/*/lib/", + "apps/*/src/", + "apps/*/test/", + "apps/*/web/" + ], + excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"] + }, + # + # Load and configure plugins here: + # + plugins: [], + # + # If you create your own checks, you must specify the source files for + # them here, so they can be loaded by Credo before running the analysis. + # + requires: [], + # + # If you want to enforce a style guide and need a more traditional linting + # experience, you can change `strict` to `true` below: + # + strict: false, + # + # To modify the timeout for parsing files, change this value: + # + parse_timeout: 5000, + # + # If you want to use uncolored output by default, you can change `color` + # to `false` below: + # + color: true, + # + # You can customize the parameters of any check by adding a second element + # to the tuple. + # + # To disable a check put `false` as second element: + # + # {Credo.Check.Design.DuplicatedCode, false} + # + checks: %{ + enabled: [ + # + ## Consistency Checks + # + {Credo.Check.Consistency.ExceptionNames, []}, + {Credo.Check.Consistency.LineEndings, []}, + {Credo.Check.Consistency.ParameterPatternMatching, []}, + {Credo.Check.Consistency.SpaceAroundOperators, []}, + {Credo.Check.Consistency.SpaceInParentheses, []}, + {Credo.Check.Consistency.TabsOrSpaces, []}, + + # + ## Design Checks + # + # You can customize the priority of any check + # Priority values are: `low, normal, high, higher` + # + {Credo.Check.Design.AliasUsage, + [priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 0]}, + {Credo.Check.Design.TagFIXME, []}, + # You can also customize the exit_status of each check. + # If you don't want TODO comments to cause `mix credo` to fail, just + # set this value to 0 (zero). + # + {Credo.Check.Design.TagTODO, [exit_status: 2]}, + + # + ## Readability Checks + # + {Credo.Check.Readability.AliasOrder, []}, + {Credo.Check.Readability.FunctionNames, []}, + {Credo.Check.Readability.LargeNumbers, []}, + {Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]}, + {Credo.Check.Readability.ModuleAttributeNames, []}, + {Credo.Check.Readability.ModuleDoc, []}, + {Credo.Check.Readability.ModuleNames, []}, + {Credo.Check.Readability.ParenthesesInCondition, []}, + {Credo.Check.Readability.ParenthesesOnZeroArityDefs, []}, + {Credo.Check.Readability.PipeIntoAnonymousFunctions, []}, + {Credo.Check.Readability.PredicateFunctionNames, []}, + {Credo.Check.Readability.PreferImplicitTry, []}, + {Credo.Check.Readability.RedundantBlankLines, []}, + {Credo.Check.Readability.Semicolons, []}, + {Credo.Check.Readability.SpaceAfterCommas, []}, + {Credo.Check.Readability.StringSigils, []}, + {Credo.Check.Readability.TrailingBlankLine, []}, + {Credo.Check.Readability.TrailingWhiteSpace, []}, + {Credo.Check.Readability.UnnecessaryAliasExpansion, []}, + {Credo.Check.Readability.VariableNames, []}, + {Credo.Check.Readability.WithSingleClause, []}, + + # + ## Refactoring Opportunities + # + {Credo.Check.Refactor.Apply, []}, + {Credo.Check.Refactor.CondStatements, []}, + {Credo.Check.Refactor.CyclomaticComplexity, []}, + {Credo.Check.Refactor.FilterCount, []}, + {Credo.Check.Refactor.FilterFilter, []}, + {Credo.Check.Refactor.FunctionArity, []}, + {Credo.Check.Refactor.LongQuoteBlocks, []}, + {Credo.Check.Refactor.MapJoin, []}, + {Credo.Check.Refactor.MatchInCondition, []}, + {Credo.Check.Refactor.NegatedConditionsInUnless, []}, + {Credo.Check.Refactor.NegatedConditionsWithElse, []}, + {Credo.Check.Refactor.Nesting, []}, + {Credo.Check.Refactor.RedundantWithClauseResult, []}, + {Credo.Check.Refactor.RejectReject, []}, + {Credo.Check.Refactor.UnlessWithElse, []}, + {Credo.Check.Refactor.WithClauses, []}, + + # + ## Warnings + # + {Credo.Check.Warning.ApplicationConfigInModuleAttribute, []}, + {Credo.Check.Warning.BoolOperationOnSameValues, []}, + {Credo.Check.Warning.Dbg, []}, + {Credo.Check.Warning.ExpensiveEmptyEnumCheck, []}, + {Credo.Check.Warning.IExPry, []}, + {Credo.Check.Warning.IoInspect, []}, + {Credo.Check.Warning.MissedMetadataKeyInLoggerConfig, []}, + {Credo.Check.Warning.OperationOnSameValues, []}, + {Credo.Check.Warning.OperationWithConstantResult, []}, + {Credo.Check.Warning.RaiseInsideRescue, []}, + {Credo.Check.Warning.SpecWithStruct, []}, + {Credo.Check.Warning.UnsafeExec, []}, + {Credo.Check.Warning.UnusedEnumOperation, []}, + {Credo.Check.Warning.UnusedFileOperation, []}, + {Credo.Check.Warning.UnusedKeywordOperation, []}, + {Credo.Check.Warning.UnusedListOperation, []}, + {Credo.Check.Warning.UnusedPathOperation, []}, + {Credo.Check.Warning.UnusedRegexOperation, []}, + {Credo.Check.Warning.UnusedStringOperation, []}, + {Credo.Check.Warning.UnusedTupleOperation, []}, + {Credo.Check.Warning.WrongTestFileExtension, []} + ], + disabled: [ + # + # Checks scheduled for next check update (opt-in for now) + {Credo.Check.Refactor.UtcNowTruncate, []}, + + # + # Controversial and experimental checks (opt-in, just move the check to `:enabled` + # and be sure to use `mix credo --strict` to see low priority checks) + # + {Credo.Check.Consistency.MultiAliasImportRequireUse, []}, + {Credo.Check.Consistency.UnusedVariableNames, []}, + {Credo.Check.Design.DuplicatedCode, []}, + {Credo.Check.Design.SkipTestWithoutComment, []}, + {Credo.Check.Readability.AliasAs, []}, + {Credo.Check.Readability.BlockPipe, []}, + {Credo.Check.Readability.ImplTrue, []}, + {Credo.Check.Readability.MultiAlias, []}, + {Credo.Check.Readability.NestedFunctionCalls, []}, + {Credo.Check.Readability.OneArityFunctionInPipe, []}, + {Credo.Check.Readability.OnePipePerLine, []}, + {Credo.Check.Readability.SeparateAliasRequire, []}, + {Credo.Check.Readability.SingleFunctionToBlockPipe, []}, + {Credo.Check.Readability.SinglePipe, []}, + {Credo.Check.Readability.Specs, []}, + {Credo.Check.Readability.StrictModuleLayout, []}, + {Credo.Check.Readability.WithCustomTaggedTuple, []}, + {Credo.Check.Refactor.ABCSize, []}, + {Credo.Check.Refactor.AppendSingleItem, []}, + {Credo.Check.Refactor.DoubleBooleanNegation, []}, + {Credo.Check.Refactor.FilterReject, []}, + {Credo.Check.Refactor.IoPuts, []}, + {Credo.Check.Refactor.MapMap, []}, + {Credo.Check.Refactor.ModuleDependencies, []}, + {Credo.Check.Refactor.NegatedIsNil, []}, + {Credo.Check.Refactor.PassAsyncInTestCases, []}, + {Credo.Check.Refactor.PipeChainStart, []}, + {Credo.Check.Refactor.RejectFilter, []}, + {Credo.Check.Refactor.VariableRebinding, []}, + {Credo.Check.Warning.LazyLogging, []}, + {Credo.Check.Warning.LeakyEnvironment, []}, + {Credo.Check.Warning.MapGetUnsafePass, []}, + {Credo.Check.Warning.MixEnv, []}, + {Credo.Check.Warning.UnsafeToAtom, []} + + # {Credo.Check.Refactor.MapInto, []}, + + # + # Custom checks can be created using `mix credo.gen.check`. + # + ] + } + } + ] +} diff --git a/.tool-versions b/.tool-versions index b4e8514..c26cf6f 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,2 @@ -elixir 1.14.2-otp-25 -erlang 25.1.1 +elixir 1.18.3-otp-27 +erlang 27.2 diff --git a/config/config.exs b/config/config.exs new file mode 100644 index 0000000..281d90c --- /dev/null +++ b/config/config.exs @@ -0,0 +1,15 @@ +import Config + +if Mix.env() == :dev do + config :git_hooks, + auto_install: true, + verbose: true, + hooks: [ + pre_push: [ + tasks: [ + {:cmd, "mix credo --strict"}, + {:cmd, "mix format"} + ] + ] + ] +end diff --git a/lib/acx.ex b/lib/acx.ex index 9184516..30aeb0e 100644 --- a/lib/acx.ex +++ b/lib/acx.ex @@ -1,4 +1,7 @@ defmodule Acx do + @moduledoc """ + Acx is an Elixir implementation of the Casbin authorization library. + """ use Application def start(_type, _args) do diff --git a/lib/acx/enforcer.ex b/lib/acx/enforcer.ex index 9cb5289..dc836d4 100644 --- a/lib/acx/enforcer.ex +++ b/lib/acx/enforcer.ex @@ -10,12 +10,14 @@ defmodule Acx.Enforcer do env: %{}, persist_adapter: nil - alias Acx.Model alias Acx.Internal.RoleGroup + alias Acx.Model alias Acx.Persist.PersistAdapter + alias Acx.Persist.ReadonlyFileAdapter - @type mapping() :: {atom(), String.t(), String.t()} | - {atom(), String.t(), String.t(), String.t()} + @type mapping() :: + {atom(), String.t(), String.t()} + | {atom(), String.t(), String.t(), String.t()} @type t() :: %__MODULE__{ model: Model.t(), @@ -26,8 +28,6 @@ defmodule Acx.Enforcer do persist_adapter: PersistAdapter.t() } - - @doc """ Loads and contructs a model from the given config file `cfile`. """ @@ -57,8 +57,8 @@ defmodule Acx.Enforcer do role_mappings |> Enum.map(fn m -> {m, RoleGroup.new(m)} end) - # TODO: What if one of the mapping name in `role_mappings` - # conflicts with sone built-in function names? + # Merge role group functions with built-in functions, prioritizing built-ins + # to prevent role mapping names from overriding core functionality env = role_groups |> Enum.map(fn {name, g} -> {name, RoleGroup.stub_2(g)} end) @@ -70,7 +70,7 @@ defmodule Acx.Enforcer do %__MODULE__{ model: model, role_groups: role_groups |> Map.new(), - persist_adapter: Acx.Persist.ReadonlyFileAdapter.new(), + persist_adapter: ReadonlyFileAdapter.new(), env: env } } @@ -96,11 +96,11 @@ defmodule Acx.Enforcer do """ @spec add_policy(t(), {atom(), [String.t()]}) :: t() | {:error, String.t()} def add_policy( - %__MODULE__{persist_adapter: adapter} = enforcer, - {_key, _attrs} = rule - ) do - with {:ok, enforcer} <- load_policy(enforcer, rule), - {:ok, adapter} <- PersistAdapter.add_policy(adapter, rule) do + %__MODULE__{persist_adapter: adapter} = enforcer, + {_key, _attrs} = rule + ) do + with {:ok, enforcer} <- load_policy(enforcer, rule), + {:ok, adapter} <- PersistAdapter.add_policy(adapter, rule) do %{enforcer | persist_adapter: adapter} else {:error, reason} -> {:error, reason} @@ -124,12 +124,12 @@ defmodule Acx.Enforcer do @spec load_policy(t(), {atom(), [String.t()]}) :: t() | {:error, String.t()} defp load_policy( - %__MODULE__{model: model, policies: policies, persist_adapter: adapter} = enforcer, - {key, attrs} - ) do - with {:ok, policy} <- Model.create_policy(model, {key, attrs}), - false <- Enum.member?(policies, policy) do - enforcer = %{enforcer | policies: [policy | policies], persist_adapter: adapter} + %__MODULE__{model: model, policies: policies, persist_adapter: adapter} = enforcer, + {key, attrs} + ) do + with {:ok, policy} <- Model.create_policy(model, {key, attrs}), + false <- Enum.member?(policies, policy) do + enforcer = %{enforcer | policies: [policy | policies], persist_adapter: adapter} {:ok, enforcer} else {:error, reason} -> {:error, reason} @@ -151,13 +151,13 @@ defmodule Acx.Enforcer do Removes the policy rule or rules that match from the enforcer. """ def remove_policy( - %__MODULE__{model: model, policies: policies, persist_adapter: adapter} = enforcer, - {key, attrs} - ) do + %__MODULE__{model: model, policies: policies, persist_adapter: adapter} = enforcer, + {key, attrs} + ) do with {:ok, policy} <- Model.create_policy(model, {key, attrs}), - true <- Enum.member?(policies, policy), - {:ok, _adapter} <- PersistAdapter.remove_policy(adapter, {key, attrs}), - policies <- Enum.reject(policies, fn p -> p == policy end) do + true <- Enum.member?(policies, policy), + {:ok, _adapter} <- PersistAdapter.remove_policy(adapter, {key, attrs}), + policies <- Enum.reject(policies, fn p -> p == policy end) do %{enforcer | policies: policies} else false -> {:error, :nonexistent} @@ -213,29 +213,38 @@ defmodule Acx.Enforcer do """ @spec remove_filtered_policy(t(), atom(), integer(), keyword()) :: t() | {:error, any()} def remove_filtered_policy( - %__MODULE__{policies: policies, persist_adapter: adapter} = enforcer, - req_key, idx, req - ) - when is_atom(req_key) and is_integer(idx) and is_list(req) do - filtered_policies = - policies - |> Enum.reject(fn %{key: key, attrs: attrs} -> - attr_values = - attrs - |> Enum.map(&elem(&1, 1)) - |> Enum.slice(idx, length(req)) + %__MODULE__{policies: policies, persist_adapter: adapter} = enforcer, + req_key, + idx, + req + ) + when is_atom(req_key) and is_integer(idx) and is_list(req) do + filtered_policies = + policies + |> Enum.reject(fn %{key: key, attrs: attrs} -> + attr_values = + attrs + |> Enum.map(&elem(&1, 1)) + |> Enum.slice(idx, length(req)) - [key | attr_values] === [req_key | req] - end) + [key | attr_values] === [req_key | req] + end) - {:ok, adapter} = PersistAdapter.remove_filtered_policy(adapter, req_key, idx, req) - %{enforcer | policies: filtered_policies, persist_adapter: adapter} + case PersistAdapter.remove_filtered_policy(adapter, req_key, idx, req) do + {:ok, adapter} -> + %{enforcer | policies: filtered_policies, persist_adapter: adapter} + + {:error, reason} -> + {:error, reason} + end end - @spec remove_filtered_policy!(t(), atom(), integer(), keyword()) :: t() | {:error, any()} + @spec remove_filtered_policy!(t(), atom(), integer(), keyword()) :: t() def remove_filtered_policy!( %__MODULE__{} = enforcer, - req_key, idx, req + req_key, + idx, + req ) when is_atom(req_key) and is_integer(idx) and is_list(req) do case remove_filtered_policy(enforcer, req_key, idx, req) do @@ -319,7 +328,8 @@ defmodule Acx.Enforcer do @spec load_policies!(t()) :: t() | {:error, any()} def load_policies!(%__MODULE__{model: m, persist_adapter: adapter} = enforcer) do case PersistAdapter.load_policies(adapter) do - {:ok, policies} -> policies + {:ok, policies} -> + policies |> Enum.map(fn [key | attrs] -> [String.to_atom(key) | attrs] end) |> Enum.filter(fn [key | _] -> Model.has_policy_key?(m, key) end) |> Enum.map(fn [key | attrs] -> {key, attrs} end) @@ -330,15 +340,16 @@ defmodule Acx.Enforcer do @spec load_policies!(t(), String.t()) :: t() def load_policies!(%__MODULE__{model: m} = enforcer, pfile) when is_binary(pfile) do - adapter = Acx.Persist.ReadonlyFileAdapter.new(pfile) - enforcer = %{enforcer | persist_adapter: adapter} - - case PersistAdapter.load_policies(adapter) do - {:ok, policies} -> policies - |> Enum.map(fn [key | attrs] -> [String.to_atom(key) | attrs] end) - |> Enum.filter(fn [key | _] -> Model.has_policy_key?(m, key) end) - |> Enum.map(fn [key | attrs] -> {key, attrs} end) - |> Enum.reduce(enforcer, &load_policy!(&2, &1)) + adapter = ReadonlyFileAdapter.new(pfile) + enforcer = %{enforcer | persist_adapter: adapter} + + case PersistAdapter.load_policies(adapter) do + {:ok, policies} -> + policies + |> Enum.map(fn [key | attrs] -> [String.to_atom(key) | attrs] end) + |> Enum.filter(fn [key | _] -> Model.has_policy_key?(m, key) end) + |> Enum.map(fn [key | attrs] -> {key, attrs} end) + |> Enum.reduce(enforcer, &load_policy!(&2, &1)) end end @@ -418,72 +429,76 @@ defmodule Acx.Enforcer do # @spec load_mapping_policy(t(), {atom(), String.t(), String.t()}) :: - t() | {:error, String.t()} + t() | {:error, String.t()} defp load_mapping_policy( - %__MODULE__{ - mapping_policies: mappings, - role_groups: groups, - env: env, - persist_adapter: adapter - } = enforcer, - {mapping_name, role1, role2} = mapping - ) - when is_atom(mapping_name) and is_binary(role1) and is_binary(role2) do - with group when not is_nil(group) <- Map.get(groups, mapping_name), - false <- Enum.member?(mappings, mapping), - group <- RoleGroup.add_inheritance(group, {role1, role2}) do - new_enforcer = %{ - enforcer - | role_groups: %{groups | mapping_name => group}, - mapping_policies: [mapping | mappings], - persist_adapter: adapter, - env: %{env | mapping_name => RoleGroup.stub_2(group)} - } - {:ok, new_enforcer} - else - nil -> - {:error, "mapping name not found: `#{mapping_name}`"} - true -> - {:error, :already_existed} + %__MODULE__{ + mapping_policies: mappings, + role_groups: groups, + env: env, + persist_adapter: adapter + } = enforcer, + {mapping_name, role1, role2} = mapping + ) + when is_atom(mapping_name) and is_binary(role1) and is_binary(role2) do + with group when not is_nil(group) <- Map.get(groups, mapping_name), + false <- Enum.member?(mappings, mapping), + group <- RoleGroup.add_inheritance(group, {role1, role2}) do + new_enforcer = %{ + enforcer + | role_groups: %{groups | mapping_name => group}, + mapping_policies: [mapping | mappings], + persist_adapter: adapter, + env: %{env | mapping_name => RoleGroup.stub_2(group)} + } + + {:ok, new_enforcer} + else + nil -> + {:error, "mapping name not found: `#{mapping_name}`"} + + true -> + {:error, :already_existed} end end @spec load_mapping_policy(t(), {atom(), String.t(), String.t(), String.t()}) :: - t() | {:error, String.t()} + t() | {:error, String.t()} defp load_mapping_policy( - %__MODULE__{ - mapping_policies: mappings, - role_groups: groups, - env: env, - persist_adapter: adapter - } = enforcer, - {mapping_name, role1, role2, dom} = mapping - ) - when is_atom(mapping_name) and is_binary(role1) and is_binary(role2) and is_binary(dom) do - with group when not is_nil(group) <- Map.get(groups, mapping_name), - false <- Enum.member?(mappings, mapping), - group <- RoleGroup.add_inheritance(group, {role1, role2 <> dom}) do - new_enforcer = %{ - enforcer - | role_groups: %{groups | mapping_name => group}, - mapping_policies: [mapping | mappings], - persist_adapter: adapter, - env: %{env | mapping_name => RoleGroup.stub_3(group)} - } - {:ok, new_enforcer} - else - nil -> - {:error, "mapping name not found: `#{mapping_name}`"} - true -> - {:error, :already_existed} + %__MODULE__{ + mapping_policies: mappings, + role_groups: groups, + env: env, + persist_adapter: adapter + } = enforcer, + {mapping_name, role1, role2, dom} = mapping + ) + when is_atom(mapping_name) and is_binary(role1) and is_binary(role2) and is_binary(dom) do + with group when not is_nil(group) <- Map.get(groups, mapping_name), + false <- Enum.member?(mappings, mapping), + group <- RoleGroup.add_inheritance(group, {role1, role2 <> dom}) do + new_enforcer = %{ + enforcer + | role_groups: %{groups | mapping_name => group}, + mapping_policies: [mapping | mappings], + persist_adapter: adapter, + env: %{env | mapping_name => RoleGroup.stub_3(group)} + } + + {:ok, new_enforcer} + else + nil -> + {:error, "mapping name not found: `#{mapping_name}`"} + + true -> + {:error, :already_existed} end end defp load_mapping_policy!( - %__MODULE__{} = enforcer, - {mapping_name, role1, role2} - ) - when is_atom(mapping_name) and is_binary(role1) and is_binary(role2) do + %__MODULE__{} = enforcer, + {mapping_name, role1, role2} + ) + when is_atom(mapping_name) and is_binary(role1) and is_binary(role2) do case load_mapping_policy(enforcer, {mapping_name, role1, role2}) do {:error, :already_existed} -> enforcer @@ -497,10 +512,10 @@ defmodule Acx.Enforcer do end defp load_mapping_policy!( - %__MODULE__{} = enforcer, - {mapping_name, role1, role2, dom} - ) - when is_atom(mapping_name) and is_binary(role1) and is_binary(role2) and is_binary(dom) do + %__MODULE__{} = enforcer, + {mapping_name, role1, role2, dom} + ) + when is_atom(mapping_name) and is_binary(role1) and is_binary(role2) and is_binary(dom) do case load_mapping_policy(enforcer, {mapping_name, role1, role2, dom}) do {:error, :already_existed} -> enforcer @@ -513,7 +528,6 @@ defmodule Acx.Enforcer do end end - @doc """ Makes `role1` inherit from (or has role ) `role2`. The `mapping_name` should be one of the names given in the model configuration file under @@ -563,12 +577,12 @@ defmodule Acx.Enforcer do {mapping_name, role1, role2} = mapping ) when is_atom(mapping_name) and is_binary(role1) and is_binary(role2) do - with {:ok, new_enforcer} <- load_mapping_policy(enforcer, mapping), - {:ok, adapter} <- PersistAdapter.add_policy(adapter, {mapping_name, [role1, role2]}) do - %{new_enforcer | persist_adapter: adapter} - else - {:error, reason} -> - {:error, reason} + with {:ok, new_enforcer} <- load_mapping_policy(enforcer, mapping), + {:ok, adapter} <- PersistAdapter.add_policy(adapter, {mapping_name, [role1, role2]}) do + %{new_enforcer | persist_adapter: adapter} + else + {:error, reason} -> + {:error, reason} end end @@ -577,16 +591,15 @@ defmodule Acx.Enforcer do {mapping_name, role1, role2, dom} = mapping ) when is_atom(mapping_name) and is_binary(role1) and is_binary(role2) and is_binary(dom) do - with {:ok, new_enforcer} <- load_mapping_policy(enforcer, mapping), - {:ok, adapter} <- PersistAdapter.add_policy(adapter, {mapping_name, [role1, role2, dom]}) do - %{new_enforcer | persist_adapter: adapter} - else - {:error, reason} -> - {:error, reason} + with {:ok, new_enforcer} <- load_mapping_policy(enforcer, mapping), + {:ok, adapter} <- PersistAdapter.add_policy(adapter, {mapping_name, [role1, role2, dom]}) do + %{new_enforcer | persist_adapter: adapter} + else + {:error, reason} -> + {:error, reason} end end - def add_mapping_policy!( %__MODULE__{} = enforcer, {mapping_name, role1, role2} @@ -620,14 +633,15 @@ defmodule Acx.Enforcer do """ def load_mapping_policies!(%__MODULE__{model: m, persist_adapter: adapter} = enforcer) do case PersistAdapter.load_policies(adapter) do - {:ok, policies} -> policies - |> Enum.map(fn [key | attrs] -> [String.to_atom(key) | attrs] end) - |> Enum.filter(fn [key | _] -> Model.has_role_mapping?(m, key) end) - |> Enum.map(fn + {:ok, policies} -> + policies + |> Enum.map(fn [key | attrs] -> [String.to_atom(key) | attrs] end) + |> Enum.filter(fn [key | _] -> Model.has_role_mapping?(m, key) end) + |> Enum.map(fn [name, r1, r2] -> {name, r1, r2} [name, r1, r2, d] -> {name, r1, r2, d} end) - |> Enum.reduce(enforcer, &load_mapping_policy!(&2, &1)) + |> Enum.reduce(enforcer, &load_mapping_policy!(&2, &1)) end end @@ -667,47 +681,59 @@ defmodule Acx.Enforcer do """ @spec remove_mapping_policy(t(), {atom(), String.t(), String.t()}) :: t() | {:error, String.t()} def remove_mapping_policy( - %__MODULE__{mapping_policies: mappings, role_groups: groups, env: env, persist_adapter: adapter} = enforcer, + %__MODULE__{ + mapping_policies: mappings, + role_groups: groups, + env: env, + persist_adapter: adapter + } = enforcer, {mapping_name, role1, role2} = mapping ) when is_atom(mapping_name) and is_binary(role1) and is_binary(role2) do - with group when not is_nil(group) <- Map.get(groups, mapping_name), - group <- RoleGroup.remove_inheritance(group, {role1, role2}), - mappings <- Enum.reject(mappings, fn m -> m == mapping end), - {:ok, adapter} <- PersistAdapter.remove_policy(adapter, {mapping_name, [role1, role2]}) do - %{ - enforcer - | role_groups: %{groups | mapping_name => group}, - mapping_policies: mappings, - persist_adapter: adapter, - env: %{env | mapping_name => RoleGroup.stub_2(group)} - } - else - nil -> - {:error, "mapping name not found: `#{mapping_name}`"} + with group when not is_nil(group) <- Map.get(groups, mapping_name), + group <- RoleGroup.remove_inheritance(group, {role1, role2}), + mappings <- Enum.reject(mappings, fn m -> m == mapping end), + {:ok, adapter} <- PersistAdapter.remove_policy(adapter, {mapping_name, [role1, role2]}) do + %{ + enforcer + | role_groups: %{groups | mapping_name => group}, + mapping_policies: mappings, + persist_adapter: adapter, + env: %{env | mapping_name => RoleGroup.stub_2(group)} + } + else + nil -> + {:error, "mapping name not found: `#{mapping_name}`"} end end - @spec remove_mapping_policy(t(), {atom(), String.t(), String.t(), String.t()}) :: t() | {:error, String.t()} + @spec remove_mapping_policy(t(), {atom(), String.t(), String.t(), String.t()}) :: + t() | {:error, String.t()} def remove_mapping_policy( - %__MODULE__{mapping_policies: mappings, role_groups: groups, env: env, persist_adapter: adapter} = enforcer, + %__MODULE__{ + mapping_policies: mappings, + role_groups: groups, + env: env, + persist_adapter: adapter + } = enforcer, {mapping_name, role1, role2, dom} = mapping ) when is_atom(mapping_name) and is_binary(role1) and is_binary(role2) and is_binary(dom) do - with group when not is_nil(group) <- Map.get(groups, mapping_name), - group <- RoleGroup.remove_inheritance(group, {role1, role2 <> dom}), - mappings <- Enum.reject(mappings, fn m -> m == mapping end), - {:ok, _adpater} <- PersistAdapter.remove_policy(adapter, {mapping_name, [role1, role2, dom]}) do - %{ - enforcer - | role_groups: %{groups | mapping_name => group}, - mapping_policies: mappings, - persist_adapter: adapter, - env: %{env | mapping_name => RoleGroup.stub_3(group)} - } - else - nil -> - {:error, "mapping name not found: `#{mapping_name}`"} + with group when not is_nil(group) <- Map.get(groups, mapping_name), + group <- RoleGroup.remove_inheritance(group, {role1, role2 <> dom}), + mappings <- Enum.reject(mappings, fn m -> m == mapping end), + {:ok, _adpater} <- + PersistAdapter.remove_policy(adapter, {mapping_name, [role1, role2, dom]}) do + %{ + enforcer + | role_groups: %{groups | mapping_name => group}, + mapping_policies: mappings, + persist_adapter: adapter, + env: %{env | mapping_name => RoleGroup.stub_3(group)} + } + else + nil -> + {:error, "mapping name not found: `#{mapping_name}`"} end end @@ -783,10 +809,11 @@ defmodule Acx.Enforcer do """ @spec list_mapping_policies(t(), integer(), keyword()) :: [mapping()] def list_mapping_policies( - %__MODULE__{mapping_policies: mapping_policies}, - idx, - criteria - ) when is_list(criteria) and is_integer(idx) do + %__MODULE__{mapping_policies: mapping_policies}, + idx, + criteria + ) + when is_list(criteria) and is_integer(idx) do mapping_policies |> Enum.filter(fn mapping -> Tuple.to_list(mapping) @@ -797,9 +824,10 @@ defmodule Acx.Enforcer do @spec list_mapping_policies(Acx.Enforcer.t(), maybe_improper_list) :: [mapping()] def list_mapping_policies( - %__MODULE__{mapping_policies: mapping_policies}, - criteria - ) when is_list(criteria) do + %__MODULE__{mapping_policies: mapping_policies}, + criteria + ) + when is_list(criteria) do mapping_policies |> Enum.filter(fn mapping -> list = Tuple.to_list(mapping) @@ -816,12 +844,17 @@ defmodule Acx.Enforcer do policies from one source and saving to another after changing adapters. """ def save_policies( - %__MODULE__{persist_adapter: adapter, policies: policies, mapping_policies: mapping_policies} = enforcer - ) do - policies = mapping_policies - |> Enum.map(&Tuple.to_list(&1)) - |> Enum.map(fn [key | attrs] -> %{key: key, attrs: attrs} end) - |> Enum.concat(policies) + %__MODULE__{ + persist_adapter: adapter, + policies: policies, + mapping_policies: mapping_policies + } = enforcer + ) do + policies = + mapping_policies + |> Enum.map(&Tuple.to_list(&1)) + |> Enum.map(fn [key | attrs] -> %{key: key, attrs: attrs} end) + |> Enum.concat(policies) case PersistAdapter.save_policies(adapter, policies) do {:error, errors} -> {:error, errors} diff --git a/lib/acx/enforcer_server.ex b/lib/acx/enforcer_server.ex index ac6d83f..adfada1 100644 --- a/lib/acx/enforcer_server.ex +++ b/lib/acx/enforcer_server.ex @@ -84,7 +84,7 @@ defmodule Acx.EnforcerServer do See `Enforcer.save_policies/1` """ def save_policies(ename) do - GenServer.call(via_tuple(ename),{:save_policies}) + GenServer.call(via_tuple(ename), {:save_policies}) end @doc """ @@ -251,7 +251,7 @@ defmodule Acx.EnforcerServer do end def handle_call({:save_policies}, _from, enforcer) do - new_enforcer = enforcer |> Enforcer.save_policies + new_enforcer = enforcer |> Enforcer.save_policies() {:reply, :ok, new_enforcer} end @@ -296,10 +296,6 @@ defmodule Acx.EnforcerServer do {:error, reason} -> {:reply, {:error, reason}, enforcer} - {:ok, new_enforcer} -> - :ets.insert(:enforcers_table, {self_name(), new_enforcer}) - {:reply, :ok, new_enforcer} - new_enforcer -> :ets.insert(:enforcers_table, {self_name(), new_enforcer}) {:reply, :ok, new_enforcer} @@ -324,13 +320,9 @@ defmodule Acx.EnforcerServer do end def handle_call({:set_persist_adapter, adapter}, _from, enforcer) do - case Enforcer.set_persist_adapter(enforcer, adapter) do - {:error, reason} -> {:reply, {:error, reason}, enforcer} - - new_enforcer -> - :ets.insert(:enforcers_table, {self_name(), new_enforcer}) - {:reply, :ok, new_enforcer} - end + new_enforcer = Enforcer.set_persist_adapter(enforcer, adapter) + :ets.insert(:enforcers_table, {self_name(), new_enforcer}) + {:reply, :ok, new_enforcer} end # @@ -344,7 +336,7 @@ defmodule Acx.EnforcerServer do end # Returns the name of `self`. - defp self_name() do + defp self_name do Registry.keys(Acx.EnforcerRegistry, self()) |> List.first() end diff --git a/lib/acx/internal/digraph.ex b/lib/acx/internal/digraph.ex index 47d157a..51de199 100644 --- a/lib/acx/internal/digraph.ex +++ b/lib/acx/internal/digraph.ex @@ -32,7 +32,7 @@ defmodule Acx.Internal.Digraph do Creates a new empty digraph. """ @spec new() :: t() - def new(), do: %__MODULE__{} + def new, do: %__MODULE__{} @doc """ Returns a list of all the vertices in the digraph. Since the underlying @@ -131,14 +131,13 @@ defmodule Acx.Internal.Digraph do with v_id <- hash(v), w_id <- hash(w), v_adj when not is_nil(v_adj) <- Map.get(adj, v_id) do - v_adj = v_adj |> MapSet.delete(w_id) - %{g | adj: %{adj | v_id => v_adj}} - else - nil -> g - end + v_adj = v_adj |> MapSet.delete(w_id) + %{g | adj: %{adj | v_id => v_adj}} + else + nil -> g + end end - @doc """ Returns a list of vertices that are adjacent to the given vertex `v`. Adjacent here means there are direct edges from `v` pointing to those diff --git a/lib/acx/internal/helpers.ex b/lib/acx/internal/helpers.ex index 488c594..64b7515 100644 --- a/lib/acx/internal/helpers.ex +++ b/lib/acx/internal/helpers.ex @@ -1,4 +1,7 @@ defmodule Acx.Internal.Helpers do + @moduledoc """ + Helper functions used internally by Acx. + """ @doc """ Returns a tuple `{succeeds, remainder, count}` where `succeeds` is the initial segment of the given `list`, in which all elements have diff --git a/lib/acx/internal/operator.ex b/lib/acx/internal/operator.ex index eee3a69..440f910 100644 --- a/lib/acx/internal/operator.ex +++ b/lib/acx/internal/operator.ex @@ -46,11 +46,11 @@ defmodule Acx.Internal.Operator do Converts a charlist to an operator based on the type of previous token. """ @spec charlist_to_operator(charlist(), atom()) :: t() - def charlist_to_operator('+', prev) + def charlist_to_operator(~c"+", prev) when prev not in [:operand, :variable, :right_paren], do: :pos - def charlist_to_operator('-', prev) + def charlist_to_operator(~c"-", prev) when prev not in [:operand, :variable, :right_paren], do: :neg @@ -60,22 +60,22 @@ defmodule Acx.Internal.Operator do Converts an operator to its textual representation. """ @spec operator_to_charlist(t()) :: charlist() - def operator_to_charlist(:dot), do: '.' - def operator_to_charlist(:not), do: '!' - def operator_to_charlist(:pos), do: '+' - def operator_to_charlist(:neg), do: '-' - def operator_to_charlist(:mul), do: '*' - def operator_to_charlist(:div), do: '/' - def operator_to_charlist(:add), do: '+' - def operator_to_charlist(:sub), do: '-' - def operator_to_charlist(:lt), do: '<' - def operator_to_charlist(:le), do: '<=' - def operator_to_charlist(:gt), do: '>' - def operator_to_charlist(:ge), do: '>=' - def operator_to_charlist(:eq), do: '==' - def operator_to_charlist(:ne), do: '!=' - def operator_to_charlist(:and), do: '&&' - def operator_to_charlist(:or), do: '||' + def operator_to_charlist(:dot), do: ~c"." + def operator_to_charlist(:not), do: ~c"!" + def operator_to_charlist(:pos), do: ~c"+" + def operator_to_charlist(:neg), do: ~c"-" + def operator_to_charlist(:mul), do: ~c"*" + def operator_to_charlist(:div), do: ~c"/" + def operator_to_charlist(:add), do: ~c"+" + def operator_to_charlist(:sub), do: ~c"-" + def operator_to_charlist(:lt), do: ~c"<" + def operator_to_charlist(:le), do: ~c"<=" + def operator_to_charlist(:gt), do: ~c">" + def operator_to_charlist(:ge), do: ~c">=" + def operator_to_charlist(:eq), do: ~c"==" + def operator_to_charlist(:ne), do: ~c"!=" + def operator_to_charlist(:and), do: ~c"&&" + def operator_to_charlist(:or), do: ~c"||" @doc """ Returns `true` if `op1` has higher precedence than `op2`, or `false` @@ -115,140 +115,110 @@ defmodule Acx.Internal.Operator do # Unary operator def apply(:not, [x]) do - try do - {:ok, !x} - rescue - e in ArithmeticError -> - {:error, e.message <> ": !#{x}"} - end + {:ok, !x} + rescue + e in ArithmeticError -> + {:error, e.message <> ": !#{x}"} end def apply(:pos, [x]) do - try do - {:ok, +x} - rescue - e in ArithmeticError -> - {:error, e.message <> ": +#{x}"} - end + {:ok, +x} + rescue + e in ArithmeticError -> + {:error, e.message <> ": +#{x}"} end def apply(:neg, [x]) do - try do - {:ok, -x} - rescue - e in ArithmeticError -> - {:error, e.message <> ": -#{x}"} - end + {:ok, -x} + rescue + e in ArithmeticError -> + {:error, e.message <> ": -#{x}"} end # Binary operator. def apply(:mul, [x, y]) do - try do - {:ok, x * y} - rescue - e in ArithmeticError -> - {:error, e.message <> ": #{x} * #{y}"} - end + {:ok, x * y} + rescue + e in ArithmeticError -> + {:error, e.message <> ": #{x} * #{y}"} end def apply(:div, [x, y]) do - try do - {:ok, x / y} - rescue - e in ArithmeticError -> - {:error, e.message <> ": #{x} / #{y}"} - end + {:ok, x / y} + rescue + e in ArithmeticError -> + {:error, e.message <> ": #{x} / #{y}"} end def apply(:add, [x, y]) do - try do - {:ok, x + y} - rescue - e in ArithmeticError -> - {:error, e.message <> ": #{x} + #{y}"} - end + {:ok, x + y} + rescue + e in ArithmeticError -> + {:error, e.message <> ": #{x} + #{y}"} end def apply(:sub, [x, y]) do - try do - {:ok, x - y} - rescue - e in ArithmeticError -> - {:error, e.message <> ": #{x} - #{y}"} - end + {:ok, x - y} + rescue + e in ArithmeticError -> + {:error, e.message <> ": #{x} - #{y}"} end def apply(:lt, [x, y]) do - try do - {:ok, x < y} - rescue - e in ArithmeticError -> - {:error, e.message <> ": #{x} < #{y}"} - end + {:ok, x < y} + rescue + e in ArithmeticError -> + {:error, e.message <> ": #{x} < #{y}"} end def apply(:le, [x, y]) do - try do - {:ok, x <= y} - rescue - e in ArithmeticError -> - {:error, e.message <> ": #{x} <= #{y}"} - end + {:ok, x <= y} + rescue + e in ArithmeticError -> + {:error, e.message <> ": #{x} <= #{y}"} end def apply(:gt, [x, y]) do - try do - {:ok, x > y} - rescue - e in ArithmeticError -> - {:error, e.message <> ": #{x} > #{y}"} - end + {:ok, x > y} + rescue + e in ArithmeticError -> + {:error, e.message <> ": #{x} > #{y}"} end def apply(:ge, [x, y]) do - try do - {:ok, x >= y} - rescue - e in ArithmeticError -> - {:error, e.message <> ": #{x} >= #{y}"} - end + {:ok, x >= y} + rescue + e in ArithmeticError -> + {:error, e.message <> ": #{x} >= #{y}"} end def apply(:eq, [x, y]) do - try do - {:ok, x == y} - rescue - e in ArithmeticError -> - {:error, e.message <> ": #{x} == #{y}"} - end + {:ok, x == y} + rescue + e in ArithmeticError -> + {:error, e.message <> ": #{x} == #{y}"} end def apply(:ne, [x, y]) do - try do - {:ok, x != y} - rescue - e in ArithmeticError -> - {:error, e.message <> ": #{x} != #{y}"} - end + {:ok, x != y} + rescue + e in ArithmeticError -> + {:error, e.message <> ": #{x} != #{y}"} end def apply(:and, [x, y]) do - try do - {:ok, x && y} - rescue - e in ArithmeticError -> - {:error, e.message <> ": #{x} && #{y}"} - end + {:ok, x && y} + rescue + e in ArithmeticError -> + {:error, e.message <> ": #{x} && #{y}"} end def apply(:or, [x, y]) do - try do - {:ok, x || y} - rescue - e in ArithmeticError -> - {:error, e.message <> ": #{x} || #{y}"} - end + {:ok, x || y} + rescue + e in ArithmeticError -> + {:error, e.message <> ": #{x} || #{y}"} end # @@ -274,18 +244,18 @@ defmodule Acx.Internal.Operator do defp precedence(:or), do: 1 # Converts from charlist to an operator. - defp charlist_to_operator('.'), do: :dot - defp charlist_to_operator('!'), do: :not - defp charlist_to_operator('*'), do: :mul - defp charlist_to_operator('/'), do: :div - defp charlist_to_operator('+'), do: :add - defp charlist_to_operator('-'), do: :sub - defp charlist_to_operator('<'), do: :lt - defp charlist_to_operator('<='), do: :le - defp charlist_to_operator('>'), do: :gt - defp charlist_to_operator('>='), do: :ge - defp charlist_to_operator('=='), do: :eq - defp charlist_to_operator('!='), do: :ne - defp charlist_to_operator('&&'), do: :and - defp charlist_to_operator('||'), do: :or + defp charlist_to_operator(~c"."), do: :dot + defp charlist_to_operator(~c"!"), do: :not + defp charlist_to_operator(~c"*"), do: :mul + defp charlist_to_operator(~c"/"), do: :div + defp charlist_to_operator(~c"+"), do: :add + defp charlist_to_operator(~c"-"), do: :sub + defp charlist_to_operator(~c"<"), do: :lt + defp charlist_to_operator(~c"<="), do: :le + defp charlist_to_operator(~c">"), do: :gt + defp charlist_to_operator(~c">="), do: :ge + defp charlist_to_operator(~c"=="), do: :eq + defp charlist_to_operator(~c"!="), do: :ne + defp charlist_to_operator(~c"&&"), do: :and + defp charlist_to_operator(~c"||"), do: :or end diff --git a/lib/acx/internal/parser.ex b/lib/acx/internal/parser.ex index a6e901c..0013559 100644 --- a/lib/acx/internal/parser.ex +++ b/lib/acx/internal/parser.ex @@ -21,7 +21,22 @@ defmodule Acx.Internal.Parser do alias Acx.Internal.{Helpers, Operator} - @operators ['.', '!', '-', '+', '*', '/', '<', '<=', '>', '>=', '==', '!=', '&&', '||'] + @operators [ + ~c".", + ~c"!", + ~c"-", + ~c"+", + ~c"*", + ~c"/", + ~c"<", + ~c"<=", + ~c">", + ~c">=", + ~c"==", + ~c"!=", + ~c"&&", + ~c"||" + ] @type postfix_term() :: {:num, number()} diff --git a/lib/acx/model.ex b/lib/acx/model.ex index b3ff6bf..815b465 100644 --- a/lib/acx/model.ex +++ b/lib/acx/model.ex @@ -16,12 +16,12 @@ defmodule Acx.Model do alias Acx.Model.{ Config, - RequestDefinition, + Matcher, + Policy, PolicyDefinition, PolicyEffect, - Matcher, Request, - Policy + RequestDefinition } @type t() :: %__MODULE__{ diff --git a/lib/acx/model/config.ex b/lib/acx/model/config.ex index b671d6e..5d2b66d 100644 --- a/lib/acx/model/config.ex +++ b/lib/acx/model/config.ex @@ -49,7 +49,7 @@ defmodule Acx.Model.Config do |> validate_sections() |> case do {:error, reason} -> - # TODO: provide more information about the error. + # Enhanced error message includes file path and detailed reason { :error, "error occurred when parsing config file #{cfile}: #{reason}" @@ -198,14 +198,14 @@ defmodule Acx.Model.Config do # See one of thes ==, != , >=, <= defp parse([ch, ?= | rest], [], eq_stack, tokens, pos) - when ch in '=!><' do + when ch in ~c"=!><" do next_token = [ch, ?=] next_pos = next_col(pos, 2) parse(rest, [@default_section], eq_stack, [next_token | tokens], next_pos) end defp parse([ch, ?= | rest], secs, eq_stack, tokens, pos) - when ch in '=!><' do + when ch in ~c"=!><" do next_token = [ch, ?=] next_pos = next_col(pos, 2) parse(rest, secs, eq_stack, [next_token | tokens], next_pos) @@ -293,7 +293,7 @@ defmodule Acx.Model.Config do {[], [], 0} end - defp parse_token([ch, ?= | rest], count) when ch in '!<>=' do + defp parse_token([ch, ?= | rest], count) when ch in ~c"!<>=" do {succeeds, rem, c} = parse_token(rest, 0) {[ch, ?= | succeeds], rem, count + 2 + c} end diff --git a/lib/acx/model/matcher.ex b/lib/acx/model/matcher.ex index 111c260..2ff31c2 100644 --- a/lib/acx/model/matcher.ex +++ b/lib/acx/model/matcher.ex @@ -33,7 +33,7 @@ defmodule Acx.Model.Matcher do prog: program() } - alias Acx.Internal.{Helpers, Parser, Operator} + alias Acx.Internal.{Helpers, Operator, Parser} @unary_operators [:not, :pos, :neg] @binary_operators [:mul, :div, :add, :sub, :lt, :le, :gt, :ge, :eq, :ne, :and, :or] @@ -304,7 +304,7 @@ defmodule Acx.Model.Matcher do defp convert_from_postfix([], [expr]), do: {:ok, expr} # Zero or two more items? - # TODO: error at what position? + # Returns syntax error when stack has incorrect number of items defp convert_from_postfix([], _), do: {:error, :syntax_error} # @@ -393,7 +393,7 @@ defmodule Acx.Model.Matcher do # Dot syntax error. defp syntax_error(%{token: :dot} = arg) do - {:error, {:syntax_error, %{arg | token: '.'}}} + {:error, {:syntax_error, %{arg | token: "."}}} end # Operator syntax error. diff --git a/lib/acx/persist/ecto_adapter.ex b/lib/acx/persist/ecto_adapter.ex index 211e9f1..a2e4870 100644 --- a/lib/acx/persist/ecto_adapter.ex +++ b/lib/acx/persist/ecto_adapter.ex @@ -9,57 +9,80 @@ defmodule Acx.Persist.EctoAdapter do defstruct repo: nil defmodule CasbinRule do + @moduledoc """ + Schema for storing Casbin rules in the database. + """ import Ecto.Changeset require Ecto.Query use Ecto.Schema @columns [:ptype, :v0, :v1, :v2, :v3, :v4, :v5, :v6] schema "casbin_rule" do - field :ptype, :string - field :v0, :string - field :v1, :string - field :v2, :string - field :v3, :string - field :v4, :string - field :v5, :string - field :v6, :string + field(:ptype, :string) + field(:v0, :string) + field(:v1, :string) + field(:v2, :string) + field(:v3, :string) + field(:v4, :string) + field(:v5, :string) + field(:v6, :string) end - @doc """ # Examples - iex> CasbinRule.policy_to_map({:p, ["admin"]}, 1) |> Map.to_list + iex> CasbinRule.policy_to_map({:p, ["admin"]}, 1) |> Map.to_list |> Enum.sort [ptype: "p", v1: "admin"] - iex> CasbinRule.policy_to_map({:p, ["admin"]}) |> Map.to_list + iex> CasbinRule.policy_to_map({:p, ["admin"]}) |> Map.to_list |> Enum.sort [ptype: "p", v0: "admin"] """ @spec policy_to_map({atom(), [String.t()]}) :: %{} def policy_to_map({key, attrs}) do - Enum.zip( @columns, [Atom.to_string(key) | attrs]) |> Map.new + Enum.zip(@columns, [Atom.to_string(key) | attrs]) |> Map.new() end def policy_to_map({key, attrs}, idx) do [kcol | cols] = @columns + arr = cols |> Enum.slice(idx, length(attrs)) - |> (&([&2 | &1])).(kcol) - |> Enum.zip([Atom.to_string(key) | attrs]) |> Map.new + |> (&[&2 | &1]).(kcol) + |> Enum.zip([Atom.to_string(key) | attrs]) + |> Map.new() + arr end - @spec create_changeset({atom(), [String.t()]}) :: %CasbinRule{} + @spec create_changeset({atom(), [String.t()]}) :: Ecto.Changeset.t() def create_changeset({_key, _attrs} = policy) do changeset(%CasbinRule{}, policy_to_map(policy)) end - @spec create_changeset(String.t(),String.t(),String.t(),String.t(),String.t(),String.t(),String.t(),String.t()) :: %CasbinRule{} + @spec create_changeset( + String.t(), + String.t(), + String.t(), + String.t(), + String.t(), + String.t(), + String.t(), + String.t() + ) :: Ecto.Changeset.t() def create_changeset(ptype, v0, v1, v2 \\ nil, v3 \\ nil, v4 \\ nil, v5 \\ nil, v6 \\ nil) do - changeset(%CasbinRule{}, %{ptype: ptype, v0: v0, v1: v1, v2: v2, v3: v3, v4: v4, v5: v5, v6: v6}) + changeset(%CasbinRule{}, %{ + ptype: ptype, + v0: v0, + v1: v1, + v2: v2, + v3: v3, + v4: v4, + v5: v5, + v6: v6 + }) end def changeset(rule, params \\ %{}) do @@ -69,19 +92,19 @@ defmodule Acx.Persist.EctoAdapter do end def changeset_to_list(%{ptype: ptype, v0: v0, v1: v1, v2: v2, v3: v3, v4: v4, v5: v5, v6: v6}) do - [ptype, v0,v1,v2,v3,v4,v5,v6] |> Enum.filter(fn a -> !Kernel.is_nil(a) end) + [ptype, v0, v1, v2, v3, v4, v5, v6] |> Enum.filter(fn a -> !Kernel.is_nil(a) end) end def changeset_to_queryable({_key, _attrs} = policy, idx) do arr = policy_to_map(policy, idx) - |> Map.to_list + |> Map.to_list() Ecto.Query.from(CasbinRule, where: ^arr) end def changeset_to_queryable({key, attrs}) do - arr = Enum.zip( @columns, [Atom.to_string(key) | attrs]) + arr = Enum.zip(@columns, [Atom.to_string(key) | attrs]) Ecto.Query.from(CasbinRule, where: ^arr) end end @@ -106,7 +129,8 @@ defmodule Acx.Persist.EctoAdapter do end def load_policies(adapter) do - policies = adapter.repo.all(CasbinRule) + policies = + adapter.repo.all(CasbinRule) |> Enum.map(&CasbinRule.changeset_to_list(&1)) {:ok, policies} @@ -129,10 +153,11 @@ defmodule Acx.Persist.EctoAdapter do end def add_policy( - %Acx.Persist.EctoAdapter{repo: repo} = adapter, - {_key, _attrs} = policy - ) do + %Acx.Persist.EctoAdapter{repo: repo} = adapter, + {_key, _attrs} = policy + ) do changeset = CasbinRule.create_changeset(policy) + case repo.insert(changeset) do {:ok, _casbin} -> {:ok, adapter} {:error, changeset} -> {:error, changeset.errors} @@ -158,10 +183,11 @@ defmodule Acx.Persist.EctoAdapter do end def remove_policy( - %Acx.Persist.EctoAdapter{repo: repo} = adapter, - {_key, _attr} = policy - ) do + %Acx.Persist.EctoAdapter{repo: repo} = adapter, + {_key, _attr} = policy + ) do f = CasbinRule.changeset_to_queryable(policy) + case repo.delete_all(f) do {:error, changeset} -> {:error, changeset.errors} _ -> {:ok, adapter} @@ -169,19 +195,19 @@ defmodule Acx.Persist.EctoAdapter do end def remove_filtered_policy( - %Acx.Persist.EctoAdapter{repo: repo} = adapter, - key, - idx, - attrs - ) do + %Acx.Persist.EctoAdapter{repo: repo} = adapter, + key, + idx, + attrs + ) do f = CasbinRule.changeset_to_queryable({key, attrs}, idx) + case repo.delete_all(f) do {:error, changeset} -> {:error, changeset.errors} _ -> {:ok, adapter} end end - @doc """ Truncates the table and inserts the provided policies. @@ -199,18 +225,23 @@ defmodule Acx.Persist.EctoAdapter do end def save_policies( - %Acx.Persist.EctoAdapter{repo: repo} = adapter, - policies - ) do - repo.transaction fn -> - repo.delete_all(CasbinRule) - Enum.each(policies, fn policy -> - changeset = CasbinRule.create_changeset(policy) - case repo.insert(changeset) do - {:ok, _casbin} -> adapter - {:error, changeset} -> {:error, changeset.errors} - end - end) + %Acx.Persist.EctoAdapter{repo: repo} = adapter, + policies + ) do + repo.transaction(fn -> insert_policies(repo, adapter, policies) end) + end + + defp insert_policies(repo, adapter, policies) do + repo.delete_all(CasbinRule) + Enum.each(policies, &insert_policy(repo, adapter, &1)) + end + + defp insert_policy(repo, adapter, policy) do + changeset = CasbinRule.create_changeset(policy) + + case repo.insert(changeset) do + {:ok, _casbin} -> adapter + {:error, changeset} -> {:error, changeset.errors} end end end diff --git a/lib/acx/persist/readonly_file_adapter.ex b/lib/acx/persist/readonly_file_adapter.ex index 7d0c4e5..5ab416a 100644 --- a/lib/acx/persist/readonly_file_adapter.ex +++ b/lib/acx/persist/readonly_file_adapter.ex @@ -1,14 +1,12 @@ defmodule Acx.Persist.ReadonlyFileAdapter do + @moduledoc """ + A read-only file adapter for loading policies from files. + """ alias Acx.Persist.PersistAdapter - # @callback load_policies() :: any() - # @callback update_policy(id :: Integer.t, ptype :: String.t, v0 :: String.t, v1 :: String.t, v2 :: String.t, v3 :: String.t, v4 :: String.t, v5 :: String.t, v6 :: String.t) :: any() - # @callback remove_policy(id :: Integrer.t) :: any() - # @callback save_policy() :: any() - defstruct policy_file: nil - def new() do + def new do %__MODULE__{policy_file: nil} end @@ -22,18 +20,20 @@ defmodule Acx.Persist.ReadonlyFileAdapter do end def load_policies(adapter) do - policies = adapter.policy_file - |> File.read! - |> String.split("\n", trim: true) - |> Enum.map(&String.split(&1, ~r{,\s*})) + policies = + adapter.policy_file + |> File.read!() + |> String.split("\n", trim: true) + |> Enum.map(&String.split(&1, ~r{,\s*})) {:ok, policies} end def load_policies(_adapter, pfile) do - policies = File.read!(pfile) - |> String.split("\n", trim: true) - |> Enum.map(&String.split(&1, ~r{,\s*})) + policies = + File.read!(pfile) + |> String.split("\n", trim: true) + |> Enum.map(&String.split(&1, ~r{,\s*})) {:ok, policies} end @@ -46,13 +46,12 @@ defmodule Acx.Persist.ReadonlyFileAdapter do {:ok, adapter} end - def remove_policy(adapter, _policy) do + def remove_policy(adapter, _policy) do {:ok, adapter} end def remove_filtered_policy(adapter, _key, _idx, _attrs) do {:ok, adapter} end - end end diff --git a/mix.exs b/mix.exs index 5cb6119..655750a 100644 --- a/mix.exs +++ b/mix.exs @@ -27,7 +27,9 @@ defmodule Acx.MixProject do # Run "mix help deps" to learn about dependencies. defp deps do [ - {:ecto_sql, "~> 3.6"} + {:ecto_sql, "~> 3.10"}, + {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, + {:git_hooks, "~> 0.7.3", only: [:dev], runtime: false} # {:dep_from_hexpm, "~> 0.3.0"}, # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} ] diff --git a/mix.lock b/mix.lock index df97dd5..01fb368 100644 --- a/mix.lock +++ b/mix.lock @@ -1,8 +1,23 @@ %{ + "blankable": {:hex, :blankable, "1.0.0", "89ab564a63c55af117e115144e3b3b57eb53ad43ba0f15553357eb283e0ed425", [:mix], [], "hexpm", "7cf11aac0e44f4eedbee0c15c1d37d94c090cb72a8d9fddf9f7aec30f9278899"}, + "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"}, - "db_connection": {:hex, :db_connection, "2.4.3", "3b9aac9f27347ec65b271847e6baeb4443d8474289bd18c1d6f4de655b70c94d", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c127c15b0fa6cfb32eed07465e05da6c815b032508d4ed7c116122871df73c12"}, - "decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"}, - "ecto": {:hex, :ecto, "3.9.4", "3ee68e25dbe0c36f980f1ba5dd41ee0d3eb0873bccae8aeaf1a2647242bffa35", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "de5f988c142a3aa4ec18b85a4ec34a2390b65b24f02385c1144252ff6ff8ee75"}, - "ecto_sql": {:hex, :ecto_sql, "3.9.2", "34227501abe92dba10d9c3495ab6770e75e79b836d114c41108a4bf2ce200ad5", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9.2", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1eb5eeb4358fdbcd42eac11c1fbd87e3affd7904e639d77903c1358b2abd3f70"}, - "telemetry": {:hex, :telemetry, "1.1.0", "a589817034a27eab11144ad24d5c0f9fab1f58173274b1e9bae7074af9cbee51", [:rebar3], [], "hexpm", "b727b2a1f75614774cff2d7565b64d0dfa5bd52ba517f16543e6fc7efcc0df48"}, + "credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"}, + "db_connection": {:hex, :db_connection, "2.8.1", "9abdc1e68c34c6163f6fb96a96532272d13ad7ca45262156ae8b7ec6d9dc4bec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a61a3d489b239d76f326e03b98794fb8e45168396c925ef25feb405ed09da8fd"}, + "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, + "dialyxir": {:hex, :dialyxir, "1.4.6", "7cca478334bf8307e968664343cbdb432ee95b4b68a9cba95bdabb0ad5bdfd9a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "8cf5615c5cd4c2da6c501faae642839c8405b49f8aa057ad4ae401cb808ef64d"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, + "ecto": {:hex, :ecto, "3.13.3", "6a983f0917f8bdc7a89e96f2bf013f220503a0da5d8623224ba987515b3f0d80", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1927db768f53a88843ff25b6ba7946599a8ca8a055f69ad8058a1432a399af94"}, + "ecto_sql": {:hex, :ecto_sql, "3.13.2", "a07d2461d84107b3d037097c822ffdd36ed69d1cf7c0f70e12a3d1decf04e2e1", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "539274ab0ecf1a0078a6a72ef3465629e4d6018a3028095dc90f60a19c371717"}, + "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, + "ex_doc": {:hex, :ex_doc, "0.38.4", "ab48dff7a8af84226bf23baddcdda329f467255d924380a0cf0cee97bb9a9ede", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "f7b62346408a83911c2580154e35613eb314e0278aeea72ed7fedef9c1f165b2"}, + "file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"}, + "git_hooks": {:hex, :git_hooks, "0.7.4", "426f643e48ae3cc5737cdaa5da093105be88080ed1551b6014952e772d3c5b88", [:mix], [{:blankable, "~> 1.0.0", [hex: :blankable, repo: "hexpm", optional: false]}, {:recase, "~> 0.8.0", [hex: :recase, repo: "hexpm", optional: false]}], "hexpm", "1b26945c8325f5258fb89e3954ad392cc6c1df94836e88fd099c933a8cc89ab9"}, + "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, + "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, + "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, + "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, + "recase": {:hex, :recase, "0.8.1", "ab98cd35857a86fa5ca99036f575241d71d77d9c2ab0c39aacf1c9b61f6f7d1d", [:mix], [], "hexpm", "9fd8d63e7e43bd9ea385b12364e305778b2bbd92537e95c4b2e26fc507d5e4c2"}, + "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, } diff --git a/test/enforcer/acl_model_test.exs b/test/enforcer/acl_model_test.exs index c62c9fb..8a8f62e 100644 --- a/test/enforcer/acl_model_test.exs +++ b/test/enforcer/acl_model_test.exs @@ -1,7 +1,7 @@ defmodule Acx.Enforcer.AclModelTest do use ExUnit.Case, async: true - alias Acx.Model.Policy alias Acx.Enforcer + alias Acx.Model.Policy @cfile "../data/acl.conf" |> Path.expand(__DIR__) @pfile "../data/acl.csv" |> Path.expand(__DIR__) diff --git a/test/enforcer/rbac_domain_model_test.exs b/test/enforcer/rbac_domain_model_test.exs index a7ce53e..859ea1f 100644 --- a/test/enforcer/rbac_domain_model_test.exs +++ b/test/enforcer/rbac_domain_model_test.exs @@ -48,7 +48,7 @@ defmodule Acx.Enforcer.RbacDomainModelTest do end) end - describe "removed role allow?/2" do + describe "removed role allow?/2" do @test_cases [ {["alice", "domain1", "data1", "read"], false}, {["alice", "domain1", "data1", "write"], false}, @@ -56,7 +56,7 @@ defmodule Acx.Enforcer.RbacDomainModelTest do {["alice", "domain2", "data2", "write"], true}, {["alice", "domain2", "data2", "no_existing"], false}, {["alice", "domain2", "no_existing", "read"], false}, - {["alice", "domain3", "data2", "read"], false}, + {["alice", "domain3", "data2", "read"], false} ] Enum.each(@test_cases, fn {req, res} -> diff --git a/test/model/policy_definition_test.exs b/test/model/policy_definition_test.exs index 3e5fec6..7e97099 100644 --- a/test/model/policy_definition_test.exs +++ b/test/model/policy_definition_test.exs @@ -1,5 +1,5 @@ defmodule Acx.Model.PolicyDefinitionTest do use ExUnit.Case, async: true - alias Acx.Model.{PolicyDefinition, Policy} + alias Acx.Model.{Policy, PolicyDefinition} doctest Acx.Model.PolicyDefinition end diff --git a/test/model/policy_effect_test.exs b/test/model/policy_effect_test.exs index ab41fc5..790061d 100644 --- a/test/model/policy_effect_test.exs +++ b/test/model/policy_effect_test.exs @@ -1,5 +1,5 @@ defmodule Acx.Model.PolicyEffectTest do use ExUnit.Case, async: true - alias Acx.Model.{PolicyEffect, PolicyDefinition} + alias Acx.Model.{PolicyDefinition, PolicyEffect} doctest Acx.Model.PolicyEffect end diff --git a/test/model/request_definition_test.exs b/test/model/request_definition_test.exs index 2de86da..2ada2a5 100644 --- a/test/model/request_definition_test.exs +++ b/test/model/request_definition_test.exs @@ -1,5 +1,5 @@ defmodule Acx.Model.RequestDefinitionTest do use ExUnit.Case, async: true - alias Acx.Model.{RequestDefinition, Request} + alias Acx.Model.{Request, RequestDefinition} doctest Acx.Model.RequestDefinition end diff --git a/test/model_test.exs b/test/model_test.exs index 5334990..b23e4b4 100644 --- a/test/model_test.exs +++ b/test/model_test.exs @@ -3,12 +3,12 @@ defmodule Acx.ModelTest do alias Acx.Model alias Acx.Model.{ - RequestDefinition, + Matcher, + Policy, PolicyDefinition, PolicyEffect, - Matcher, Request, - Policy + RequestDefinition } doctest Acx.Model diff --git a/test/persist/ecto_acl_test.exs b/test/persist/ecto_acl_test.exs index f14c061..ec52a96 100644 --- a/test/persist/ecto_acl_test.exs +++ b/test/persist/ecto_acl_test.exs @@ -1,6 +1,7 @@ defmodule Acx.Persist.EctoAclTest do use ExUnit.Case, async: true alias Acx.Enforcer + alias Acx.Persist.EctoAdapter defmodule MockAclRepo do use Acx.Persist.MockRepo, pfile: "../data/acl.csv" |> Path.expand(__DIR__) @@ -10,14 +11,13 @@ defmodule Acx.Persist.EctoAclTest do @repo MockAclRepo setup do - adapter = Acx.Persist.EctoAdapter.new(@repo) + adapter = EctoAdapter.new(@repo) {:ok, e} = Enforcer.init(@cfile, adapter) e = Enforcer.load_policies!(e) {:ok, e: e} end - describe "allow?/2" do @test_cases [ {["alice", "blog_post", "create"], true}, @@ -44,5 +44,4 @@ defmodule Acx.Persist.EctoAclTest do end end) end - end diff --git a/test/persist/ecto_adapter_test.exs b/test/persist/ecto_adapter_test.exs index 11e4c4e..ce81b76 100644 --- a/test/persist/ecto_adapter_test.exs +++ b/test/persist/ecto_adapter_test.exs @@ -1,7 +1,8 @@ defmodule Acx.Persist.EctoAdapterTest do use ExUnit.Case, async: true - alias Acx.Persist.PersistAdapter + alias Acx.Persist.EctoAdapter alias Acx.Persist.EctoAdapter.CasbinRule + alias Acx.Persist.PersistAdapter doctest Acx.Persist.EctoAdapter doctest Acx.Persist.PersistAdapter.Acx.Persist.EctoAdapter doctest Acx.Persist.EctoAdapter.CasbinRule @@ -14,19 +15,22 @@ defmodule Acx.Persist.EctoAdapterTest do @repo MockTestRepo test "loads policies from the database" do - expected = {:ok, [ - ["p", "alice", "blog_post", "create"], - ["p", "alice", "blog_post", "delete"], - ["p", "alice", "blog_post", "modify"], - ["p", "alice", "blog_post", "read"], - ["p", "bob", "blog_post", "read"], - ["p", "peter", "blog_post", "create"], - ["p", "peter", "blog_post", "modify"], - ["p", "peter", "blog_post", "read"] - ]} + expected = + {:ok, + [ + ["p", "alice", "blog_post", "create"], + ["p", "alice", "blog_post", "delete"], + ["p", "alice", "blog_post", "modify"], + ["p", "alice", "blog_post", "read"], + ["p", "bob", "blog_post", "read"], + ["p", "peter", "blog_post", "create"], + ["p", "peter", "blog_post", "modify"], + ["p", "peter", "blog_post", "read"] + ]} - loaded = Acx.Persist.EctoAdapter.new(@repo) - |> Acx.Persist.PersistAdapter.load_policies + loaded = + EctoAdapter.new(@repo) + |> PersistAdapter.load_policies() assert loaded === expected end diff --git a/test/persist/ecto_rbac_domain_test.exs b/test/persist/ecto_rbac_domain_test.exs index 9834a14..7d97652 100644 --- a/test/persist/ecto_rbac_domain_test.exs +++ b/test/persist/ecto_rbac_domain_test.exs @@ -1,6 +1,7 @@ defmodule Acx.Persist.EctoRbacDomainTest do use ExUnit.Case, async: true alias Acx.Enforcer + alias Acx.Persist.EctoAdapter @cfile "../data/rbac_domain.conf" |> Path.expand(__DIR__) @@ -11,11 +12,12 @@ defmodule Acx.Persist.EctoRbacDomainTest do @repo MockAclRepo setup do - adapter = Acx.Persist.EctoAdapter.new(@repo) + adapter = EctoAdapter.new(@repo) {:ok, e} = Enforcer.init(@cfile, adapter) - e = Enforcer.load_policies!(e) - |> Enforcer.load_mapping_policies! + e = + Enforcer.load_policies!(e) + |> Enforcer.load_mapping_policies!() {:ok, e: e} end diff --git a/test/persist/ecto_rbac_test.exs b/test/persist/ecto_rbac_test.exs index 179bb3c..c824c57 100644 --- a/test/persist/ecto_rbac_test.exs +++ b/test/persist/ecto_rbac_test.exs @@ -1,6 +1,7 @@ defmodule Acx.Persist.EctoRbacTest do use ExUnit.Case, async: true alias Acx.Enforcer + alias Acx.Persist.EctoAdapter @cfile "../data/rbac.conf" |> Path.expand(__DIR__) @@ -11,11 +12,12 @@ defmodule Acx.Persist.EctoRbacTest do @repo MockAclRepo setup do - adapter = Acx.Persist.EctoAdapter.new(@repo) + adapter = EctoAdapter.new(@repo) {:ok, e} = Enforcer.init(@cfile, adapter) - e = Enforcer.load_policies!(e) - |> Enforcer.load_mapping_policies! + e = + Enforcer.load_policies!(e) + |> Enforcer.load_mapping_policies!() {:ok, e: e} end @@ -50,6 +52,7 @@ defmodule Acx.Persist.EctoRbacTest do describe "when removed mapping policy author -> reader" do setup [:setup_delete_author_role] + @test_cases [ {["bob", "blog_post", "read"], true}, {["bob", "blog_post", "create"], false}, @@ -71,6 +74,4 @@ defmodule Acx.Persist.EctoRbacTest do end end) end - - end diff --git a/test/persist/readonly_file_adapter_test.exs b/test/persist/readonly_file_adapter_test.exs index d33366a..cdbcd21 100644 --- a/test/persist/readonly_file_adapter_test.exs +++ b/test/persist/readonly_file_adapter_test.exs @@ -1,29 +1,31 @@ defmodule Acx.Persist.ReadonlyFileAdapterTest do -use ExUnit.Case, async: true -alias Acx.Persist.PersistAdapter -doctest Acx.Persist.ReadonlyFileAdapter + use ExUnit.Case, async: true + alias Acx.Persist.PersistAdapter + alias Acx.Persist.ReadonlyFileAdapter + doctest Acx.Persist.ReadonlyFileAdapter + describe "given a policy file" do + @pfile "../data/acl.csv" |> Path.expand(__DIR__) -describe "given a policy file" do - @pfile "../data/acl.csv" |> Path.expand(__DIR__) + test "loads all of the policies" do + expected = + {:ok, + [ + ["p", "alice", "blog_post", "create"], + ["p", "alice", "blog_post", "delete"], + ["p", "alice", "blog_post", "modify"], + ["p", "alice", "blog_post", "read"], + ["p", "bob", "blog_post", "read"], + ["p", "peter", "blog_post", "create"], + ["p", "peter", "blog_post", "modify"], + ["p", "peter", "blog_post", "read"] + ]} - test "loads all of the policies" do - expected = {:ok, [ - ["p", "alice", "blog_post", "create"], - ["p", "alice", "blog_post", "delete"], - ["p", "alice", "blog_post", "modify"], - ["p", "alice", "blog_post", "read"], - ["p", "bob", "blog_post", "read"], - ["p", "peter", "blog_post", "create"], - ["p", "peter", "blog_post", "modify"], - ["p", "peter", "blog_post", "read"] - ]} + loaded = + ReadonlyFileAdapter.new(@pfile) + |> PersistAdapter.load_policies() - - loaded = Acx.Persist.ReadonlyFileAdapter.new(@pfile) - |> PersistAdapter.load_policies - assert loaded === expected + assert loaded === expected + end end end - -end diff --git a/test/support/mock_repo.ex b/test/support/mock_repo.ex index bf68cf0..9def80b 100644 --- a/test/support/mock_repo.ex +++ b/test/support/mock_repo.ex @@ -1,20 +1,24 @@ defmodule Acx.Persist.MockRepo do + @moduledoc """ + Mock repository for testing Ecto adapter functionality. + """ defmacro __using__(opts) do pfile = opts[:pfile] + quote do - alias Ecto.Changeset alias Acx.Persist.EctoAdapter.CasbinRule + alias Ecto.Changeset def to_changeset(id, rule) do Enum.zip([:ptype, :v0, :v1, :v2, :v3, :v4, :v5, :v6], rule) - |> Map.new + |> Map.new() |> then(&Map.merge(%Acx.Persist.EctoAdapter.CasbinRule{id: id}, &1)) end def all(CasbinRule, _opts \\ []) do unquote(pfile) - |> File.read! + |> File.read!() |> String.split("\n", trim: true) |> Enum.map(&String.split(&1, ~r{,\s*})) |> Enum.with_index(1) @@ -36,5 +40,4 @@ defmodule Acx.Persist.MockRepo do end end end - end