1
1
Fork 1
mirror of https://github.com/oddlama/nix-config.git synced 2025-10-10 23:00:39 +02:00

chore: more firezone

This commit is contained in:
oddlama 2025-02-04 23:53:19 +01:00
parent eeb248b582
commit bbb76ae5ec
No known key found for this signature in database
GPG key ID: 14EFE510775FE39A
4 changed files with 528 additions and 66 deletions

View file

@ -9,7 +9,12 @@ let
attrNames attrNames
boolToString boolToString
concatLines concatLines
concatLists
concatMapAttrs
concatStringsSep
filterAttrs filterAttrs
filterAttrsRecursive
flip
forEach forEach
getExe getExe
isBool isBool
@ -20,11 +25,26 @@ let
mkIf mkIf
mkMerge mkMerge
mkOption mkOption
mkPackageOption
optionalAttrs
recursiveUpdate
subtractLists subtractLists
toUpper
types types
; ;
cfg = config.services.firezone.server; cfg = config.services.firezone.server;
jsonFormat = pkgs.formats.json { };
availableAuthAdapters = [
"email"
"openid_connect"
"userpass"
"token"
"google_workspace"
"microsoft_entra"
"okta"
"jumpcloud"
];
# All non-secret environment variables or the given component # All non-secret environment variables or the given component
collectEnvironment = collectEnvironment =
@ -75,40 +95,60 @@ let
'') '')
); );
provisionStateJson =
let
# Convert clientSecretFile options into the real counterpart
augmentedAccounts = flip mapAttrs cfg.provision.accounts (
accountName: account:
account
// {
auth = flip mapAttrs account.auth (
authName: auth:
recursiveUpdate auth (
optionalAttrs (auth.adapter_config.clientSecretFile != null) {
adapter_config.client_secret = "{env:AUTH_CLIENT_SECRET_${toUpper accountName}_${toUpper authName}}";
}
)
);
}
);
in
jsonFormat.generate "provision-state.json" {
# Do not include any clientSecretFile attributes in the resulting json
accounts = filterAttrsRecursive (k: _: k != "clientSecretFile") augmentedAccounts;
};
commonServiceConfig = { commonServiceConfig = {
# AmbientCapablities = "CAP_NET_ADMIN"; AmbientCapablities = [ ];
# CapabilityBoundingSet = "CAP_CHOWN CAP_NET_ADMIN"; CapabilityBoundingSet = [ ];
# DeviceAllow = "/dev/net/tun"; LockPersonality = "true";
# LockPersonality = "true"; MemoryDenyWriteExecute = "true";
# LogsDirectory = "dev.firezone.client"; NoNewPrivileges = "true";
# LogsDirectoryMode = "755"; PrivateMounts = "true";
# MemoryDenyWriteExecute = "true"; PrivateTmp = "true";
# NoNewPrivileges = "true"; PrivateUsers = "false";
# PrivateMounts = "true"; ProcSubset = "pid";
# PrivateTmp = "true"; ProtectClock = "true";
# PrivateUsers = "false"; ProtectControlGroups = "true";
# ProcSubset = "pid"; ProtectHome = "true";
# ProtectClock = "true"; ProtectHostname = "true";
# ProtectControlGroups = "true"; ProtectKernelLogs = "true";
# ProtectHome = "true"; ProtectKernelModules = "true";
# ProtectHostname = "true"; ProtectKernelTunables = "true";
# ProtectKernelLogs = "true"; ProtectProc = "invisible";
# ProtectKernelModules = "true"; ProtectSystem = "strict";
# ProtectKernelTunables = "true"; RestrictAddressFamilies = [
# ProtectProc = "invisible"; "AF_INET"
# ProtectSystem = "strict"; "AF_INET6"
# RestrictAddressFamilies = [ "AF_NETLINK"
# "AF_INET" "AF_UNIX"
# "AF_INET6" ];
# "AF_NETLINK" RestrictNamespaces = "true";
# "AF_UNIX" RestrictRealtime = "true";
# ]; RestrictSUIDSGID = "true";
# RestrictNamespaces = "true"; SystemCallArchitectures = "native";
# RestrictRealtime = "true"; SystemCallFilter = "@system-service";
# RestrictSUIDSGID = "true"; UMask = "077";
# SystemCallArchitectures = "native";
# SystemCallFilter = "@aio @basic-io @file-system @io-event @ipc @network-io @signal @system-service";
# UMask = "077";
DynamicUser = true; DynamicUser = true;
User = "firezone"; User = "firezone";
@ -117,6 +157,7 @@ let
StateDirectory = "firezone"; StateDirectory = "firezone";
WorkingDirectory = "/var/lib/firezone"; WorkingDirectory = "/var/lib/firezone";
Type = "exec";
LoadCredential = mapAttrsToList (secretName: secretFile: "${secretName}:${secretFile}") ( LoadCredential = mapAttrsToList (secretName: secretFile: "${secretName}:${secretFile}") (
filterAttrs (_: v: v != null) cfg.settingsSecret filterAttrs (_: v: v != null) cfg.settingsSecret
); );
@ -124,10 +165,9 @@ let
componentOptions = component: { componentOptions = component: {
enable = mkEnableOption "the Firezone ${component} server"; enable = mkEnableOption "the Firezone ${component} server";
# TODO: single package plus web and api passthrough. package = mkPackageOption pkgs "firezone-server-${component}" { };
# package = mkPackageOption pkgs "firezone-server" { };
settings = lib.mkOption { settings = mkOption {
description = '' description = ''
Environment variables for this component of the Firezone server. For a Environment variables for this component of the Firezone server. For a
list of available variables, please refer to the [upstream definitions](https://github.com/firezone/firezone/blob/main/elixir/apps/domain/lib/domain/config/definitions.ex). list of available variables, please refer to the [upstream definitions](https://github.com/firezone/firezone/blob/main/elixir/apps/domain/lib/domain/config/definitions.ex).
@ -140,7 +180,7 @@ let
overwritten by this option. overwritten by this option.
''; '';
default = { }; default = { };
type = lib.types.submodule { type = types.submodule {
freeformType = types.attrsOf ( freeformType = types.attrsOf (
types.oneOf [ types.oneOf [
types.bool types.bool
@ -214,7 +254,7 @@ in
option for more information regarding the actual variables and how option for more information regarding the actual variables and how
filtering rules are applied for each component. filtering rules are applied for each component.
''; '';
type = lib.types.submodule { type = types.submodule {
freeformType = types.attrsOf types.path; freeformType = types.attrsOf types.path;
options = { options = {
RELEASE_COOKIE = mkOption { RELEASE_COOKIE = mkOption {
@ -325,7 +365,7 @@ in
}; };
}; };
settings = lib.mkOption { settings = mkOption {
description = '' description = ''
Environment variables for the Firezone server. For a list of available Environment variables for the Firezone server. For a list of available
variables, please refer to the [upstream definitions](https://github.com/firezone/firezone/blob/main/elixir/apps/domain/lib/domain/config/definitions.ex). variables, please refer to the [upstream definitions](https://github.com/firezone/firezone/blob/main/elixir/apps/domain/lib/domain/config/definitions.ex).
@ -336,7 +376,7 @@ in
override specific variables passed to that component. override specific variables passed to that component.
''; '';
default = { }; default = { };
type = lib.types.submodule { type = types.submodule {
freeformType = types.attrsOf ( freeformType = types.attrsOf (
types.oneOf [ types.oneOf [
types.bool types.bool
@ -350,6 +390,63 @@ in
}; };
}; };
smtp = {
configureManually = mkOption {
type = types.bool;
default = false;
description = ''
Outbound email configuration is mandatory for Firezone and supports
many different delivery adapters. Yet, most users will only need an
SMTP relay to send emails, so this configuration enforced by default.
If you want to utilize an alternative way to send emails (e.g. via a
supportd API-based service), enable this option and define
`OUTBOUND_EMAIL_FROM`, `OUTBOUND_EMAIL_ADAPTER` and
`OUTBOUND_EMAIL_ADAPTER_OPTS` manually via
{option}`services.firezone.server.settings` and/or
{option}`services.firezone.server.settingsSecret`.
The Firezone documentation holds [a list of supported Swoosh adapters](https://github.com/firezone/firezone/blob/main/website/src/app/docs/reference/env-vars/readme.mdx#outbound-emails).
'';
};
from = mkOption {
type = types.str;
example = "firezone@example.com";
description = "Outbound SMTP FROM address";
};
host = mkOption {
type = types.str;
example = "mail.example.com";
description = "Outbound SMTP host";
};
port = mkOption {
type = types.port;
example = 465;
description = "Outbound SMTP port";
};
implicitTls = mkOption {
type = types.bool;
default = false;
description = "Whether to use implicit TLS instead of STARTTLS (usually port 465)";
};
username = mkOption {
type = types.str;
example = "firezone@example.com";
description = "Username to authenticate against the SMTP relay";
};
passwordFile = mkOption {
type = types.path;
example = "/run/secrets/smtp-password";
description = "File containing the password for the given username. Beware that a file in the nix store will be world readable.";
};
};
domain = componentOptions "domain"; domain = componentOptions "domain";
web = componentOptions "web" // { web = componentOptions "web" // {
@ -411,9 +508,160 @@ in
description = "A list of trusted proxies"; description = "A list of trusted proxies";
}; };
}; };
provision = {
enable = mkEnableOption "provisioning of the Firezone domain server";
accounts = mkOption {
type = types.attrsOf (
types.submodule {
freeformType = jsonFormat.type;
options = {
name = mkOption {
type = types.str;
description = "The account name";
example = "My Organization";
};
features =
let
mkFeatureOption =
name: default:
mkOption {
type = types.bool;
inherit default;
description = "Whether to enable the `${name}` feature for this account.";
};
in
{
flow_activities = mkFeatureOption "flow_activities" true;
policy_conditions = mkFeatureOption "policy_conditions" true;
multi_site_resources = mkFeatureOption "multi_site_resources" true;
traffic_filters = mkFeatureOption "traffic_filters" true;
self_hosted_relays = mkFeatureOption "self_hosted_relays" true;
idp_sync = mkFeatureOption "idp_sync" true;
rest_api = mkFeatureOption "rest_api" true;
internet_resource = mkFeatureOption "internet_resource" true;
};
actors = mkOption {
type = types.attrsOf (
types.submodule {
freeformType = jsonFormat.type;
options = {
type = mkOption {
type = types.enum [
"account_admin_user"
"account_user"
"service_account"
"api_client"
];
description = "The account type";
};
email = mkOption {
type = types.str;
description = "The email address used to authenticate as this account";
};
};
}
);
default = { };
example = {
admin = {
type = "account_admin_user";
email = "admin@myorg.example.com";
};
};
description = "All actors (users) to provision.";
};
auth = mkOption {
type = types.attrsOf (
types.submodule {
freeformType = jsonFormat.type;
options = {
adapter = mkOption {
type = types.enum availableAuthAdapters;
description = "The auth adapter type";
};
adapter_config.clientSecretFile = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
A file containing a the client secret for an openid_connect adapter.
You only need to set this if this is an openid_connect provider.
'';
};
};
}
);
default = { };
example = {
myoidcprovider = {
adapter = "openid_connect";
adapter_config = {
client_id = "clientid";
clientSecretFile = "/run/secrets/oidc-client-secret";
response_type = "code";
scope = "openid email name";
discorvery_document_uri = "https://auth.example.com/.well-known/openid-configuration";
};
};
};
description = "All authentication providers to provision.";
};
};
}
);
default = { };
example = {
main = {
name = "My Account / Organization";
metadata.stripe.billing_email = "org@myorg.example.com";
features.rest_api = false;
};
};
description = ''
All accounts to provision. The key specified here will become the
account slug. By using `"{file:/path/to/file}"` as a string value
anywhere in these settings, the provisioning script will replace that
value with the content of the given file at runtime.
Please refer to the [Firezone source code](https://github.com/firezone/firezone/blob/main/elixir/apps/domain/lib/domain/accounts/account.ex)
for all available properties.
'';
};
};
}; };
config = mkMerge [ config = mkMerge [
{
assertions =
[
{
assertion = cfg.provision.enable -> cfg.domain.enable;
message = "Provisioning must be done on a machine running the firezone domain server";
}
]
++ concatLists (
flip mapAttrsToList cfg.provision.accounts (
accountName: accountCfg:
[
{
assertion = (builtins.match "^[[:lower:]_-]+$" accountName) != null;
message = "An account name must contain only lowercase characters and underscores, as it will be used as the URL slug for this account.";
}
]
++ flip mapAttrsToList accountCfg.auth (
authName: _: {
assertion = (builtins.match "^[[:alnum:]_-]+$" authName) != null;
message = "An authentication provider name must contain only letters, numbers, underscores or dashes.";
}
)
)
);
}
# Enable all components if the main server is enabled # Enable all components if the main server is enabled
(mkIf cfg.enable { (mkIf cfg.enable {
services.firezone.server.domain.enable = true; services.firezone.server.domain.enable = true;
@ -447,14 +695,14 @@ in
virtualHosts.${cfg.nginx.webDomain} = { virtualHosts.${cfg.nginx.webDomain} = {
forceSSL = mkDefault true; forceSSL = mkDefault true;
locations."/" = { locations."/" = {
proxyPass = "http://${cfg.dashboardAddress}"; proxyPass = "http://${cfg.web.address}:${toString cfg.web.port}";
proxyWebsockets = true; proxyWebsockets = true;
}; };
}; };
virtualHosts.${cfg.nginx.apiDomain} = { virtualHosts.${cfg.nginx.apiDomain} = {
forceSSL = mkDefault true; forceSSL = mkDefault true;
locations."/" = { locations."/" = {
proxyPass = "http://${cfg.dashboardAddress}"; proxyPass = "http://${cfg.api.address}:${toString cfg.api.port}";
proxyWebsockets = true; proxyWebsockets = true;
}; };
}; };
@ -464,7 +712,7 @@ in
{ {
services.firezone.server = { services.firezone.server = {
settings = { settings = {
LOG_LEVEL = mkDefault "debug"; LOG_LEVEL = mkDefault "info";
RELEASE_HOSTNAME = mkDefault "localhost.localdomain"; RELEASE_HOSTNAME = mkDefault "localhost.localdomain";
ERLANG_CLUSTER_ADAPTER = mkDefault "Elixir.Cluster.Strategy.Epmd"; ERLANG_CLUSTER_ADAPTER = mkDefault "Elixir.Cluster.Strategy.Epmd";
@ -483,7 +731,7 @@ in
# sufficient for small to medium deployments # sufficient for small to medium deployments
DATABASE_POOL_SIZE = "16"; DATABASE_POOL_SIZE = "16";
AUTH_PROVIDER_ADAPTERS = mkDefault "email,openid_connect,userpass,token"; AUTH_PROVIDER_ADAPTERS = mkDefault (concatStringsSep "," availableAuthAdapters);
FEATURE_FLOW_ACTIVITIES_ENABLED = mkDefault true; FEATURE_FLOW_ACTIVITIES_ENABLED = mkDefault true;
FEATURE_POLICY_CONDITIONS_ENABLED = mkDefault true; FEATURE_POLICY_CONDITIONS_ENABLED = mkDefault true;
@ -499,11 +747,13 @@ in
domain.settings = { domain.settings = {
ERLANG_DISTRIBUTION_PORT = mkDefault 9000; ERLANG_DISTRIBUTION_PORT = mkDefault 9000;
HEALTHZ_PORT = mkDefault 4000; HEALTHZ_PORT = mkDefault 4000;
BACKGROUND_JOBS_ENABLED = mkDefault true;
}; };
web.settings = { web.settings = {
ERLANG_DISTRIBUTION_PORT = mkDefault 9001; ERLANG_DISTRIBUTION_PORT = mkDefault 9001;
HEALTHZ_PORT = mkDefault 4001; HEALTHZ_PORT = mkDefault 4001;
BACKGROUND_JOBS_ENABLED = mkDefault false;
PHOENIX_LISTEN_ADDRESS = mkDefault cfg.web.address; PHOENIX_LISTEN_ADDRESS = mkDefault cfg.web.address;
PHOENIX_EXTERNAL_TRUSTED_PROXIES = mkDefault (builtins.toJSON cfg.web.trustedProxies); PHOENIX_EXTERNAL_TRUSTED_PROXIES = mkDefault (builtins.toJSON cfg.web.trustedProxies);
@ -517,6 +767,7 @@ in
api.settings = { api.settings = {
ERLANG_DISTRIBUTION_PORT = mkDefault 9002; ERLANG_DISTRIBUTION_PORT = mkDefault 9002;
HEALTHZ_PORT = mkDefault 4002; HEALTHZ_PORT = mkDefault 4002;
BACKGROUND_JOBS_ENABLED = mkDefault false;
PHOENIX_LISTEN_ADDRESS = mkDefault cfg.api.address; PHOENIX_LISTEN_ADDRESS = mkDefault cfg.api.address;
PHOENIX_EXTERNAL_TRUSTED_PROXIES = mkDefault (builtins.toJSON cfg.api.trustedProxies); PHOENIX_EXTERNAL_TRUSTED_PROXIES = mkDefault (builtins.toJSON cfg.api.trustedProxies);
@ -528,9 +779,49 @@ in
}; };
}; };
} }
(mkIf (!cfg.smtp.configureManually) {
services.firezone.server.settings = {
OUTBOUND_EMAIL_ADAPTER = "Elixir.Swoosh.Adapters.Mua";
OUTBOUND_EMAIL_ADAPTER_OPTS = builtins.toJSON { };
OUTBOUND_EMAIL_FROM = cfg.smtp.from;
OUTBOUND_EMAIL_SMTP_HOST = cfg.smtp.host;
OUTBOUND_EMAIL_SMTP_PORT = toString cfg.smtp.port;
OUTBOUND_EMAIL_SMTP_PROTOCOL = if cfg.smtp.implicitTls then "ssl" else "tcp";
OUTBOUND_EMAIL_SMTP_USERNAME = cfg.smtp.username;
};
services.firezone.server.settingsSecret = {
OUTBOUND_EMAIL_SMTP_PASSWORD = cfg.smtp.passwordFile;
};
})
(mkIf cfg.provision.enable {
# Load client secrets from authentication providers
services.firezone.server.settingsSecret = flip concatMapAttrs cfg.provision.accounts (
accountName: accountCfg:
flip concatMapAttrs accountCfg.auth (
authName: authCfg:
optionalAttrs (authCfg.adapter_config.clientSecretFile != null) {
"AUTH_CLIENT_SECRET_${toUpper accountName}_${toUpper authName}" =
authCfg.adapter_config.clientSecretFile;
}
)
);
})
(mkIf (cfg.openClusterFirewall && cfg.domain.enable) {
networking.firewall.allowedTCPPorts = [
cfg.domain.settings.ERLANG_DISTRIBUTION_PORT
];
})
(mkIf (cfg.openClusterFirewall && cfg.web.enable) {
networking.firewall.allowedTCPPorts = [
cfg.web.settings.ERLANG_DISTRIBUTION_PORT
];
})
(mkIf (cfg.openClusterFirewall && cfg.api.enable) {
networking.firewall.allowedTCPPorts = [
cfg.api.settings.ERLANG_DISTRIBUTION_PORT
];
})
(mkIf (cfg.domain.enable || cfg.web.enable || cfg.api.enable) { (mkIf (cfg.domain.enable || cfg.web.enable || cfg.api.enable) {
# FIXME: mkIf openClusterFirewall {};
systemd.slices.system-firezone = { systemd.slices.system-firezone = {
description = "Firezone Slice"; description = "Firezone Slice";
}; };
@ -556,13 +847,8 @@ in
${loadSecretEnvironment "domain"} ${loadSecretEnvironment "domain"}
echo "Running migrations" echo "Running migrations"
${getExe pkgs.firezone-server-domain} eval Domain.Release.migrate ${getExe cfg.domain.package} eval Domain.Release.migrate
'';
echo "Provisioning"
''; # FIXME: ^----- aaaaaaaaaaaaaaaaaa
#FIXME: aaaaaaaaaaaaaaaaaa
#FIXME: aaaaaaaaaaaaaaaaaa
#FIXME: aaaaaaaaaaaaaaaaaa
# We use the domain environment to be able to run migrations # We use the domain environment to be able to run migrations
environment = collectEnvironment "domain"; environment = collectEnvironment "domain";
@ -581,7 +867,26 @@ in
script = '' script = ''
${loadSecretEnvironment "domain"} ${loadSecretEnvironment "domain"}
exec ${getExe pkgs.firezone-server-domain} start; exec ${getExe cfg.domain.package} start;
'';
postStart = mkIf cfg.provision.enable ''
${loadSecretEnvironment "domain"}
# Wait for the firezone server to come online
count=0
while ! ${getExe cfg.domain.package} pid >/dev/null
do
sleep 1
if [[ "$count" -eq 30 ]]; then
echo "Tried for at least 30 seconds, giving up..."
exit 1
fi
count=$((count++))
done
ln -sTf ${provisionStateJson} provision-state.json
${getExe cfg.domain.package} rpc 'Code.eval_file("${./provision.exs}")'
''; '';
environment = collectEnvironment "domain"; environment = collectEnvironment "domain";
@ -597,7 +902,7 @@ in
script = '' script = ''
${loadSecretEnvironment "web"} ${loadSecretEnvironment "web"}
exec ${getExe pkgs.firezone-server-web} start; exec ${getExe cfg.web.package} start;
''; '';
environment = collectEnvironment "web"; environment = collectEnvironment "web";
@ -613,7 +918,7 @@ in
script = '' script = ''
${loadSecretEnvironment "api"} ${loadSecretEnvironment "api"}
exec ${getExe pkgs.firezone-server-api} start; exec ${getExe cfg.api.package} start;
''; '';
environment = collectEnvironment "api"; environment = collectEnvironment "api";

155
modules/provision.exs Normal file
View file

@ -0,0 +1,155 @@
defmodule Provision do
alias Domain.{Repo, Accounts, Auth, Actors}
require Logger
defp resolve_references(value) when is_map(value) do
Enum.into(value, %{}, fn {k, v} -> {k, resolve_references(v)} end)
end
defp resolve_references(value) when is_list(value) do
Enum.map(value, &resolve_references/1)
end
defp resolve_references(value) when is_binary(value) do
Regex.replace(~r/\{env:([^}]+)\}/, value, fn _, var ->
System.get_env(var) || raise "Environment variable #{var} not set"
end)
end
defp resolve_references(value), do: value
defp atomize_keys(map) when is_map(map) do
Enum.into(map, %{}, fn {k, v} ->
{
if(is_binary(k), do: String.to_atom(k), else: k),
if(is_map(v), do: atomize_keys(v), else: v)
}
end)
end
def provision() do
IO.inspect("Starting provisioning", label: "INFO")
json_file = "provision-state.json"
{:ok, raw_json} = File.read(json_file)
{:ok, %{"accounts" => accounts}} = Jason.decode(raw_json)
accounts = resolve_references(accounts)
multi = Enum.reduce(accounts, Ecto.Multi.new(), fn {slug, account_data}, multi ->
account_attrs = atomize_keys(%{
name: account_data["name"],
slug: slug,
features: Map.get(account_data, "features", %{}),
metadata: Map.get(account_data, "metadata", %{}),
limits: Map.get(account_data, "limits", %{}),
})
multi = multi
|> Ecto.Multi.run({:account, slug}, fn repo, _changes ->
case Accounts.fetch_account_by_id_or_slug(slug) do
{:ok, acc} ->
IO.inspect("Updating existing account #{slug}", label: "INFO")
updated_acc = acc |> Ecto.Changeset.change(account_attrs) |> repo.update!()
{:ok, {:existing, updated_acc}}
_ ->
IO.inspect("Creating new account #{slug}", label: "INFO")
{:ok, account} = Accounts.create_account(account_attrs)
{:ok, {:new, account}}
end
end)
|> Ecto.Multi.run({:everyone_group, slug}, fn _repo, changes ->
case Map.get(changes, {:account, slug}) do
{:new, account} ->
IO.inspect("Creating Everyone group for new account", label: "INFO")
Actors.create_managed_group(account, %{name: "Everyone", membership_rules: [%{operator: true}]})
{:existing, _account} ->
{:ok, :skipped}
end
end)
|> Ecto.Multi.run({:provider, slug}, fn _repo, changes ->
case Map.get(changes, {:account, slug}) do
{:new, account} ->
IO.inspect("Creating default email provider for new account", label: "INFO")
Auth.create_provider(account, %{name: "Email", adapter: :email, adapter_config: %{}})
{:existing, account} ->
Auth.Provider.Query.not_disabled()
|> Auth.Provider.Query.by_adapter(:email)
|> Auth.Provider.Query.by_account_id(account.id)
|> Repo.fetch(Auth.Provider.Query, [])
end
end)
multi = Enum.reduce(account_data["actors"] || %{}, multi, fn {name, actor_data}, multi ->
actor_attrs = atomize_keys(%{
name: name,
type: String.to_atom(actor_data["type"]),
})
Ecto.Multi.run(multi, {:actor, slug, name}, fn repo, changes ->
{_, account} = changes[{:account, slug}]
case Repo.get_by(Actors.Actor, account_id: account.id, name: name) do
nil ->
IO.inspect("Creating new actor #{name}", label: "INFO")
{:ok, actor} = Actors.create_actor(account, actor_attrs)
{:ok, {:new, actor}}
act ->
IO.inspect("Updating existing actor #{name}", label: "INFO")
updated_act = act |> Ecto.Changeset.change(actor_attrs) |> repo.update!()
{:ok, {:existing, updated_act}}
end
end)
|> Ecto.Multi.run({:actor_identity, slug, name}, fn repo, changes ->
email_provider = changes[{:provider, slug}]
case Map.get(changes, {:actor, slug, name}) do
{:new, actor} ->
IO.inspect("Creating actor email identity", label: "INFO")
Auth.create_identity(actor, email_provider, %{
provider_identifier: actor_data["email"],
provider_identifier_confirmation: actor_data["email"]
})
{:existing, actor} ->
IO.inspect("Updating actor email identity", label: "INFO")
{:ok, identity} = Auth.Identity.Query.not_deleted()
|> Auth.Identity.Query.by_actor_id(actor.id)
|> Auth.Identity.Query.by_provider_id(email_provider.id)
|> Repo.fetch(Auth.Identity.Query, [])
{:ok, identity |> Ecto.Changeset.change(%{
provider_identifier: actor_data["email"],
}) |> repo.update!()}
end
end)
end)
multi = Enum.reduce(account_data["auth"] || %{}, multi, fn {name, provider_data}, multi ->
provider_attrs = %{
name: name,
adapter: String.to_atom(provider_data["adapter"]),
adapter_config: provider_data["adapter_config"],
}
Ecto.Multi.run(multi, {:provider, slug, name}, fn repo, changes ->
{_, account} = changes[{:account, slug}]
case Repo.get_by(Auth.Provider, account_id: account.id, name: name) do
nil ->
IO.inspect("Creating new provider #{name}", label: "INFO")
Auth.create_provider(account, provider_attrs)
existing ->
IO.inspect("Updating existing provider #{name}", label: "INFO")
{:ok, existing |> Ecto.Changeset.change(provider_attrs) |> repo.update!()}
end
end)
end)
multi
end)
case Repo.transaction(multi) do
{:ok, _result} ->
Logger.info("Provisioning completed successfully.")
{:error, step, reason, _changes} ->
Logger.error("Provisioning failed at step #{inspect(step)}: #{inspect(reason)}")
end
end
end
Provision.provision()

View file

@ -60,7 +60,7 @@ index c4e06bc58..89533fb81 100644
- {:error, :send_email, _reason, _effects_so_far} -> - {:error, :send_email, _reason, _effects_so_far} ->
+ {:error, :send_email, reason, _effects_so_far} -> + {:error, :send_email, reason, _effects_so_far} ->
+ Logger.info("aaaaaaaaaaaaaaaaaa", reason: inspect(reason)) + Logger.info("failed to send email", reason: inspect(reason))
changeset = changeset =
Ecto.Changeset.add_error( Ecto.Changeset.add_error(
changeset, changeset,
@ -103,12 +103,12 @@ index 15037e0a3..475c4ddfb 100644
- from_email: compile_config!(:outbound_email_from) - from_email: compile_config!(:outbound_email_from)
+ adapter: compile_config!(:outbound_email_adapter), + adapter: compile_config!(:outbound_email_adapter),
+ from_email: compile_config!(:outbound_email_from), + from_email: compile_config!(:outbound_email_from),
+ protocol: :ssl, + protocol: String.to_atom(System.get_env("OUTBOUND_EMAIL_SMTP_PROTOCOL")),
+ relay: System.get_env("OUTBOUND_EMAIL_RELAY"), + relay: System.get_env("OUTBOUND_EMAIL_SMTP_HOST"),
+ port: 465, + port: String.to_integer(System.get_env("OUTBOUND_EMAIL_SMTP_PORT")),
+ auth: [ + auth: [
+ username: System.get_env("OUTBOUND_EMAIL_USERNAME"), + username: System.get_env("OUTBOUND_EMAIL_SMTP_USERNAME"),
+ password: System.get_env("OUTBOUND_EMAIL_PASSWORD") + password: System.get_env("OUTBOUND_EMAIL_SMTP_PASSWORD")
+ ] + ]
] ++ compile_config!(:outbound_email_adapter_opts) ] ++ compile_config!(:outbound_email_adapter_opts)

View file

@ -3,6 +3,7 @@
}: }:
{ {
lib, lib,
nixosTests,
fetchFromGitHub, fetchFromGitHub,
beamPackages, beamPackages,
pnpm_9, pnpm_9,
@ -41,9 +42,6 @@ beamPackages.mixRelease rec {
cat >> config/runtime.exs <<EOF cat >> config/runtime.exs <<EOF
config :tzdata, :data_dir, System.get_env("TZDATA_DIR") config :tzdata, :data_dir, System.get_env("TZDATA_DIR")
EOF EOF
# TODO replace https://firezone.statuspage.io with custom link,
# unfortunately simple replace only works at compile time
''; '';
postBuild = '' postBuild = ''
@ -115,8 +113,12 @@ beamPackages.mixRelease rec {
}; };
}; };
passthru.tests = {
inherit (nixosTests) firezone;
};
meta = { meta = {
description = "Backend server and Admin UI for the Firezone zero-trust access platform"; description = "Backend server for the Firezone zero-trust access platform";
homepage = "https://github.com/firezone/firezone"; homepage = "https://github.com/firezone/firezone";
license = lib.licenses.asl20; license = lib.licenses.asl20;
maintainers = with lib.maintainers; [ maintainers = with lib.maintainers; [