Skip to content

Commit 9494329

Browse files
committed
feat: deactivate and reactivate service account
1 parent f6e9868 commit 9494329

File tree

7 files changed

+551
-52
lines changed

7 files changed

+551
-52
lines changed

guard/lib/guard/grpc_servers/service_account_server.ex

Lines changed: 69 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -185,28 +185,88 @@ defmodule Guard.GrpcServers.ServiceAccountServer do
185185
)
186186
end
187187

188-
@spec delete(ServiceAccountPB.DeleteRequest.t(), GRPC.Server.Stream.t()) ::
189-
ServiceAccountPB.DeleteResponse.t()
190-
def delete(%ServiceAccountPB.DeleteRequest{service_account_id: service_account_id}, _stream) do
188+
@spec deactivate(ServiceAccountPB.DeactivateRequest.t(), GRPC.Server.Stream.t()) ::
189+
ServiceAccountPB.DeactivateResponse.t()
190+
def deactivate(
191+
%ServiceAccountPB.DeactivateRequest{service_account_id: service_account_id},
192+
_stream
193+
) do
194+
observe_and_log(
195+
"grpc.service_account.deactivate",
196+
%{service_account_id: service_account_id},
197+
fn ->
198+
validate_uuid!(service_account_id)
199+
200+
case Guard.ServiceAccount.Actions.deactivate(service_account_id) do
201+
{:ok, :deactivated} ->
202+
ServiceAccountPB.DeactivateResponse.new()
203+
204+
{:error, :not_found} ->
205+
grpc_error!(:not_found, "Service account #{service_account_id} not found")
206+
207+
{:error, reason} ->
208+
Logger.error(
209+
"Failed to deactivate service account #{service_account_id}: #{inspect(reason)}"
210+
)
211+
212+
grpc_error!(:internal, "Failed to deactivate service account")
213+
end
214+
end
215+
)
216+
end
217+
218+
@spec reactivate(ServiceAccountPB.ReactivateRequest.t(), GRPC.Server.Stream.t()) ::
219+
ServiceAccountPB.ReactivateResponse.t()
220+
def reactivate(
221+
%ServiceAccountPB.ReactivateRequest{service_account_id: service_account_id},
222+
_stream
223+
) do
224+
observe_and_log(
225+
"grpc.service_account.reactivate",
226+
%{service_account_id: service_account_id},
227+
fn ->
228+
validate_uuid!(service_account_id)
229+
230+
case Guard.ServiceAccount.Actions.reactivate(service_account_id) do
231+
{:ok, :reactivated} ->
232+
ServiceAccountPB.ReactivateResponse.new()
233+
234+
{:error, :not_found} ->
235+
grpc_error!(:not_found, "Service account #{service_account_id} not found")
236+
237+
{:error, reason} ->
238+
Logger.error(
239+
"Failed to reactivate service account #{service_account_id}: #{inspect(reason)}"
240+
)
241+
242+
grpc_error!(:internal, "Failed to reactivate service account")
243+
end
244+
end
245+
)
246+
end
247+
248+
@spec destroy(ServiceAccountPB.DestroyRequest.t(), GRPC.Server.Stream.t()) ::
249+
ServiceAccountPB.DestroyResponse.t()
250+
def destroy(%ServiceAccountPB.DestroyRequest{service_account_id: service_account_id}, _stream) do
191251
observe_and_log(
192-
"grpc.service_account.delete",
252+
"grpc.service_account.destroy",
193253
%{service_account_id: service_account_id},
194254
fn ->
195255
validate_uuid!(service_account_id)
196256

197-
case Guard.ServiceAccount.Actions.delete(service_account_id) do
198-
{:ok, :deleted} ->
199-
ServiceAccountPB.DeleteResponse.new()
257+
case Guard.ServiceAccount.Actions.destroy(service_account_id) do
258+
{:ok, :destroyed} ->
259+
ServiceAccountPB.DestroyResponse.new()
200260

201261
{:error, :not_found} ->
202262
grpc_error!(:not_found, "Service account #{service_account_id} not found")
203263

204264
{:error, reason} ->
205265
Logger.error(
206-
"Failed to delete service account #{service_account_id}: #{inspect(reason)}"
266+
"Failed to destroy service account #{service_account_id}: #{inspect(reason)}"
207267
)
208268

209-
grpc_error!(:internal, "Failed to delete service account")
269+
grpc_error!(:internal, "Failed to destroy service account")
210270
end
211271
end
212272
)

guard/lib/guard/service_account/actions.ex

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -74,16 +74,50 @@ defmodule Guard.ServiceAccount.Actions do
7474
end
7575

7676
@doc """
77-
Soft delete a service account by deactivating the associated user.
77+
Deactivate a service account by setting the user's deactivated flag to true.
7878
7979
This marks the user as deactivated rather than physically deleting records,
8080
allowing for audit trails and potential recovery.
8181
"""
82-
@spec delete(String.t()) :: {:ok, :deleted} | {:error, atom()}
83-
def delete(service_account_id) do
84-
case ServiceAccount.delete(service_account_id) do
85-
{:ok, :deleted} ->
86-
{:ok, :deleted}
82+
@spec deactivate(String.t()) :: {:ok, :deactivated} | {:error, atom()}
83+
def deactivate(service_account_id) do
84+
case ServiceAccount.deactivate(service_account_id) do
85+
{:ok, :deactivated} ->
86+
{:ok, :deactivated}
87+
88+
{:error, error} ->
89+
{:error, error}
90+
end
91+
end
92+
93+
@doc """
94+
Reactivate a previously deactivated service account.
95+
96+
This sets the user's deactivated flag to false, allowing the service account
97+
to be used again.
98+
"""
99+
@spec reactivate(String.t()) :: {:ok, :reactivated} | {:error, atom()}
100+
def reactivate(service_account_id) do
101+
case ServiceAccount.reactivate(service_account_id) do
102+
{:ok, :reactivated} ->
103+
{:ok, :reactivated}
104+
105+
{:error, error} ->
106+
{:error, error}
107+
end
108+
end
109+
110+
@doc """
111+
Permanently destroy a service account and all associated records.
112+
113+
This physically deletes the service account and user records from the database.
114+
This action cannot be undone and should be used with caution.
115+
"""
116+
@spec destroy(String.t()) :: {:ok, :destroyed} | {:error, atom()}
117+
def destroy(service_account_id) do
118+
case ServiceAccount.destroy(service_account_id) do
119+
{:ok, :destroyed} ->
120+
{:ok, :destroyed}
87121

88122
{:error, error} ->
89123
{:error, error}

guard/lib/guard/store/service_account.ex

Lines changed: 132 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -140,17 +140,17 @@ defmodule Guard.Store.ServiceAccount do
140140
end
141141

142142
@doc """
143-
Delete (deactivate) a service account.
143+
Deactivate a service account.
144144
145145
Performs a soft delete by setting the user's deactivated flag to true.
146146
"""
147-
@spec delete(String.t()) :: {:ok, :deleted} | {:error, :not_found | :internal_error}
148-
def delete(service_account_id) when is_binary(service_account_id) do
147+
@spec deactivate(String.t()) :: {:ok, :deactivated} | {:error, :not_found | :internal_error}
148+
def deactivate(service_account_id) when is_binary(service_account_id) do
149149
if valid_uuid?(service_account_id) do
150150
case FrontRepo.transaction(fn ->
151151
with {:ok, _current_data} <- find(service_account_id),
152152
{:ok, _updated_user} <- deactivate_user_record(service_account_id) do
153-
:deleted
153+
:deactivated
154154
else
155155
{:error, :not_found} ->
156156
FrontRepo.rollback(:not_found)
@@ -159,7 +159,7 @@ defmodule Guard.Store.ServiceAccount do
159159
FrontRepo.rollback(:internal_error)
160160
end
161161
end) do
162-
{:ok, :deleted} -> {:ok, :deleted}
162+
{:ok, :deactivated} -> {:ok, :deactivated}
163163
{:error, reason} -> {:error, reason}
164164
end
165165
else
@@ -168,7 +168,92 @@ defmodule Guard.Store.ServiceAccount do
168168
rescue
169169
e ->
170170
Logger.error(
171-
"Error during service account deletion #{inspect(service_account_id)}: #{inspect(e)}"
171+
"Error during service account deactivation #{inspect(service_account_id)}: #{inspect(e)}"
172+
)
173+
174+
{:error, :internal_error}
175+
end
176+
177+
@doc """
178+
Reactivate a service account.
179+
180+
Reactivates a previously deactivated service account by setting the user's deactivated flag to false.
181+
"""
182+
@spec reactivate(String.t()) :: {:ok, :reactivated} | {:error, :not_found | :internal_error}
183+
def reactivate(service_account_id) when is_binary(service_account_id) do
184+
if valid_uuid?(service_account_id) do
185+
case FrontRepo.transaction(fn ->
186+
# Use a modified query that includes deactivated service accounts
187+
query =
188+
build_service_account_query()
189+
|> where([sa, u], sa.id == ^service_account_id)
190+
|> where([sa, u], is_nil(u.blocked_at))
191+
192+
case FrontRepo.one(query) do
193+
nil ->
194+
FrontRepo.rollback(:not_found)
195+
196+
_service_account ->
197+
case reactivate_user_record(service_account_id) do
198+
{:ok, _updated_user} -> :reactivated
199+
{:error, _reason} -> FrontRepo.rollback(:internal_error)
200+
end
201+
end
202+
end) do
203+
{:ok, :reactivated} -> {:ok, :reactivated}
204+
{:error, reason} -> {:error, reason}
205+
end
206+
else
207+
{:error, :invalid_id}
208+
end
209+
rescue
210+
e ->
211+
Logger.error(
212+
"Error during service account reactivation #{inspect(service_account_id)}: #{inspect(e)}"
213+
)
214+
215+
{:error, :internal_error}
216+
end
217+
218+
@doc """
219+
Destroy a service account.
220+
221+
Permanently deletes the service account and associated user records from the database.
222+
This action cannot be undone.
223+
"""
224+
@spec destroy(String.t()) :: {:ok, :destroyed} | {:error, :not_found | :internal_error}
225+
def destroy(service_account_id) when is_binary(service_account_id) do
226+
if valid_uuid?(service_account_id) do
227+
case FrontRepo.transaction(fn ->
228+
# Use a modified query that includes deactivated service accounts for destruction
229+
query =
230+
build_service_account_query()
231+
|> where([sa, u], sa.id == ^service_account_id)
232+
|> where([sa, u], is_nil(u.blocked_at))
233+
234+
case FrontRepo.one(query) do
235+
nil ->
236+
FrontRepo.rollback(:not_found)
237+
238+
_service_account ->
239+
with {:ok, _} <- destroy_service_account_record(service_account_id),
240+
{:ok, _} <- destroy_user_record(service_account_id) do
241+
:destroyed
242+
else
243+
{:error, _reason} -> FrontRepo.rollback(:internal_error)
244+
end
245+
end
246+
end) do
247+
{:ok, :destroyed} -> {:ok, :destroyed}
248+
{:error, reason} -> {:error, reason}
249+
end
250+
else
251+
{:error, :invalid_id}
252+
end
253+
rescue
254+
e ->
255+
Logger.error(
256+
"Error during service account destruction #{inspect(service_account_id)}: #{inspect(e)}"
172257
)
173258

174259
{:error, :internal_error}
@@ -340,6 +425,47 @@ defmodule Guard.Store.ServiceAccount do
340425
end
341426
end
342427

428+
defp reactivate_user_record(user_id) do
429+
user = FrontRepo.get!(User, user_id)
430+
431+
changeset =
432+
User.changeset(user, %{
433+
deactivated: false,
434+
deactivated_at: nil
435+
})
436+
437+
case FrontRepo.update(changeset) do
438+
{:ok, user} -> {:ok, user}
439+
{:error, changeset} -> {:error, changeset.errors}
440+
end
441+
end
442+
443+
defp destroy_service_account_record(service_account_id) do
444+
case FrontRepo.get(ServiceAccount, service_account_id) do
445+
nil ->
446+
{:error, :not_found}
447+
448+
service_account ->
449+
case FrontRepo.delete(service_account) do
450+
{:ok, _} -> {:ok, :deleted}
451+
{:error, changeset} -> {:error, changeset.errors}
452+
end
453+
end
454+
end
455+
456+
defp destroy_user_record(user_id) do
457+
case FrontRepo.get(User, user_id) do
458+
nil ->
459+
{:error, :not_found}
460+
461+
user ->
462+
case FrontRepo.delete(user) do
463+
{:ok, _} -> {:ok, :deleted}
464+
{:error, changeset} -> {:error, changeset.errors}
465+
end
466+
end
467+
end
468+
343469
defp update_user_token(user_id, hashed_token) do
344470
user = FrontRepo.get!(User, user_id)
345471

guard/lib/internal_api/service_account.pb.ex

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ defmodule InternalApi.ServiceAccount.DeactivateResponse do
135135
defstruct []
136136
end
137137

138-
defmodule InternalApi.ServiceAccount.DeleteRequest do
138+
defmodule InternalApi.ServiceAccount.ReactivateRequest do
139139
@moduledoc false
140140
use Protobuf, syntax: :proto3
141141

@@ -147,7 +147,26 @@ defmodule InternalApi.ServiceAccount.DeleteRequest do
147147
field(:service_account_id, 1, type: :string)
148148
end
149149

150-
defmodule InternalApi.ServiceAccount.DeleteResponse do
150+
defmodule InternalApi.ServiceAccount.ReactivateResponse do
151+
@moduledoc false
152+
use Protobuf, syntax: :proto3
153+
154+
defstruct []
155+
end
156+
157+
defmodule InternalApi.ServiceAccount.DestroyRequest do
158+
@moduledoc false
159+
use Protobuf, syntax: :proto3
160+
161+
@type t :: %__MODULE__{
162+
service_account_id: String.t()
163+
}
164+
165+
defstruct [:service_account_id]
166+
field(:service_account_id, 1, type: :string)
167+
end
168+
169+
defmodule InternalApi.ServiceAccount.DestroyResponse do
151170
@moduledoc false
152171
use Protobuf, syntax: :proto3
153172

@@ -245,9 +264,15 @@ defmodule InternalApi.ServiceAccount.ServiceAccountService.Service do
245264
)
246265

247266
rpc(
248-
:Delete,
249-
InternalApi.ServiceAccount.DeleteRequest,
250-
InternalApi.ServiceAccount.DeleteResponse
267+
:Reactivate,
268+
InternalApi.ServiceAccount.ReactivateRequest,
269+
InternalApi.ServiceAccount.ReactivateResponse
270+
)
271+
272+
rpc(
273+
:Destroy,
274+
InternalApi.ServiceAccount.DestroyRequest,
275+
InternalApi.ServiceAccount.DestroyResponse
251276
)
252277

253278
rpc(

0 commit comments

Comments
 (0)