1defmodule Provision do
2 alias Domain.{Repo, Accounts, Auth, Actors, Resources, Tokens, Gateways, Relays, Policies}
3 require Logger
4
5 # UUID Mapping handling
6 defmodule UuidMapping do
7 @mapping_file "provision-uuids.json"
8
9 # Loads the mapping from file
10 def load do
11 mappings = case File.read(@mapping_file) do
12 {:ok, content} ->
13 case Jason.decode(content) do
14 {:ok, mapping} -> mapping
15 _ -> %{"accounts" => %{}}
16 end
17
18 _ -> %{"accounts" => %{}}
19 end
20 Process.put(:uuid_mappings, mappings)
21 mappings
22 end
23
24 # Saves the current mapping (defaulting to the one in the process dictionary)
25 def save(mapping \\ Process.get(:uuid_mappings)) do
26 File.write!(@mapping_file, Jason.encode!(mapping))
27 end
28
29 # Retrieves the account-level mapping from a given mapping (or from Process)
30 def get_account(mapping \\ Process.get(:uuid_mappings), account_slug) do
31 get_in(mapping, ["accounts", account_slug]) || %{}
32 end
33
34 # Retrieves the entity mapping for a specific account and type
35 def get_entities(mapping \\ Process.get(:uuid_mappings), account_slug, type) do
36 get_in(mapping, ["accounts", account_slug, type]) || %{}
37 end
38
39 # Retrieves an entity mapping for a specific account, type and external_id
40 def get_entity(mapping \\ Process.get(:uuid_mappings), account_slug, type, external_id) do
41 get_in(mapping, ["accounts", account_slug, type, external_id])
42 end
43
44 # Updates (or creates) the account UUID mapping and stores it in the process dictionary.
45 def update_account(account_slug, uuid) do
46 mapping = Process.get(:uuid_mappings) || load()
47 mapping = ensure_account_exists(mapping, account_slug)
48 mapping = put_in(mapping, ["accounts", account_slug, "id"], uuid)
49 Process.put(:uuid_mappings, mapping)
50 mapping
51 end
52
53 # Ensures that the given account exists in the mapping.
54 def ensure_account_exists(mapping, account_slug) do
55 if not Map.has_key?(mapping["accounts"], account_slug) do
56 put_in(mapping, ["accounts", account_slug], %{})
57 else
58 mapping
59 end
60 end
61
62 # Updates (or creates) the mapping for entities of a given type for the account.
63 def update_entities(account_slug, type, new_entries) do
64 mapping = Process.get(:uuid_mappings) || load()
65 mapping = ensure_account_exists(mapping, account_slug)
66 current = get_entities(mapping, account_slug, type)
67 mapping = put_in(mapping, ["accounts", account_slug, type], Map.merge(current, new_entries))
68 Process.put(:uuid_mappings, mapping)
69 mapping
70 end
71
72 # Removes an entire account from the mapping.
73 def remove_account(account_slug) do
74 mapping = Process.get(:uuid_mappings) || load()
75 mapping = update_in(mapping, ["accounts"], fn accounts ->
76 Map.delete(accounts, account_slug)
77 end)
78 Process.put(:uuid_mappings, mapping)
79 mapping
80 end
81
82 # Removes a specific entity mapping for the account.
83 def remove_entity(account_slug, type, key) do
84 mapping = Process.get(:uuid_mappings) || load()
85 mapping = update_in(mapping, ["accounts", account_slug, type], fn entities ->
86 Map.delete(entities || %{}, key)
87 end)
88 Process.put(:uuid_mappings, mapping)
89 mapping
90 end
91 end
92
93 defp resolve_references(value) when is_map(value) do
94 Enum.into(value, %{}, fn {k, v} -> {k, resolve_references(v)} end)
95 end
96
97 defp resolve_references(value) when is_list(value) do
98 Enum.map(value, &resolve_references/1)
99 end
100
101 defp resolve_references(value) when is_binary(value) do
102 Regex.replace(~r/\{env:([^}]+)\}/, value, fn _, var ->
103 System.get_env(var) || raise "Environment variable #{var} not set"
104 end)
105 end
106
107 defp resolve_references(value), do: value
108
109 defp atomize_keys(map) when is_map(map) do
110 Enum.into(map, %{}, fn {k, v} ->
111 {
112 if(is_binary(k), do: String.to_atom(k), else: k),
113 if(is_map(v), do: atomize_keys(v), else: v)
114 }
115 end)
116 end
117
118 defp cleanup_account(uuid) do
119 case Accounts.fetch_account_by_id_or_slug(uuid) do
120 {:ok, value} when value.deleted_at == nil ->
121 Logger.info("Deleting removed account #{value.slug}")
122 value |> Ecto.Changeset.change(%{ deleted_at: DateTime.utc_now() }) |> Repo.update!()
123 _ -> :ok
124 end
125 end
126
127 defp cleanup_actor(uuid, subject) do
128 case Actors.fetch_actor_by_id(uuid, subject) do
129 {:ok, value} ->
130 Logger.info("Deleting removed actor #{value.name}")
131 {:ok, _} = Actors.delete_actor(value, subject)
132 _ -> :ok
133 end
134 end
135
136 defp cleanup_provider(uuid, subject) do
137 case Auth.fetch_provider_by_id(uuid, subject) do
138 {:ok, value} ->
139 Logger.info("Deleting removed provider #{value.name}")
140 {:ok, _} = Auth.delete_provider(value, subject)
141 _ -> :ok
142 end
143 end
144
145 defp cleanup_gateway_group(uuid, subject) do
146 case Gateways.fetch_group_by_id(uuid, subject) do
147 {:ok, value} ->
148 Logger.info("Deleting removed gateway group #{value.name}")
149 {:ok, _} = Gateways.delete_group(value, subject)
150 _ -> :ok
151 end
152 end
153
154 defp cleanup_relay_group(uuid, subject) do
155 case Relays.fetch_group_by_id(uuid, subject) do
156 {:ok, value} ->
157 Logger.info("Deleting removed relay group #{value.name}")
158 {:ok, _} = Relays.delete_group(value, subject)
159 _ -> :ok
160 end
161 end
162
163 defp cleanup_actor_group(uuid, subject) do
164 case Actors.fetch_group_by_id(uuid, subject) do
165 {:ok, value} ->
166 Logger.info("Deleting removed actor group #{value.name}")
167 {:ok, _} = Actors.delete_group(value, subject)
168 _ -> :ok
169 end
170 end
171
172 # Fetch resource by uuid, but follow the chain of replacements if any
173 defp fetch_resource(uuid, subject) do
174 case Resources.fetch_resource_by_id(uuid, subject) do
175 {:ok, resource} when resource.replaced_by_resource_id != nil -> fetch_resource(resource.replaced_by_resource_id, subject)
176 v -> v
177 end
178 end
179
180 defp cleanup_resource(uuid, subject) do
181 case fetch_resource(uuid, subject) do
182 {:ok, value} when value.deleted_at == nil ->
183 Logger.info("Deleting removed resource #{value.name}")
184 {:ok, _} = Resources.delete_resource(value, subject)
185 _ -> :ok
186 end
187 end
188
189 # Fetch policy by uuid, but follow the chain of replacements if any
190 defp fetch_policy(uuid, subject) do
191 case Policies.fetch_policy_by_id(uuid, subject) do
192 {:ok, policy} when policy.replaced_by_policy_id != nil -> fetch_policy(policy.replaced_by_policy_id, subject)
193 v -> v
194 end
195 end
196
197 defp cleanup_policy(uuid, subject) do
198 case fetch_policy(uuid, subject) do
199 {:ok, value} when value.deleted_at == nil ->
200 Logger.info("Deleting removed policy #{value.description}")
201 {:ok, _} = Policies.delete_policy(value, subject)
202 _ -> :ok
203 end
204 end
205
206 defp cleanup_entity_type(account_slug, entity_type, cleanup_fn, temp_admin_subject) do
207 # Get mapping for this entity type
208 existing_entities = UuidMapping.get_entities(account_slug, entity_type)
209 # Get current entities from account data
210 current_entities = Process.get(:current_entities)
211 # Determine which ones to remove
212 removed_entity_ids = Map.keys(existing_entities) -- (current_entities[entity_type] || [])
213
214 # Process each entity to remove
215 Enum.each(removed_entity_ids, fn entity_id ->
216 case existing_entities[entity_id] do
217 nil -> :ok
218 uuid ->
219 cleanup_fn.(uuid, temp_admin_subject)
220 UuidMapping.remove_entity(account_slug, entity_type, entity_id)
221 end
222 end)
223 end
224
225 defp collect_current_entities(account_data) do
226 %{
227 "actors" => Map.keys(account_data["actors"] || %{}),
228 "providers" => Map.keys(account_data["auth"] || %{}),
229 "gateway_groups" => Map.keys(account_data["gatewayGroups"] || %{}),
230 "relay_groups" => Map.keys(account_data["relayGroups"] || %{}),
231 "actor_groups" => Map.keys(account_data["groups"] || %{}) ++ ["everyone"],
232 "resources" => Map.keys(account_data["resources"] || %{}),
233 "policies" => Map.keys(account_data["policies"] || %{})
234 }
235 end
236
237 defp nil_if_deleted_or_not_found(value) do
238 case value do
239 nil -> nil
240 {:error, :not_found} -> nil
241 {:ok, value} when value.deleted_at != nil -> nil
242 v -> v
243 end
244 end
245
246 defp create_temp_admin(account, email_provider) do
247 temp_admin_actor_email = "firezone-provision@localhost.local"
248 temp_admin_actor_context = %Auth.Context{
249 type: :browser,
250 user_agent: "Unspecified/0.0",
251 remote_ip: {127, 0, 0, 1},
252 remote_ip_location_region: "N/A",
253 remote_ip_location_city: "N/A",
254 remote_ip_location_lat: 0.0,
255 remote_ip_location_lon: 0.0
256 }
257
258 {:ok, temp_admin_actor} =
259 Actors.create_actor(account, %{
260 type: :account_admin_user,
261 name: "Provisioning"
262 })
263
264 {:ok, temp_admin_actor_email_identity} =
265 Auth.create_identity(temp_admin_actor, email_provider, %{
266 provider_identifier: temp_admin_actor_email,
267 provider_identifier_confirmation: temp_admin_actor_email
268 })
269
270 {:ok, temp_admin_actor_token} =
271 Auth.create_token(temp_admin_actor_email_identity, temp_admin_actor_context, "temporarynonce", DateTime.utc_now() |> DateTime.add(1, :hour))
272
273 {:ok, temp_admin_subject} =
274 Auth.build_subject(temp_admin_actor_token, temp_admin_actor_context)
275
276 {temp_admin_subject, temp_admin_actor, temp_admin_actor_email_identity, temp_admin_actor_token}
277 end
278
279 defp cleanup_temp_admin(temp_admin_actor, temp_admin_actor_email_identity, temp_admin_actor_token, subject) do
280 Logger.info("Cleaning up temporary admin actor")
281 {:ok, _} = Tokens.delete_token(temp_admin_actor_token, subject)
282 {:ok, _} = Auth.delete_identity(temp_admin_actor_email_identity, subject)
283 {:ok, _} = Actors.delete_actor(temp_admin_actor, subject)
284 end
285
286 def provision() do
287 Logger.info("Starting provisioning")
288
289 # Load desired state
290 json_file = "provision-state.json"
291 {:ok, raw_json} = File.read(json_file)
292 {:ok, %{"accounts" => accounts}} = Jason.decode(raw_json)
293 accounts = resolve_references(accounts)
294
295 # Load existing UUID mappings into the process dictionary.
296 UuidMapping.load()
297
298 # Clean up removed accounts first
299 current_account_slugs = Map.keys(accounts)
300 existing_accounts = Map.keys(Process.get(:uuid_mappings)["accounts"])
301 removed_accounts = existing_accounts -- current_account_slugs
302
303 Enum.each(removed_accounts, fn slug ->
304 if uuid = get_in(Process.get(:uuid_mappings), ["accounts", slug, "id"]) do
305 cleanup_account(uuid)
306 # Remove the account from the UUID mapping.
307 UuidMapping.remove_account(slug)
308 end
309 end)
310
311 multi = Enum.reduce(accounts, Ecto.Multi.new(), fn {slug, account_data}, multi ->
312 account_attrs = atomize_keys(%{
313 name: account_data["name"],
314 slug: slug,
315 features: Map.get(account_data, "features", %{}),
316 metadata: Map.get(account_data, "metadata", %{}),
317 limits: Map.get(account_data, "limits", %{})
318 })
319
320 multi = multi
321 |> Ecto.Multi.run({:account, slug}, fn repo, _changes ->
322 case Accounts.fetch_account_by_id_or_slug(slug) do
323 {:ok, acc} ->
324 Logger.info("Updating existing account #{slug}")
325 updated_acc = acc |> Ecto.Changeset.change(account_attrs) |> repo.update!()
326 {:ok, {:existing, updated_acc}}
327 _ ->
328 Logger.info("Creating new account #{slug}")
329 {:ok, account} = Accounts.create_account(account_attrs)
330
331 Logger.info("Creating internet gateway group")
332 {:ok, internet_site} = Gateways.create_internet_group(account)
333
334 Logger.info("Creating internet resource")
335 {:ok, _internet_resource} = Resources.create_internet_resource(account, internet_site)
336
337 # Store mapping of slug to UUID
338 UuidMapping.update_account(slug, account.id)
339 {:ok, {:new, account}}
340 end
341 end)
342 |> Ecto.Multi.run({:everyone_group, slug}, fn _repo, changes ->
343 case Map.get(changes, {:account, slug}) do
344 {:new, account} ->
345 Logger.info("Creating everyone group for new account")
346 {:ok, actor_group} = Actors.create_managed_group(account, %{name: "Everyone", membership_rules: [%{operator: true}]})
347 UuidMapping.update_entities(slug, "actor_groups", %{"everyone" => actor_group.id})
348 {:ok, actor_group}
349 {:existing, _account} ->
350 {:ok, :skipped}
351 end
352 end)
353 |> Ecto.Multi.run({:email_provider, slug}, fn _repo, changes ->
354 case Map.get(changes, {:account, slug}) do
355 {:new, account} ->
356 Logger.info("Creating default email provider for new account")
357 Auth.create_provider(account, %{name: "Email", adapter: :email, adapter_config: %{}})
358 {:existing, account} ->
359 Auth.Provider.Query.not_disabled()
360 |> Auth.Provider.Query.by_adapter(:email)
361 |> Auth.Provider.Query.by_account_id(account.id)
362 |> Repo.fetch(Auth.Provider.Query, [])
363 end
364 end)
365 |> Ecto.Multi.run({:temp_admin, slug}, fn _repo, changes ->
366 {_, account} = changes[{:account, slug}]
367 email_provider = changes[{:email_provider, slug}]
368 {:ok, create_temp_admin(account, email_provider)}
369 end)
370
371 # Clean up removed entities for this account after we have an admin subject
372 multi = multi
373 |> Ecto.Multi.run({:cleanup_entities, slug}, fn _repo, changes ->
374 {temp_admin_subject, _, _, _} = changes[{:temp_admin, slug}]
375
376 # Store current entities in process dictionary for our helper function
377 current_entities = collect_current_entities(account_data)
378 Process.put(:current_entities, current_entities)
379
380 # Define entity types and their cleanup functions
381 entity_types = [
382 {"actors", &cleanup_actor/2},
383 {"providers", &cleanup_provider/2},
384 {"gateway_groups", &cleanup_gateway_group/2},
385 {"relay_groups", &cleanup_relay_group/2},
386 {"actor_groups", &cleanup_actor_group/2},
387 {"resources", &cleanup_resource/2},
388 {"policies", &cleanup_policy/2}
389 ]
390
391 # Clean up each entity type
392 Enum.each(entity_types, fn {entity_type, cleanup_fn} ->
393 cleanup_entity_type(slug, entity_type, cleanup_fn, temp_admin_subject)
394 end)
395
396 {:ok, :cleaned}
397 end)
398
399 # Create or update actors
400 multi = Enum.reduce(account_data["actors"] || %{}, multi, fn {external_id, actor_data}, multi ->
401 actor_attrs = atomize_keys(%{
402 name: actor_data["name"],
403 type: String.to_atom(actor_data["type"])
404 })
405
406 Ecto.Multi.run(multi, {:actor, slug, external_id}, fn _repo, changes ->
407 {_, account} = changes[{:account, slug}]
408 {temp_admin_subject, _, _, _} = changes[{:temp_admin, slug}]
409 uuid = UuidMapping.get_entity(slug, "actors", external_id)
410 case uuid && Actors.fetch_actor_by_id(uuid, temp_admin_subject) |> nil_if_deleted_or_not_found() do
411 nil ->
412 Logger.info("Creating new actor #{actor_data["name"]}")
413 {:ok, actor} = Actors.create_actor(account, actor_attrs)
414 # Update the mapping without manually handling Process.get/put.
415 UuidMapping.update_entities(slug, "actors", %{external_id => actor.id})
416 {:ok, {:new, actor}}
417 {:ok, existing_actor} ->
418 Logger.info("Updating existing actor #{actor_data["name"]}")
419 {:ok, updated_act} = Actors.update_actor(existing_actor, actor_attrs, temp_admin_subject)
420 {:ok, {:existing, updated_act}}
421 end
422 end)
423 |> Ecto.Multi.run({:actor_identity, slug, external_id}, fn repo, changes ->
424 email_provider = changes[{:email_provider, slug}]
425 case Map.get(changes, {:actor, slug, external_id}) do
426 {:new, actor} ->
427 Logger.info("Creating actor email identity")
428 Auth.create_identity(actor, email_provider, %{
429 provider_identifier: actor_data["email"],
430 provider_identifier_confirmation: actor_data["email"]
431 })
432 {:existing, actor} ->
433 Logger.info("Updating actor email identity")
434 {:ok, identity} = Auth.Identity.Query.not_deleted()
435 |> Auth.Identity.Query.by_actor_id(actor.id)
436 |> Auth.Identity.Query.by_provider_id(email_provider.id)
437 |> Repo.fetch(Auth.Identity.Query, [])
438
439 {:ok, identity |> Ecto.Changeset.change(%{
440 provider_identifier: actor_data["email"]
441 }) |> repo.update!()}
442 end
443 end)
444 end)
445
446 # Create or update providers
447 multi = Enum.reduce(account_data["auth"] || %{}, multi, fn {external_id, provider_data}, multi ->
448 Ecto.Multi.run(multi, {:provider, slug, external_id}, fn repo, changes ->
449 provider_attrs = %{
450 name: provider_data["name"],
451 adapter: String.to_atom(provider_data["adapter"]),
452 adapter_config: provider_data["adapter_config"]
453 }
454
455 {_, account} = changes[{:account, slug}]
456 {temp_admin_subject, _, _, _} = changes[{:temp_admin, slug}]
457 uuid = UuidMapping.get_entity(slug, "providers", external_id)
458 case uuid && Auth.fetch_provider_by_id(uuid, temp_admin_subject) |> nil_if_deleted_or_not_found() do
459 nil ->
460 Logger.info("Creating new provider #{provider_data["name"]}")
461 {:ok, provider} = Auth.create_provider(account, provider_attrs)
462 UuidMapping.update_entities(slug, "providers", %{external_id => provider.id})
463 {:ok, provider}
464 {:ok, existing} ->
465 Logger.info("Updating existing provider #{provider_data["name"]}")
466 {:ok, existing |> Ecto.Changeset.change(provider_attrs) |> repo.update!()}
467 end
468 end)
469 end)
470
471 # Create or update gateway_groups
472 multi = Enum.reduce(account_data["gatewayGroups"] || %{}, multi, fn {external_id, gateway_group_data}, multi ->
473 Ecto.Multi.run(multi, {:gateway_group, slug, external_id}, fn _repo, changes ->
474 gateway_group_attrs = %{
475 name: gateway_group_data["name"],
476 tokens: [%{}]
477 }
478
479 {_, account} = changes[{:account, slug}]
480 {temp_admin_subject, _, _, _} = changes[{:temp_admin, slug}]
481 uuid = UuidMapping.get_entity(slug, "gateway_groups", external_id)
482 case uuid && Gateways.fetch_group_by_id(uuid, temp_admin_subject) |> nil_if_deleted_or_not_found() do
483 nil ->
484 Logger.info("Creating new gateway group #{gateway_group_data["name"]}")
485 gateway_group = account
486 |> Gateways.Group.Changeset.create(gateway_group_attrs, temp_admin_subject)
487 |> Repo.insert!()
488 UuidMapping.update_entities(slug, "gateway_groups", %{external_id => gateway_group.id})
489 {:ok, gateway_group}
490 {:ok, existing} ->
491 # Nothing to update
492 {:ok, existing}
493 end
494 end)
495 end)
496
497 # Create or update relay_groups
498 multi = Enum.reduce(account_data["relayGroups"] || %{}, multi, fn {external_id, relay_group_data}, multi ->
499 Ecto.Multi.run(multi, {:relay_group, slug, external_id}, fn _repo, changes ->
500 relay_group_attrs = %{
501 name: relay_group_data["name"]
502 }
503
504 {temp_admin_subject, _, _, _} = changes[{:temp_admin, slug}]
505 uuid = UuidMapping.get_entity(slug, "relay_groups", external_id)
506 existing_relay_group = uuid && Relays.fetch_group_by_id(uuid, temp_admin_subject)
507 case existing_relay_group do
508 v when v in [nil, {:error, :not_found}] ->
509 Logger.info("Creating new relay group #{relay_group_data["name"]}")
510 {:ok, relay_group} = Relays.create_group(relay_group_attrs, temp_admin_subject)
511 UuidMapping.update_entities(slug, "relay_groups", %{external_id => relay_group.id})
512 {:ok, relay_group}
513 {:ok, existing} ->
514 # Nothing to update
515 {:ok, existing}
516 end
517 end)
518 end)
519
520 # Create or update actor_groups
521 multi = Enum.reduce(account_data["groups"] || %{}, multi, fn {external_id, actor_group_data}, multi ->
522 Ecto.Multi.run(multi, {:actor_group, slug, external_id}, fn _repo, changes ->
523 actor_group_attrs = %{
524 name: actor_group_data["name"],
525 type: :static
526 }
527
528 {temp_admin_subject, _, _, _} = changes[{:temp_admin, slug}]
529 uuid = UuidMapping.get_entity(slug, "actor_groups", external_id)
530 case uuid && Actors.fetch_group_by_id(uuid, temp_admin_subject) |> nil_if_deleted_or_not_found() do
531 nil ->
532 Logger.info("Creating new actor group #{actor_group_data["name"]}")
533 {:ok, actor_group} = Actors.create_group(actor_group_attrs, temp_admin_subject)
534 UuidMapping.update_entities(slug, "actor_groups", %{external_id => actor_group.id})
535 {:ok, actor_group}
536 {:ok, existing} ->
537 # Nothing to update
538 {:ok, existing}
539 end
540 end)
541 |> Ecto.Multi.run({:actor_group_members, slug, external_id}, fn repo, changes ->
542 {_, account} = changes[{:account, slug}]
543 group_uuid = UuidMapping.get_entity(slug, "actor_groups", external_id)
544
545 memberships =
546 Actors.Membership.Query.all()
547 |> Actors.Membership.Query.by_group_id(group_uuid)
548 |> Actors.Membership.Query.returning_all()
549 |> Repo.all()
550
551 existing_members = Enum.map(memberships, fn membership -> membership.actor_id end)
552 desired_members = Enum.map(actor_group_data["members"] || [], fn member ->
553 uuid = UuidMapping.get_entity(slug, "actors", member)
554 if uuid == nil do
555 raise "Cannot find provisioned actor #{member} to add to group"
556 end
557 uuid
558 end)
559
560 missing_members = desired_members -- existing_members
561 untracked_members = existing_members -- desired_members
562
563 Logger.info("Updating members for actor group #{external_id}")
564 Enum.each(missing_members || [], fn actor_uuid ->
565 Logger.info("Adding member #{external_id}")
566 Actors.Membership.Changeset.upsert(account.id, %Actors.Membership{}, %{
567 group_id: group_uuid,
568 actor_id: actor_uuid
569 })
570 |> repo.insert!()
571 end)
572
573 if actor_group_data["forceMembers"] == true do
574 # Remove untracked members
575 to_delete = Enum.map(untracked_members, fn actor_uuid -> {group_uuid, actor_uuid} end)
576 if to_delete != [] do
577 Actors.Membership.Query.by_group_id_and_actor_id({:in, to_delete})
578 |> repo.delete_all()
579 end
580 end
581
582 {:ok, nil}
583 end)
584 end)
585
586 # Create or update resources
587 multi = Enum.reduce(account_data["resources"] || %{}, multi, fn {external_id, resource_data}, multi ->
588 Ecto.Multi.run(multi, {:resource, slug, external_id}, fn _repo, changes ->
589 resource_attrs = %{
590 type: String.to_atom(resource_data["type"]),
591 name: resource_data["name"],
592 address: resource_data["address"],
593 address_description: resource_data["address_description"],
594 connections: Enum.map(resource_data["gatewayGroups"] || [], fn group ->
595 %{gateway_group_id: UuidMapping.get_entity(slug, "gateway_groups", group)}
596 end),
597 filters: Enum.map(resource_data["filters"] || [], fn filter ->
598 %{
599 ports: filter["ports"] || [],
600 protocol: String.to_atom(filter["protocol"])
601 }
602 end)
603 }
604
605 {temp_admin_subject, _, _, _} = changes[{:temp_admin, slug}]
606 uuid = UuidMapping.get_entity(slug, "resources", external_id)
607 case uuid && fetch_resource(uuid, temp_admin_subject) |> nil_if_deleted_or_not_found() do
608 nil ->
609 Logger.info("Creating new resource #{resource_data["name"]}")
610 {:ok, resource} = Resources.create_resource(resource_attrs, temp_admin_subject)
611 UuidMapping.update_entities(slug, "resources", %{external_id => resource.id})
612 {:ok, resource}
613 {:ok, existing} ->
614 existing = Repo.preload(existing, :connections)
615 Logger.info("Updating existing resource #{resource_data["name"]}")
616 only_updated_attrs = resource_attrs
617 |> Enum.reject(fn {key, value} ->
618 case key do
619 # Compare connections by gateway_group_id only
620 :connections -> value == Enum.map(existing.connections || [], fn conn -> Map.take(conn, [:gateway_group_id]) end)
621 # Compare filters by ports and protocol only
622 :filters -> value == Enum.map(existing.filters || [], fn filter -> Map.take(filter, [:ports, :protocol]) end)
623 _ -> Map.get(existing, key) == value
624 end
625 end)
626 |> Enum.into(%{})
627
628 if only_updated_attrs == %{} do
629 {:ok, existing}
630 else
631 resource = case existing |> Resources.update_resource(resource_attrs, temp_admin_subject) do
632 {:replaced, _old, new} ->
633 UuidMapping.update_entities(slug, "resources", %{external_id => new.id})
634 new
635 {:updated, value} -> value
636 x -> x
637 end
638
639 {:ok, resource}
640 end
641 end
642 end)
643 end)
644
645 # Create or update policies
646 multi = Enum.reduce(account_data["policies"] || %{}, multi, fn {external_id, policy_data}, multi ->
647 Ecto.Multi.run(multi, {:policy, slug, external_id}, fn _repo, changes ->
648 policy_attrs = %{
649 description: policy_data["description"],
650 actor_group_id: UuidMapping.get_entity(slug, "actor_groups", policy_data["group"]),
651 resource_id: UuidMapping.get_entity(slug, "resources", policy_data["resource"])
652 }
653
654 {temp_admin_subject, _, _, _} = changes[{:temp_admin, slug}]
655 uuid = UuidMapping.get_entity(slug, "policies", external_id)
656 case uuid && fetch_policy(uuid, temp_admin_subject) |> nil_if_deleted_or_not_found() do
657 nil ->
658 Logger.info("Creating new policy #{policy_data["name"]}")
659 {:ok, policy} = Policies.create_policy(policy_attrs, temp_admin_subject)
660 UuidMapping.update_entities(slug, "policies", %{external_id => policy.id})
661 {:ok, policy}
662 {:ok, existing} ->
663 Logger.info("Updating existing policy #{policy_data["name"]}")
664 only_updated_attrs = policy_attrs
665 |> Enum.reject(fn {key, value} -> Map.get(existing, key) == value end)
666 |> Enum.into(%{})
667
668 if only_updated_attrs == %{} do
669 {:ok, existing}
670 else
671 policy = case existing |> Policies.update_policy(policy_attrs, temp_admin_subject) do
672 {:replaced, _old, new} ->
673 UuidMapping.update_entities(slug, "policies", %{external_id => new.id})
674 new
675 {:updated, value} -> value
676 x -> x
677 end
678
679 {:ok, policy}
680 end
681 end
682 end)
683 end)
684
685 # Clean up temporary admin after all operations
686 multi |> Ecto.Multi.run({:cleanup_temp_admin, slug}, fn _repo, changes ->
687 {temp_admin_subject, temp_admin_actor, temp_admin_actor_email_identity, temp_admin_actor_token} =
688 changes[{:temp_admin, slug}]
689
690 cleanup_temp_admin(temp_admin_actor, temp_admin_actor_email_identity, temp_admin_actor_token, temp_admin_subject)
691 {:ok, :cleaned}
692 end)
693 end)
694 |> Ecto.Multi.run({:save_state}, fn _repo, _changes ->
695 # Save all UUID mappings to disk.
696 UuidMapping.save()
697 {:ok, :saved}
698 end)
699
700 case Repo.transaction(multi) do
701 {:ok, _result} ->
702 Logger.info("Provisioning completed successfully")
703 {:error, step, reason, _changes} ->
704 Logger.error("Provisioning failed at step #{inspect(step)}, no changes were applied: #{inspect(reason)}")
705 end
706 end
707end
708
709Provision.provision()