forked from mirrors_public/oddlama_nix-config
chore: more firezone
This commit is contained in:
parent
eeb248b582
commit
bbb76ae5ec
4 changed files with 528 additions and 66 deletions
|
@ -9,7 +9,12 @@ let
|
|||
attrNames
|
||||
boolToString
|
||||
concatLines
|
||||
concatLists
|
||||
concatMapAttrs
|
||||
concatStringsSep
|
||||
filterAttrs
|
||||
filterAttrsRecursive
|
||||
flip
|
||||
forEach
|
||||
getExe
|
||||
isBool
|
||||
|
@ -20,11 +25,26 @@ let
|
|||
mkIf
|
||||
mkMerge
|
||||
mkOption
|
||||
mkPackageOption
|
||||
optionalAttrs
|
||||
recursiveUpdate
|
||||
subtractLists
|
||||
toUpper
|
||||
types
|
||||
;
|
||||
|
||||
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
|
||||
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 = {
|
||||
# AmbientCapablities = "CAP_NET_ADMIN";
|
||||
# CapabilityBoundingSet = "CAP_CHOWN CAP_NET_ADMIN";
|
||||
# DeviceAllow = "/dev/net/tun";
|
||||
# LockPersonality = "true";
|
||||
# LogsDirectory = "dev.firezone.client";
|
||||
# LogsDirectoryMode = "755";
|
||||
# MemoryDenyWriteExecute = "true";
|
||||
# NoNewPrivileges = "true";
|
||||
# PrivateMounts = "true";
|
||||
# PrivateTmp = "true";
|
||||
# PrivateUsers = "false";
|
||||
# ProcSubset = "pid";
|
||||
# ProtectClock = "true";
|
||||
# ProtectControlGroups = "true";
|
||||
# ProtectHome = "true";
|
||||
# ProtectHostname = "true";
|
||||
# ProtectKernelLogs = "true";
|
||||
# ProtectKernelModules = "true";
|
||||
# ProtectKernelTunables = "true";
|
||||
# ProtectProc = "invisible";
|
||||
# ProtectSystem = "strict";
|
||||
# RestrictAddressFamilies = [
|
||||
# "AF_INET"
|
||||
# "AF_INET6"
|
||||
# "AF_NETLINK"
|
||||
# "AF_UNIX"
|
||||
# ];
|
||||
# RestrictNamespaces = "true";
|
||||
# RestrictRealtime = "true";
|
||||
# RestrictSUIDSGID = "true";
|
||||
# SystemCallArchitectures = "native";
|
||||
# SystemCallFilter = "@aio @basic-io @file-system @io-event @ipc @network-io @signal @system-service";
|
||||
# UMask = "077";
|
||||
AmbientCapablities = [ ];
|
||||
CapabilityBoundingSet = [ ];
|
||||
LockPersonality = "true";
|
||||
MemoryDenyWriteExecute = "true";
|
||||
NoNewPrivileges = "true";
|
||||
PrivateMounts = "true";
|
||||
PrivateTmp = "true";
|
||||
PrivateUsers = "false";
|
||||
ProcSubset = "pid";
|
||||
ProtectClock = "true";
|
||||
ProtectControlGroups = "true";
|
||||
ProtectHome = "true";
|
||||
ProtectHostname = "true";
|
||||
ProtectKernelLogs = "true";
|
||||
ProtectKernelModules = "true";
|
||||
ProtectKernelTunables = "true";
|
||||
ProtectProc = "invisible";
|
||||
ProtectSystem = "strict";
|
||||
RestrictAddressFamilies = [
|
||||
"AF_INET"
|
||||
"AF_INET6"
|
||||
"AF_NETLINK"
|
||||
"AF_UNIX"
|
||||
];
|
||||
RestrictNamespaces = "true";
|
||||
RestrictRealtime = "true";
|
||||
RestrictSUIDSGID = "true";
|
||||
SystemCallArchitectures = "native";
|
||||
SystemCallFilter = "@system-service";
|
||||
UMask = "077";
|
||||
|
||||
DynamicUser = true;
|
||||
User = "firezone";
|
||||
|
@ -117,6 +157,7 @@ let
|
|||
StateDirectory = "firezone";
|
||||
WorkingDirectory = "/var/lib/firezone";
|
||||
|
||||
Type = "exec";
|
||||
LoadCredential = mapAttrsToList (secretName: secretFile: "${secretName}:${secretFile}") (
|
||||
filterAttrs (_: v: v != null) cfg.settingsSecret
|
||||
);
|
||||
|
@ -124,10 +165,9 @@ let
|
|||
|
||||
componentOptions = component: {
|
||||
enable = mkEnableOption "the Firezone ${component} server";
|
||||
# TODO: single package plus web and api passthrough.
|
||||
# package = mkPackageOption pkgs "firezone-server" { };
|
||||
package = mkPackageOption pkgs "firezone-server-${component}" { };
|
||||
|
||||
settings = lib.mkOption {
|
||||
settings = mkOption {
|
||||
description = ''
|
||||
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).
|
||||
|
@ -140,7 +180,7 @@ let
|
|||
overwritten by this option.
|
||||
'';
|
||||
default = { };
|
||||
type = lib.types.submodule {
|
||||
type = types.submodule {
|
||||
freeformType = types.attrsOf (
|
||||
types.oneOf [
|
||||
types.bool
|
||||
|
@ -214,7 +254,7 @@ in
|
|||
option for more information regarding the actual variables and how
|
||||
filtering rules are applied for each component.
|
||||
'';
|
||||
type = lib.types.submodule {
|
||||
type = types.submodule {
|
||||
freeformType = types.attrsOf types.path;
|
||||
options = {
|
||||
RELEASE_COOKIE = mkOption {
|
||||
|
@ -325,7 +365,7 @@ in
|
|||
};
|
||||
};
|
||||
|
||||
settings = lib.mkOption {
|
||||
settings = mkOption {
|
||||
description = ''
|
||||
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).
|
||||
|
@ -336,7 +376,7 @@ in
|
|||
override specific variables passed to that component.
|
||||
'';
|
||||
default = { };
|
||||
type = lib.types.submodule {
|
||||
type = types.submodule {
|
||||
freeformType = types.attrsOf (
|
||||
types.oneOf [
|
||||
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";
|
||||
|
||||
web = componentOptions "web" // {
|
||||
|
@ -411,9 +508,160 @@ in
|
|||
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 [
|
||||
{
|
||||
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
|
||||
(mkIf cfg.enable {
|
||||
services.firezone.server.domain.enable = true;
|
||||
|
@ -447,14 +695,14 @@ in
|
|||
virtualHosts.${cfg.nginx.webDomain} = {
|
||||
forceSSL = mkDefault true;
|
||||
locations."/" = {
|
||||
proxyPass = "http://${cfg.dashboardAddress}";
|
||||
proxyPass = "http://${cfg.web.address}:${toString cfg.web.port}";
|
||||
proxyWebsockets = true;
|
||||
};
|
||||
};
|
||||
virtualHosts.${cfg.nginx.apiDomain} = {
|
||||
forceSSL = mkDefault true;
|
||||
locations."/" = {
|
||||
proxyPass = "http://${cfg.dashboardAddress}";
|
||||
proxyPass = "http://${cfg.api.address}:${toString cfg.api.port}";
|
||||
proxyWebsockets = true;
|
||||
};
|
||||
};
|
||||
|
@ -464,7 +712,7 @@ in
|
|||
{
|
||||
services.firezone.server = {
|
||||
settings = {
|
||||
LOG_LEVEL = mkDefault "debug";
|
||||
LOG_LEVEL = mkDefault "info";
|
||||
RELEASE_HOSTNAME = mkDefault "localhost.localdomain";
|
||||
|
||||
ERLANG_CLUSTER_ADAPTER = mkDefault "Elixir.Cluster.Strategy.Epmd";
|
||||
|
@ -483,7 +731,7 @@ in
|
|||
# sufficient for small to medium deployments
|
||||
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_POLICY_CONDITIONS_ENABLED = mkDefault true;
|
||||
|
@ -499,11 +747,13 @@ in
|
|||
domain.settings = {
|
||||
ERLANG_DISTRIBUTION_PORT = mkDefault 9000;
|
||||
HEALTHZ_PORT = mkDefault 4000;
|
||||
BACKGROUND_JOBS_ENABLED = mkDefault true;
|
||||
};
|
||||
|
||||
web.settings = {
|
||||
ERLANG_DISTRIBUTION_PORT = mkDefault 9001;
|
||||
HEALTHZ_PORT = mkDefault 4001;
|
||||
BACKGROUND_JOBS_ENABLED = mkDefault false;
|
||||
|
||||
PHOENIX_LISTEN_ADDRESS = mkDefault cfg.web.address;
|
||||
PHOENIX_EXTERNAL_TRUSTED_PROXIES = mkDefault (builtins.toJSON cfg.web.trustedProxies);
|
||||
|
@ -517,6 +767,7 @@ in
|
|||
api.settings = {
|
||||
ERLANG_DISTRIBUTION_PORT = mkDefault 9002;
|
||||
HEALTHZ_PORT = mkDefault 4002;
|
||||
BACKGROUND_JOBS_ENABLED = mkDefault false;
|
||||
|
||||
PHOENIX_LISTEN_ADDRESS = mkDefault cfg.api.address;
|
||||
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) {
|
||||
# FIXME: mkIf openClusterFirewall {};
|
||||
|
||||
systemd.slices.system-firezone = {
|
||||
description = "Firezone Slice";
|
||||
};
|
||||
|
@ -556,13 +847,8 @@ in
|
|||
${loadSecretEnvironment "domain"}
|
||||
|
||||
echo "Running migrations"
|
||||
${getExe pkgs.firezone-server-domain} eval Domain.Release.migrate
|
||||
|
||||
echo "Provisioning"
|
||||
''; # FIXME: ^----- aaaaaaaaaaaaaaaaaa
|
||||
#FIXME: aaaaaaaaaaaaaaaaaa
|
||||
#FIXME: aaaaaaaaaaaaaaaaaa
|
||||
#FIXME: aaaaaaaaaaaaaaaaaa
|
||||
${getExe cfg.domain.package} eval Domain.Release.migrate
|
||||
'';
|
||||
|
||||
# We use the domain environment to be able to run migrations
|
||||
environment = collectEnvironment "domain";
|
||||
|
@ -581,7 +867,26 @@ in
|
|||
|
||||
script = ''
|
||||
${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";
|
||||
|
@ -597,7 +902,7 @@ in
|
|||
|
||||
script = ''
|
||||
${loadSecretEnvironment "web"}
|
||||
exec ${getExe pkgs.firezone-server-web} start;
|
||||
exec ${getExe cfg.web.package} start;
|
||||
'';
|
||||
|
||||
environment = collectEnvironment "web";
|
||||
|
@ -613,7 +918,7 @@ in
|
|||
|
||||
script = ''
|
||||
${loadSecretEnvironment "api"}
|
||||
exec ${getExe pkgs.firezone-server-api} start;
|
||||
exec ${getExe cfg.api.package} start;
|
||||
'';
|
||||
|
||||
environment = collectEnvironment "api";
|
||||
|
|
155
modules/provision.exs
Normal file
155
modules/provision.exs
Normal 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()
|
|
@ -60,7 +60,7 @@ index c4e06bc58..89533fb81 100644
|
|||
|
||||
- {: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 =
|
||||
Ecto.Changeset.add_error(
|
||||
changeset,
|
||||
|
@ -103,12 +103,12 @@ index 15037e0a3..475c4ddfb 100644
|
|||
- from_email: compile_config!(:outbound_email_from)
|
||||
+ adapter: compile_config!(:outbound_email_adapter),
|
||||
+ from_email: compile_config!(:outbound_email_from),
|
||||
+ protocol: :ssl,
|
||||
+ relay: System.get_env("OUTBOUND_EMAIL_RELAY"),
|
||||
+ port: 465,
|
||||
+ protocol: String.to_atom(System.get_env("OUTBOUND_EMAIL_SMTP_PROTOCOL")),
|
||||
+ relay: System.get_env("OUTBOUND_EMAIL_SMTP_HOST"),
|
||||
+ port: String.to_integer(System.get_env("OUTBOUND_EMAIL_SMTP_PORT")),
|
||||
+ auth: [
|
||||
+ username: System.get_env("OUTBOUND_EMAIL_USERNAME"),
|
||||
+ password: System.get_env("OUTBOUND_EMAIL_PASSWORD")
|
||||
+ username: System.get_env("OUTBOUND_EMAIL_SMTP_USERNAME"),
|
||||
+ password: System.get_env("OUTBOUND_EMAIL_SMTP_PASSWORD")
|
||||
+ ]
|
||||
] ++ compile_config!(:outbound_email_adapter_opts)
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
}:
|
||||
{
|
||||
lib,
|
||||
nixosTests,
|
||||
fetchFromGitHub,
|
||||
beamPackages,
|
||||
pnpm_9,
|
||||
|
@ -41,9 +42,6 @@ beamPackages.mixRelease rec {
|
|||
cat >> config/runtime.exs <<EOF
|
||||
config :tzdata, :data_dir, System.get_env("TZDATA_DIR")
|
||||
EOF
|
||||
|
||||
# TODO replace https://firezone.statuspage.io with custom link,
|
||||
# unfortunately simple replace only works at compile time
|
||||
'';
|
||||
|
||||
postBuild = ''
|
||||
|
@ -115,8 +113,12 @@ beamPackages.mixRelease rec {
|
|||
};
|
||||
};
|
||||
|
||||
passthru.tests = {
|
||||
inherit (nixosTests) firezone;
|
||||
};
|
||||
|
||||
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";
|
||||
license = lib.licenses.asl20;
|
||||
maintainers = with lib.maintainers; [
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue