From e93f335ebdbbdcaa22d88de4d3f96b69de0283de Mon Sep 17 00:00:00 2001 From: Victor del Pino Date: Wed, 29 Apr 2026 19:17:20 +0200 Subject: [PATCH 01/10] feat: TD-8083 Add user group management features - Introduced `get_group_by_name` function to retrieve groups by their name. - Added support for handling multiple group names in `get_group_by_name`. - Updated `do_put_group` to include an alias for groups. - Enhanced `read_group_by_name` to fetch groups based on a list of names. - Modified tests to cover new group alias functionality and retrieval methods. --- CHANGELOG.md | 6 +++ lib/td_cache/templates/acl_loader.ex | 2 +- lib/td_cache/user_cache.ex | 52 +++++++++++++++++++---- test/support/factory.ex | 3 +- test/td_cache/user_cache_test.exs | 62 ++++++++++++++++++++++++++++ 5 files changed, 116 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 290db4c..eb44a02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [Unreleased] + +### Added + +- [TD-8083] Add user group management features + ## [8.3.0] 2026-03-12 ### Changed diff --git a/lib/td_cache/templates/acl_loader.ex b/lib/td_cache/templates/acl_loader.ex index 986f6a1..9d74c42 100644 --- a/lib/td_cache/templates/acl_loader.ex +++ b/lib/td_cache/templates/acl_loader.ex @@ -82,7 +82,7 @@ defmodule TdCache.Templates.AclLoader do |> Enum.map(fn group_id -> case UserCache.get_group(group_id) do {:ok, nil} -> nil - {:ok, group} -> Map.take(group, [:id, :name]) + {:ok, group} -> Map.take(group, [:id, :name, :alias]) end end) |> Enum.reject(&is_nil/1) diff --git a/lib/td_cache/user_cache.ex b/lib/td_cache/user_cache.ex index 8bd3130..3ae7f16 100644 --- a/lib/td_cache/user_cache.ex +++ b/lib/td_cache/user_cache.ex @@ -27,6 +27,8 @@ defmodule TdCache.UserCache do "user:#{id}:roles" end + def user_group_name_to_id, do: "user_group:user_group_name_to_id" + def user_group(id) do "user_group:#{id}" end @@ -118,6 +120,10 @@ defmodule TdCache.UserCache do GenServer.call(__MODULE__, {:get_group, id}) end + def get_group_by_name(name) do + GenServer.call(__MODULE__, {:get_group_by_name, name}) + end + def put(user) do GenServer.call(__MODULE__, {:put, user}) end @@ -191,6 +197,11 @@ defmodule TdCache.UserCache do {:reply, {:ok, group}, state} end + def handle_call({:get_group_by_name, name}, _from, state) do + group = read_group_by_name(name) + {:reply, {:ok, group}, state} + end + def handle_call({:put, user}, _from, state) do reply = put_user(user) {:reply, reply, state} @@ -279,6 +290,17 @@ defmodule TdCache.UserCache do end end + defp read_group_by_name(names) when is_list(names), do: Enum.map(names, &read_group_by_name/1) + + defp read_group_by_name(name) when is_binary(name) do + name = String.replace_prefix(name, "group:", "") + + case Redix.command!(["HGET", Keys.user_group_name_to_id(), name]) do + nil -> nil + id -> read_group(id) + end + end + defp read_group(id) when is_binary(id) do id |> String.to_integer() @@ -416,20 +438,36 @@ defmodule TdCache.UserCache do end) end - defp do_put_group(%{id: id, name: name}) do + defp do_put_group(%{id: id, name: name, alias: alias} = group) do [ ["DEL", Keys.user_group(id)], - ["HSET", Keys.user_group(id), %{name: name}], + ["HSET", Keys.user_group(id), %{name: name, alias: alias}], ["SADD", Keys.group_ids(), id] ] + |> add_group_name(group) |> Redix.transaction_pipeline() end + defp add_group_name(pipeline, %{id: id, name: name}) do + pipeline ++ [["HSET", Keys.user_group_name_to_id(), name, id]] + end + defp do_delete_group(id) do - Redix.transaction_pipeline([ - ["DEL", Keys.user_group(id)], - ["DEL", Keys.user_group_roles(id)], - ["SREM", Keys.group_ids(), id] - ]) + case Redix.command!(["HMGET", Keys.user_group(id), "alias", "name"]) do + [nil, nil] -> + Redix.transaction_pipeline([ + ["DEL", Keys.user_group(id)], + ["DEL", Keys.user_group_roles(id)], + ["SREM", Keys.group_ids(), id] + ]) + + [_alias, name] -> + Redix.transaction_pipeline([ + ["DEL", Keys.user_group(id)], + ["DEL", Keys.user_group_roles(id)], + ["SREM", Keys.group_ids(), id], + ["HDEL", Keys.user_group_name_to_id(), name] + ]) + end end end diff --git a/test/support/factory.ex b/test/support/factory.ex index ac10698..266b846 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -109,7 +109,8 @@ defmodule TdCache.Factory do %{ id: unique_id(), name: sequence("group_name"), - description: sequence("group_description") + description: sequence("group_description"), + alias: sequence("group_alias") } end diff --git a/test/td_cache/user_cache_test.exs b/test/td_cache/user_cache_test.exs index b00c567..2930b21 100644 --- a/test/td_cache/user_cache_test.exs +++ b/test/td_cache/user_cache_test.exs @@ -250,8 +250,70 @@ defmodule TdCache.UserCacheTest do end end + describe "user_groups" do + test "put_group returns OK" do + group = build(:group) + assert {:ok, [_, 2, 1, 1]} = put_user_group(group) + end + + test "get_group returns a map with name and alias" do + group = build(:group) + put_user_group(group) + {:ok, g} = UserCache.get_group(group.id) + assert g == Map.take(group, [:name, :alias, :id]) + end + + test "get_group_by_name returns a map with name and alias " do + group = build(:group) + put_user_group(group) + {:ok, g} = UserCache.get_group_by_name(group.name) + assert g == Map.take(group, [:name, :alias, :id]) + end + + test "get_group_by_name returns groups from a list of names" do + group1 = build(:group) + group2 = build(:group) + put_user_group(group1) + put_user_group(group2) + + {:ok, groups} = UserCache.get_group_by_name([group1.name, group2.name]) + + assert groups == [ + Map.take(group1, [:name, :alias, :id]), + Map.take(group2, [:name, :alias, :id]) + ] + end + + test "get_group_by_name returns groups from a list of group names and user names" do + group1 = build(:group) + group2 = build(:group) + put_user_group(group1) + put_user_group(group2) + + user1 = build(:user) + user2 = build(:user) + put_user(user1) + put_user(user2) + + {:ok, groups} = + UserCache.get_group_by_name([group1.name, group2.name, user1.user_name, user2.user_name]) + + assert groups == [ + Map.take(group1, [:name, :alias, :id]), + Map.take(group2, [:name, :alias, :id]), + nil, + nil + ] + end + end + defp put_user(%{id: id} = user) do on_exit(fn -> UserCache.delete(id) end) UserCache.put(user) end + + defp put_user_group(%{id: id} = group) do + on_exit(fn -> UserCache.delete_group(id) end) + UserCache.put_group(group) + end end From 28004d11d593c5ff46d5236a10561812d1b3a710 Mon Sep 17 00:00:00 2001 From: Victor del Pino Date: Thu, 30 Apr 2026 10:40:29 +0200 Subject: [PATCH 02/10] feat: TD-8083 Enhance user group management with alias support - Added `add_group_alias` and `delete_group_name` functions to handle group aliases in the user cache. - Updated `do_put_group` and `do_delete_group` to manage group aliases during group operations. - Modified `get_group_by_name` to retrieve groups by their alias. - Adjusted tests to validate new alias functionality in user group management and template processing. --- lib/td_cache/templates/field_formatter.ex | 5 +++- lib/td_cache/user_cache.ex | 25 +++++++++++++++---- test/td_cache/templates/preprocessor_test.exs | 8 +++--- test/td_cache/user_cache_test.exs | 11 +++++++- 4 files changed, 38 insertions(+), 11 deletions(-) diff --git a/lib/td_cache/templates/field_formatter.ex b/lib/td_cache/templates/field_formatter.ex index 8a105b0..14fcdf3 100644 --- a/lib/td_cache/templates/field_formatter.ex +++ b/lib/td_cache/templates/field_formatter.ex @@ -77,10 +77,13 @@ defmodule TdCache.Templates.FieldFormatter do ) when not is_nil(role_name) do groups = Map.get(user_group_roles, role_name, []) - names = Enum.map(groups, & &1.name) + names = Enum.map(groups, &group_name_or_alias/1) values = Map.put(values, "processed_groups", names) Map.put(field, "values", values) end defp apply_user_group_meta(field, _role, _user_roles), do: field + + defp group_name_or_alias(%{alias: alias, name: name}) when alias in [nil, ""], do: name + defp group_name_or_alias(%{alias: alias}), do: alias end diff --git a/lib/td_cache/user_cache.ex b/lib/td_cache/user_cache.ex index 3ae7f16..6f70e92 100644 --- a/lib/td_cache/user_cache.ex +++ b/lib/td_cache/user_cache.ex @@ -445,6 +445,7 @@ defmodule TdCache.UserCache do ["SADD", Keys.group_ids(), id] ] |> add_group_name(group) + |> add_group_alias(group) |> Redix.transaction_pipeline() end @@ -452,6 +453,12 @@ defmodule TdCache.UserCache do pipeline ++ [["HSET", Keys.user_group_name_to_id(), name, id]] end + defp add_group_alias(pipeline, %{id: id, alias: alias}) when alias not in [nil, ""] do + pipeline ++ [["HSET", Keys.user_group_name_to_id(), alias, id]] + end + + defp add_group_alias(pipeline, _), do: pipeline + defp do_delete_group(id) do case Redix.command!(["HMGET", Keys.user_group(id), "alias", "name"]) do [nil, nil] -> @@ -461,13 +468,21 @@ defmodule TdCache.UserCache do ["SREM", Keys.group_ids(), id] ]) - [_alias, name] -> - Redix.transaction_pipeline([ + [alias, name] -> + [ ["DEL", Keys.user_group(id)], ["DEL", Keys.user_group_roles(id)], - ["SREM", Keys.group_ids(), id], - ["HDEL", Keys.user_group_name_to_id(), name] - ]) + ["SREM", Keys.group_ids(), id] + ] + |> delete_group_name(name) + |> delete_group_name(alias) + |> Redix.transaction_pipeline() end end + + defp delete_group_name(pipeline, name) when name not in [nil, ""] do + pipeline ++ [["HDEL", Keys.user_group_name_to_id(), name]] + end + + defp delete_group_name(pipeline, _), do: pipeline end diff --git a/test/td_cache/templates/preprocessor_test.exs b/test/td_cache/templates/preprocessor_test.exs index c841df8..be11fbb 100644 --- a/test/td_cache/templates/preprocessor_test.exs +++ b/test/td_cache/templates/preprocessor_test.exs @@ -52,7 +52,7 @@ defmodule TdCache.Templates.PreprocessorTest do test "preprocess_template/2 enriches user_group role fields" do %{id: domain_id} = CacheHelpers.insert_domain() %{id: user_id, full_name: full_name} = CacheHelpers.insert_user() - %{id: group_id, name: group_name} = CacheHelpers.insert_group() + %{id: group_id, alias: group_alias} = CacheHelpers.insert_group() AclCache.set_acl_roles("domain", domain_id, [@role_name]) AclCache.set_acl_group_roles("domain", domain_id, [@role_name]) @@ -83,7 +83,7 @@ defmodule TdCache.Templates.PreprocessorTest do "values" => %{ "role_groups" => @role_name, "processed_users" => [full_name], - "processed_groups" => [group_name] + "processed_groups" => [group_alias] } } @@ -131,7 +131,7 @@ defmodule TdCache.Templates.PreprocessorTest do test "preprocess_template/2 process dynamic table type fields" do %{id: domain_id} = CacheHelpers.insert_domain() %{id: user_id, full_name: full_name} = CacheHelpers.insert_user() - %{id: group_id, name: group_name} = CacheHelpers.insert_group() + %{id: group_id, alias: group_alias} = CacheHelpers.insert_group() AclCache.set_acl_roles("domain", domain_id, [@role_name]) AclCache.set_acl_group_roles("domain", domain_id, [@role_name]) @@ -206,7 +206,7 @@ defmodule TdCache.Templates.PreprocessorTest do "name" => "user_group_field_col", "type" => "user_group", "values" => %{ - "processed_groups" => [group_name], + "processed_groups" => [group_alias], "processed_users" => [full_name], "role_groups" => "foo_role" } diff --git a/test/td_cache/user_cache_test.exs b/test/td_cache/user_cache_test.exs index 2930b21..48cb8c1 100644 --- a/test/td_cache/user_cache_test.exs +++ b/test/td_cache/user_cache_test.exs @@ -253,7 +253,7 @@ defmodule TdCache.UserCacheTest do describe "user_groups" do test "put_group returns OK" do group = build(:group) - assert {:ok, [_, 2, 1, 1]} = put_user_group(group) + assert {:ok, [_, 2, 1, 1, 1]} = put_user_group(group) end test "get_group returns a map with name and alias" do @@ -270,6 +270,15 @@ defmodule TdCache.UserCacheTest do assert g == Map.take(group, [:name, :alias, :id]) end + test "get_group_by_name returns a group by alias" do + group = build(:group) + put_user_group(group) + + {:ok, g} = UserCache.get_group_by_name(group.alias) + + assert g == Map.take(group, [:name, :alias, :id]) + end + test "get_group_by_name returns groups from a list of names" do group1 = build(:group) group2 = build(:group) From a48b19e3d7eeac4a6f63d0909a46a00bc70498aa Mon Sep 17 00:00:00 2001 From: Victor del Pino Date: Thu, 30 Apr 2026 11:00:25 +0200 Subject: [PATCH 03/10] feat: TD-8083 Add support for group role fields in template processing - Implemented a new `format` function for handling group type fields in the template processor. - Enhanced the `preprocess_template` test to validate the enrichment of group role fields with associated group aliases. - Updated tests to ensure correct processing of group roles in templates. --- lib/td_cache/templates/field_formatter.ex | 5 +++ test/td_cache/templates/preprocessor_test.exs | 35 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/lib/td_cache/templates/field_formatter.ex b/lib/td_cache/templates/field_formatter.ex index 14fcdf3..524a4b1 100644 --- a/lib/td_cache/templates/field_formatter.ex +++ b/lib/td_cache/templates/field_formatter.ex @@ -19,6 +19,11 @@ defmodule TdCache.Templates.FieldFormatter do apply_role_meta(field, claims, role_name, user_roles) end + def format(%{"type" => "group", "values" => %{"role_groups" => role_name}} = field, ctx) do + user_group_roles = Map.get(ctx, :user_group_roles, %{}) + apply_user_group_meta(field, role_name, user_group_roles) + end + def format(%{"type" => "user_group", "values" => %{"role_groups" => role_name}} = field, ctx) do claims = Map.get(ctx, :claims, nil) user_roles = Map.get(ctx, :user_roles, %{}) diff --git a/test/td_cache/templates/preprocessor_test.exs b/test/td_cache/templates/preprocessor_test.exs index be11fbb..af21ee2 100644 --- a/test/td_cache/templates/preprocessor_test.exs +++ b/test/td_cache/templates/preprocessor_test.exs @@ -212,5 +212,40 @@ defmodule TdCache.Templates.PreprocessorTest do } } end + + test "preprocess_template/2 enriches group role fields with groups only" do + %{id: domain_id} = CacheHelpers.insert_domain() + %{id: user_id} = CacheHelpers.insert_user() + %{id: group_id, alias: group_alias} = CacheHelpers.insert_group() + + AclCache.set_acl_roles("domain", domain_id, [@role_name]) + AclCache.set_acl_group_roles("domain", domain_id, [@role_name]) + AclCache.set_acl_role_users("domain", domain_id, @role_name, [user_id]) + AclCache.set_acl_role_groups("domain", domain_id, @role_name, [group_id]) + + ctx = %{domain_ids: [domain_id], claims: %{user_id: user_id}} + + fields = [ + %{ + "name" => "group_field", + "type" => "group", + "values" => %{"role_groups" => @role_name} + } + ] + + template = %{content: [%{"name" => "group1", "fields" => fields}]} + + assert %{content: [%{"fields" => [group_field]}]} = + Preprocessor.preprocess_template(template, ctx) + + assert group_field == %{ + "name" => "group_field", + "type" => "group", + "values" => %{ + "role_groups" => @role_name, + "processed_groups" => [group_alias] + } + } + end end end From 2dd18ad170b73ece2e0056442315d648a2c80c00 Mon Sep 17 00:00:00 2001 From: Guillermo Rodriguez Date: Wed, 6 May 2026 13:08:19 +0200 Subject: [PATCH 04/10] fix: TD-8083 Update user cache to correctly handle group name and alias during deletion --- lib/td_cache/user_cache.ex | 17 ++++++----- test/td_cache/user_cache_test.exs | 50 +++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 8 deletions(-) diff --git a/lib/td_cache/user_cache.ex b/lib/td_cache/user_cache.ex index 6f70e92..ac9e1ea 100644 --- a/lib/td_cache/user_cache.ex +++ b/lib/td_cache/user_cache.ex @@ -293,9 +293,9 @@ defmodule TdCache.UserCache do defp read_group_by_name(names) when is_list(names), do: Enum.map(names, &read_group_by_name/1) defp read_group_by_name(name) when is_binary(name) do - name = String.replace_prefix(name, "group:", "") + name_without_prefix = String.replace_prefix(name, "group:", "") - case Redix.command!(["HGET", Keys.user_group_name_to_id(), name]) do + case Redix.command!(["HGET", Keys.user_group_name_to_id(), name_without_prefix]) do nil -> nil id -> read_group(id) end @@ -438,10 +438,10 @@ defmodule TdCache.UserCache do end) end - defp do_put_group(%{id: id, name: name, alias: alias} = group) do + defp do_put_group(%{id: id, name: name, alias: group_alias} = group) do [ ["DEL", Keys.user_group(id)], - ["HSET", Keys.user_group(id), %{name: name, alias: alias}], + ["HSET", Keys.user_group(id), %{name: name, alias: group_alias}], ["SADD", Keys.group_ids(), id] ] |> add_group_name(group) @@ -453,8 +453,9 @@ defmodule TdCache.UserCache do pipeline ++ [["HSET", Keys.user_group_name_to_id(), name, id]] end - defp add_group_alias(pipeline, %{id: id, alias: alias}) when alias not in [nil, ""] do - pipeline ++ [["HSET", Keys.user_group_name_to_id(), alias, id]] + defp add_group_alias(pipeline, %{id: id, alias: group_alias}) + when group_alias not in [nil, ""] do + pipeline ++ [["HSET", Keys.user_group_name_to_id(), group_alias, id]] end defp add_group_alias(pipeline, _), do: pipeline @@ -468,14 +469,14 @@ defmodule TdCache.UserCache do ["SREM", Keys.group_ids(), id] ]) - [alias, name] -> + [group_alias, name] -> [ ["DEL", Keys.user_group(id)], ["DEL", Keys.user_group_roles(id)], ["SREM", Keys.group_ids(), id] ] |> delete_group_name(name) - |> delete_group_name(alias) + |> delete_group_name(group_alias) |> Redix.transaction_pipeline() end end diff --git a/test/td_cache/user_cache_test.exs b/test/td_cache/user_cache_test.exs index 48cb8c1..0e77626 100644 --- a/test/td_cache/user_cache_test.exs +++ b/test/td_cache/user_cache_test.exs @@ -314,6 +314,56 @@ defmodule TdCache.UserCacheTest do nil ] end + + test "delete_group removes group lookups by name and alias" do + group = build(:group) + put_user_group(group) + + assert {:ok, %{id: id}} = UserCache.get_group_by_name(group.name) + assert id == group.id + assert {:ok, %{id: id}} = UserCache.get_group_by_name(group.alias) + assert id == group.id + + assert {:ok, _} = UserCache.delete_group(group.id) + + assert {:ok, nil} = UserCache.get_group(group.id) + assert {:ok, nil} = UserCache.get_group_by_name(group.name) + assert {:ok, nil} = UserCache.get_group_by_name(group.alias) + end + + test "delete_group removes group name lookup when alias is nil" do + group = :group |> build() |> Map.put(:alias, nil) + + put_user_group(group) + + assert {:ok, %{id: id}} = UserCache.get_group_by_name(group.name) + assert id == group.id + + assert {:ok, _} = UserCache.delete_group(group.id) + + assert {:ok, nil} = UserCache.get_group(group.id) + assert {:ok, nil} = UserCache.get_group_by_name(group.name) + end + + test "delete_group does not delete user cache with same id" do + group = build(:group) + + user = :user |> build() |> Map.put(:id, group.id) + + put_user_group(group) + put_user(user) + + assert {:ok, %{id: id}} = UserCache.get_group(group.id) + assert id == group.id + assert {:ok, %{id: id}} = UserCache.get(user.id) + assert id == user.id + + assert {:ok, _} = UserCache.delete_group(group.id) + + assert {:ok, nil} = UserCache.get_group(group.id) + assert {:ok, %{id: id}} = UserCache.get(user.id) + assert id == user.id + end end defp put_user(%{id: id} = user) do From 999785f2fa91197e9863f026c944073e40186b7a Mon Sep 17 00:00:00 2001 From: Guillermo Rodriguez Date: Wed, 6 May 2026 13:15:50 +0200 Subject: [PATCH 05/10] test: TD-8083 Add tests to ignore users and groups not present in cache --- test/td_cache/templates/acl_loader_test.exs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/test/td_cache/templates/acl_loader_test.exs b/test/td_cache/templates/acl_loader_test.exs index 968fcb4..50c7e19 100644 --- a/test/td_cache/templates/acl_loader_test.exs +++ b/test/td_cache/templates/acl_loader_test.exs @@ -45,6 +45,16 @@ defmodule TdCache.Templates.AclLoaderTest do %{id: user_id_2, full_name: user_name_2} ] end + + test "ignores users not present in cache" do + domain = CacheHelpers.insert_domain() + missing_user_id = System.unique_integer([:positive]) + + AclCache.set_acl_roles("domain", domain.id, ["role1"]) + AclCache.set_acl_role_users("domain", domain.id, "role1", [missing_user_id]) + + assert %{"role1" => []} = AclLoader.get_roles_and_users([domain.id]) + end end describe "get_roles_and_groups/1" do @@ -79,5 +89,15 @@ defmodule TdCache.Templates.AclLoaderTest do assert Enum.sort([id1, id2]) == Enum.sort([group_id_1, group_id_2]) end + + test "ignores groups not present in cache" do + domain = CacheHelpers.insert_domain() + missing_group_id = System.unique_integer([:positive]) + + AclCache.set_acl_group_roles("domain", domain.id, ["role1"]) + AclCache.set_acl_role_groups("domain", domain.id, "role1", [missing_group_id]) + + assert %{"role1" => []} = AclLoader.get_roles_and_groups([domain.id]) + end end end From 46ec95e3be266d86d832dab935113610e0837b09 Mon Sep 17 00:00:00 2001 From: Guillermo Rodriguez Date: Wed, 6 May 2026 15:46:20 +0200 Subject: [PATCH 06/10] feat: TD-8083 Update group management to handle name and alias changes --- lib/td_cache/user_cache.ex | 11 +++++++++++ test/td_cache/user_cache_test.exs | 33 +++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/lib/td_cache/user_cache.ex b/lib/td_cache/user_cache.ex index ac9e1ea..a116d63 100644 --- a/lib/td_cache/user_cache.ex +++ b/lib/td_cache/user_cache.ex @@ -439,11 +439,15 @@ defmodule TdCache.UserCache do end defp do_put_group(%{id: id, name: name, alias: group_alias} = group) do + [old_name, old_alias] = Redix.command!(["HMGET", Keys.user_group(id), "name", "alias"]) + [ ["DEL", Keys.user_group(id)], ["HSET", Keys.user_group(id), %{name: name, alias: group_alias}], ["SADD", Keys.group_ids(), id] ] + |> remove_group_name_if_changed(old_name, name) + |> remove_group_name_if_changed(old_alias, group_alias) |> add_group_name(group) |> add_group_alias(group) |> Redix.transaction_pipeline() @@ -460,6 +464,13 @@ defmodule TdCache.UserCache do defp add_group_alias(pipeline, _), do: pipeline + defp remove_group_name_if_changed(pipeline, old_value, new_value) + when old_value not in [nil, ""] and old_value != new_value do + pipeline ++ [["HDEL", Keys.user_group_name_to_id(), old_value]] + end + + defp remove_group_name_if_changed(pipeline, _old_value, _new_value), do: pipeline + defp do_delete_group(id) do case Redix.command!(["HMGET", Keys.user_group(id), "alias", "name"]) do [nil, nil] -> diff --git a/test/td_cache/user_cache_test.exs b/test/td_cache/user_cache_test.exs index 0e77626..09c6b30 100644 --- a/test/td_cache/user_cache_test.exs +++ b/test/td_cache/user_cache_test.exs @@ -315,6 +315,39 @@ defmodule TdCache.UserCacheTest do ] end + test "put_group updates name and alias indexes when values change" do + group = build(:group) + put_user_group(group) + + updated_group = %{ + group + | name: "#{group.name}_updated", + alias: "#{group.alias}_updated" + } + + assert {:ok, _} = UserCache.put_group(updated_group) + + assert {:ok, nil} = UserCache.get_group_by_name(group.name) + assert {:ok, nil} = UserCache.get_group_by_name(group.alias) + + assert {:ok, %{id: id}} = UserCache.get_group_by_name(updated_group.name) + assert id == group.id + assert {:ok, %{id: id}} = UserCache.get_group_by_name(updated_group.alias) + assert id == group.id + end + + test "put_group removes previous alias index when alias becomes nil" do + group = build(:group) + put_user_group(group) + + updated_group = %{group | alias: nil} + assert {:ok, _} = UserCache.put_group(updated_group) + + assert {:ok, nil} = UserCache.get_group_by_name(group.alias) + assert {:ok, %{id: id}} = UserCache.get_group_by_name(group.name) + assert id == group.id + end + test "delete_group removes group lookups by name and alias" do group = build(:group) put_user_group(group) From 377feaf695b42f028f5d27c221ffffa60e2636e3 Mon Sep 17 00:00:00 2001 From: Guillermo Rodriguez Date: Wed, 6 May 2026 16:38:23 +0200 Subject: [PATCH 07/10] feat: TD-8083 Add cache clearing functions for users and groups --- lib/td_cache/user_cache.ex | 69 +++++++++++++++++++++++++++++++ test/td_cache/user_cache_test.exs | 60 +++++++++++++++++++++++++++ 2 files changed, 129 insertions(+) diff --git a/lib/td_cache/user_cache.ex b/lib/td_cache/user_cache.ex index a116d63..6228a8b 100644 --- a/lib/td_cache/user_cache.ex +++ b/lib/td_cache/user_cache.ex @@ -148,6 +148,10 @@ defmodule TdCache.UserCache do GenServer.call(__MODULE__, {:delete, id}) end + def clear_users_cache do + GenServer.call(__MODULE__, :clear_users_cache) + end + def put_group(group) do GenServer.call(__MODULE__, {:put_group, group}) end @@ -156,6 +160,10 @@ defmodule TdCache.UserCache do GenServer.call(__MODULE__, {:delete_group, id}) end + def clear_groups_cache do + GenServer.call(__MODULE__, :clear_groups_cache) + end + def ids_key, do: Keys.ids() def group_ids_key, do: Keys.group_ids() @@ -234,6 +242,11 @@ defmodule TdCache.UserCache do {:reply, reply, state} end + def handle_call(:clear_users_cache, _from, state) do + reply = do_clear_users_cache() + {:reply, reply, state} + end + def handle_call({:put_group, group}, _from, state) do reply = do_put_group(group) {:reply, reply, state} @@ -244,6 +257,11 @@ defmodule TdCache.UserCache do {:reply, reply, state} end + def handle_call(:clear_groups_cache, _from, state) do + reply = do_clear_groups_cache() + {:reply, reply, state} + end + ## Private functions defp get_cache(key, fun) do @@ -497,4 +515,55 @@ defmodule TdCache.UserCache do end defp delete_group_name(pipeline, _), do: pipeline + + defp do_clear_users_cache do + cached_ids = Redix.command!(["SMEMBERS", Keys.ids()]) + + user_keys = + ["KEYS", "user:*"] + |> Redix.command!() + |> Enum.reject(&String.contains?(&1, ":roles")) + + user_roles_keys = Redix.command!(["KEYS", "user:*:roles*"]) + + cmds = + [ + ["DEL", Keys.ids()], + ["DEL", Keys.name_to_id()], + ["DEL", Keys.user_name_to_id()], + ["DEL", Keys.external_id_to_id()] + ] ++ + Enum.map(user_keys ++ user_roles_keys, &["DEL", &1]) + + reply = Redix.transaction_pipeline(cmds) + + Enum.each(cached_ids, fn id -> + ConCache.delete(:users, id) + + case Integer.parse(id) do + {int_id, ""} -> ConCache.delete(:users, int_id) + _ -> :ok + end + end) + + reply + end + + defp do_clear_groups_cache do + group_keys = + ["KEYS", "user_group:*"] + |> Redix.command!() + |> Enum.reject(&String.ends_with?(&1, ":roles")) + + group_roles_keys = Redix.command!(["KEYS", "user_group:*:roles"]) + + cmds = + [ + ["DEL", Keys.group_ids()], + ["DEL", Keys.user_group_name_to_id()] + ] ++ + Enum.map(group_keys ++ group_roles_keys, &["DEL", &1]) + + Redix.transaction_pipeline(cmds) + end end diff --git a/test/td_cache/user_cache_test.exs b/test/td_cache/user_cache_test.exs index 09c6b30..da07d13 100644 --- a/test/td_cache/user_cache_test.exs +++ b/test/td_cache/user_cache_test.exs @@ -119,6 +119,36 @@ defmodule TdCache.UserCacheTest do assert UserCache.id_to_email_map() == %{} end + + test "clear_users_cache removes all user cache keys and keeps group keys" do + user = build(:user) + other_user = build(:user) + group = build(:group) + + put_user(user) + put_user(other_user) + put_user_group(group) + + assert {:ok, %{id: id}} = UserCache.get(user.id) + assert id == user.id + assert {:ok, %{id: id}} = UserCache.get(other_user.id) + assert id == other_user.id + assert {:ok, %{id: id}} = UserCache.get_by_name(user.full_name) + assert id == user.id + assert {:ok, %{id: id}} = UserCache.get_by_user_name(other_user.user_name) + assert id == other_user.id + assert {:ok, %{id: id}} = UserCache.get_group(group.id) + assert id == group.id + + assert {:ok, _} = UserCache.clear_users_cache() + + assert {:ok, nil} = UserCache.get(user.id) + assert {:ok, nil} = UserCache.get(other_user.id) + assert {:ok, nil} = UserCache.get_by_name(user.full_name) + assert {:ok, nil} = UserCache.get_by_user_name(other_user.user_name) + assert {:ok, %{id: id}} = UserCache.get_group(group.id) + assert id == group.id + end end describe "refresh_all_roles/1 refresh_resource_roles/3 and get_roles/1" do @@ -397,6 +427,36 @@ defmodule TdCache.UserCacheTest do assert {:ok, %{id: id}} = UserCache.get(user.id) assert id == user.id end + + test "clear_groups_cache removes all group cache keys and keeps user keys" do + group = build(:group) + other_group = build(:group) + user = build(:user) + + put_user_group(group) + put_user_group(other_group) + put_user(user) + + assert {:ok, %{id: id}} = UserCache.get_group(group.id) + assert id == group.id + assert {:ok, %{id: id}} = UserCache.get_group(other_group.id) + assert id == other_group.id + assert {:ok, %{id: id}} = UserCache.get_group_by_name(group.name) + assert id == group.id + assert {:ok, %{id: id}} = UserCache.get_group_by_name(other_group.alias) + assert id == other_group.id + assert {:ok, %{id: id}} = UserCache.get(user.id) + assert id == user.id + + assert {:ok, _} = UserCache.clear_groups_cache() + + assert {:ok, nil} = UserCache.get_group(group.id) + assert {:ok, nil} = UserCache.get_group(other_group.id) + assert {:ok, nil} = UserCache.get_group_by_name(group.name) + assert {:ok, nil} = UserCache.get_group_by_name(other_group.alias) + assert {:ok, %{id: id}} = UserCache.get(user.id) + assert id == user.id + end end defp put_user(%{id: id} = user) do From 4d3115b0d932c6874f209b077940119338d2beb0 Mon Sep 17 00:00:00 2001 From: Guillermo Rodriguez Date: Thu, 7 May 2026 16:35:56 +0200 Subject: [PATCH 08/10] refactor: TD-8083 Simplify group name and alias handling in field formatter --- lib/td_cache/templates/field_formatter.ex | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/td_cache/templates/field_formatter.ex b/lib/td_cache/templates/field_formatter.ex index 524a4b1..d2a8b52 100644 --- a/lib/td_cache/templates/field_formatter.ex +++ b/lib/td_cache/templates/field_formatter.ex @@ -89,6 +89,8 @@ defmodule TdCache.Templates.FieldFormatter do defp apply_user_group_meta(field, _role, _user_roles), do: field - defp group_name_or_alias(%{alias: alias, name: name}) when alias in [nil, ""], do: name - defp group_name_or_alias(%{alias: alias}), do: alias + defp group_name_or_alias(%{alias: group_alias, name: name}) when group_alias in [nil, ""], + do: name + + defp group_name_or_alias(%{alias: group_alias}), do: group_alias end From e41b465ff3bb83140e106c00ed25e278ad21ff8b Mon Sep 17 00:00:00 2001 From: Guillermo Rodriguez Date: Thu, 7 May 2026 17:47:52 +0200 Subject: [PATCH 09/10] refactor: TD-8083 Improve group ID parsing in user cache --- lib/td_cache/user_cache.ex | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/td_cache/user_cache.ex b/lib/td_cache/user_cache.ex index 6228a8b..9c24142 100644 --- a/lib/td_cache/user_cache.ex +++ b/lib/td_cache/user_cache.ex @@ -320,9 +320,10 @@ defmodule TdCache.UserCache do end defp read_group(id) when is_binary(id) do - id - |> String.to_integer() - |> read_group() + case Integer.parse(id) do + {parsed_id, ""} -> read_group(parsed_id) + _ -> nil + end end defp read_group(id) do From 48b16f9a22a405e7910d1de84eae3bba149b2289 Mon Sep 17 00:00:00 2001 From: Guillermo Rodriguez Date: Tue, 12 May 2026 13:24:41 +0200 Subject: [PATCH 10/10] chore: TD-8083 bump version 8.6.0 --- CHANGELOG.md | 2 +- mix.exs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb44a02..eea2bd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [8.6.0] 2026-05-12 ### Added diff --git a/mix.exs b/mix.exs index 799057b..d92da33 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule TdCache.MixProject do def project do [ app: :td_cache, - version: "8.3.0", + version: "8.6.0", elixir: "~> 1.18", elixirc_paths: elixirc_paths(Mix.env()), start_permanent: Mix.env() == :prod,