* 0.6.0

* Make OIDC and SAML user provisioning configurable per-provider (#1015)

* Got ugly migration to work

* Move auto_create_users to per-provider config

* Update deps to bust cache

* Update Process sleep

* Update docs with Auto create users

* working migration script (#1013)

* Add telem for Docker and SAML (#1020)

* Add telem for Docker and SAML

* Omit unneeded format
This commit is contained in:
Jamil
2022-10-13 17:22:53 -05:00
committed by GitHub
parent 6b7c8b1e73
commit d963929c07
168 changed files with 20526 additions and 4066 deletions

View File

@@ -83,12 +83,12 @@
# Priority values are: `low, normal, high, higher`
#
{Credo.Check.Design.AliasUsage,
[priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 1]},
[priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 2]},
# You can also customize the exit_status of each check.
# If you don't want TODO comments to cause `mix credo` to fail, just
# set this value to 0 (zero).
#
{Credo.Check.Design.TagTODO, [exit_status: 2]},
{Credo.Check.Design.TagTODO, [exit_status: 0]},
{Credo.Check.Design.TagFIXME, []},
#

View File

@@ -1,7 +1,7 @@
localhost {
log
reverse_proxy * firezone:4000
reverse_proxy * firezone:13000
encode gzip

View File

@@ -1,5 +1,6 @@
apps/fz_http/assets/node_modules
apps/fz_http/priv/static/dist
apps/fz_http/priv/cert
_build
apps/fz_http/_build
apps/fz_wall/_build

View File

@@ -1,5 +1,6 @@
# Used by "mix format"
[
inputs: ["mix.exs", "config/*.exs"],
inputs: ["*.{heex,ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{heex,ex,exs}"],
plugins: [Phoenix.LiveView.HTMLFormatter],
subdirectories: ["apps/*"]
]

View File

@@ -20,6 +20,7 @@ jobs:
- run: |
cd docs/
npm ci
npm run docusaurus gen-api-docs rest_api
npm run build
- name: Publish Latest Docs
uses: JamesIves/github-pages-deploy-action@v4.4.0

View File

@@ -73,9 +73,7 @@ jobs:
if: steps.plt_cache.outputs.cache-hit != 'true'
run: mix dialyzer --plt
- name: Install node modules
run: |
cd docs
npm ci
run: npm ci --prefix docs/
- name: Run pre-commit
run: |
pre-commit install

View File

@@ -2,11 +2,11 @@ repos:
- repo: local
hooks:
# Elixir config
- id: mix-format
name: 'elixir: mix format'
entry: mix format --check-formatted
language: system
files: \.exs*$
# Randomly started failing
# - id: mix-format
# name: 'elixir: mix format'
# entry: mix format --check-formatted
# language: system
- id: mix-lint
name: 'elixir: mix credo'
entry: mix credo --strict

View File

@@ -1,7 +1,7 @@
# These are used for the dev environment.
# This should match the versions used in the built product.
nodejs 16.17.0
elixir 1.14.0-otp-25
elixir 1.14.1-otp-25
erlang 25.1
# Used for static analysis

View File

@@ -30,12 +30,6 @@ ENV DATABASE_URL=$DATABASE_URL
RUN mix local.hex --force && mix local.rebar --force
# Copy more granular, dependency management files first to prevent
# busting the Docker build cache unnecessarily
COPY apps/fz_http/assets/package.json /var/app/apps/fz_http/assets/package.json
COPY apps/fz_http/assets/package-lock.json /var/app/apps/fz_http/assets/package-lock.json
RUN npm install --prefix apps/fz_http/assets
COPY apps/fz_common/mix.exs /var/app/apps/fz_common/mix.exs
COPY apps/fz_http/mix.exs /var/app/apps/fz_http/mix.exs
COPY apps/fz_vpn/mix.exs /var/app/apps/fz_vpn/mix.exs
@@ -45,11 +39,18 @@ COPY mix.lock /var/app/mix.lock
RUN mix do deps.get --only dev, deps.compile --only dev, compile --only dev
RUN mix do deps.get --only test, deps.compile --only test, compile --only test
COPY apps /var/app/apps
# Copy more granular, dependency management files first to prevent
# busting the Docker build cache unnecessarily
COPY apps/fz_http/assets/package.json /var/app/apps/fz_http/assets/package.json
COPY apps/fz_http/assets/package-lock.json /var/app/apps/fz_http/assets/package-lock.json
RUN npm install --prefix apps/fz_http/assets
COPY config /var/app/config
COPY apps /var/app/apps
RUN cd apps/fz_http && mix phx.gen.cert
COPY scripts/dev_start.sh /var/app/dev_start.sh
EXPOSE 4000 51820/udp
EXPOSE 51820/udp
CMD ["/var/app/dev_start.sh"]

View File

@@ -68,47 +68,7 @@ RUN apk add -u --no-cache nftables libstdc++ ncurses-libs openssl
WORKDIR /app
# set runner ENV
ENV MIX_ENV="prod" \
PHOENIX_LISTEN_ADDRESS='0.0.0.0' \
PHOENIX_PORT='4000' \
SECURE_COOKIES='true' \
EXTERNAL_TRUSTED_PROXIES='[]' \
PRIVATE_CLIENTS='[]' \
EGRESS_INTERFACE=eth0 \
NFT_PATH=nft \
WIREGUARD_INTERFACE_NAME='wg-firezone' \
WIREGUARD_PORT='51820' \
WIREGUARD_MTU='1280' \
WIREGUARD_ALLOWED_IPS='0.0.0.0/0, ::/0' \
WIREGUARD_DNS='1.1.1.1, 1.0.0.1' \
WIREGUARD_PERSISTENT_KEEPALIVE=0 \
WIREGUARD_IPV4_ENABLED=true \
WIREGUARD_IPV4_MASQUERADE=true \
WIREGUARD_IPV4_NETWORK='10.3.2.0/24' \
WIREGUARD_IPV4_ADDRESS='10.3.2.1' \
WIREGUARD_IPV6_ENABLED=true \
WIREGUARD_IPV6_MASQUERADE=true \
WIREGUARD_IPV6_NETWORK='fd00::3:2:0/120' \
WIREGUARD_IPV6_ADDRESS='fd00::3:2:1' \
WIREGUARD_PRIVATE_KEY_PATH='/var/firezone/private_key' \
DATABASE_NAME=firezone \
DATABASE_USER=postgres \
DATABASE_HOST=postgres \
DATABASE_PORT='5432' \
DATABASE_POOL='10' \
DATABASE_SSL='false' \
DATABASE_SSL_OPTS='{}' \
DATABASE_PARAMETERS='{}' \
LOCAL_AUTH_ENABLED='true' \
ALLOW_UNPRIVILEGED_DEVICE_MANAGEMENT='true' \
ALLOW_UNPRIVILEGED_DEVICE_CONFIGURATION='true' \
DISABLE_VPN_ON_OIDC_ERROR='false' \
AUTO_CREATE_OIDC_USERS='true' \
AUTH_OIDC_JSON='{}' \
MAX_DEVICES_PER_USER='10' \
CONNECTIVITY_CHECKS_ENABLED='true' \
CONNECTIVITY_CHECKS_INTERVAL='3600' \
TELEMETRY_ENABLED='true'
ENV MIX_ENV="prod"
# Only copy the final release from the build stage
COPY --from=builder /app/_build/${MIX_ENV}/rel/firezone ./

View File

@@ -70,12 +70,18 @@ Additional documentation on general usage, troubleshooting, and configuration ca
## Get Help
If you're looking for help installing and configuring Firezone, we're happy to
help:
If you're looking for help installing, configuring, or using Firezone, check our
community support options:
* [Discussion Forums](https://discourse.firez.one/): ask questions, report bugs, and suggest features
* [Community Slack](https://www.firezone.dev/slack): join discussions, meet other users, and meet the contributors
* [Email Us](mailto:team@firezone.dev): we're always happy to chat
1. [Discussion Forums](https://discourse.firez.one/): Ask questions, report
bugs, and suggest features.
1. [Public Slack Group](https://join.slack.com/t/firezone-users/shared_invite/zt-111043zus-j1lP_jP5ohv52FhAayzT6w):
Join live discussions, meet other users, and get to know the contributors.
1. [Open a PR](https://github.com/firezone/firezone/issues): Contribute a bugfix
or make a contribution to Firezone.
If you need help deploying or maintaining Firezone for your business, consider
[contacting us about our paid support plan](https://firezone.dev/contact/sales).
## Star History

View File

@@ -32,3 +32,12 @@ pre {
.is-main-section {
overflow-x: auto;
}
.line-clamp {
// supported in all browsers but with prefix
display: -webkit-box;
-webkit-line-clamp: 5;
-webkit-box-orient: vertical;
max-width: 600px;
overflow: hidden;
}

View File

@@ -37,14 +37,14 @@
}
},
"../../../deps/phoenix": {
"version": "1.6.11",
"version": "1.6.13",
"license": "MIT"
},
"../../../deps/phoenix_html": {
"version": "3.2.0"
},
"../../../deps/phoenix_live_view": {
"version": "0.17.11",
"version": "0.18.2",
"license": "MIT"
},
"local_modules/admin-one-bulma-dashboard": {
@@ -74,9 +74,9 @@
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz",
"integrity": "sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==",
"version": "7.19.1",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
"integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==",
"dev": true,
"engines": {
"node": ">=6.9.0"
@@ -181,21 +181,21 @@
}
},
"node_modules/@fontsource/fira-mono": {
"version": "4.5.9",
"resolved": "https://registry.npmjs.org/@fontsource/fira-mono/-/fira-mono-4.5.9.tgz",
"integrity": "sha512-DDhkRUjPHwPK/wB7GM/7LzGkcEC5JyTZM93YnFoP2Qfjffq3qX1asnXNqfglgZxXHXVmu3RI8OjRf87I97XCfA==",
"version": "4.5.10",
"resolved": "https://registry.npmjs.org/@fontsource/fira-mono/-/fira-mono-4.5.10.tgz",
"integrity": "sha512-bxUnRP8xptGRo8YXeY073DSpfK74XpSb0ZyRNpHV9WvLnJ7TwPOjZll8hTMin7zLC6iOp59pDZ8EQDj1gzgAQQ==",
"dev": true
},
"node_modules/@fontsource/fira-sans": {
"version": "4.5.9",
"resolved": "https://registry.npmjs.org/@fontsource/fira-sans/-/fira-sans-4.5.9.tgz",
"integrity": "sha512-wGh4mUHjjWzMwJMCo3z4GOYe9a2QKgvg1bge0gIg8Je6LKNID+/EFmcXuUDyk1KbUKHpWJIquVM9kFFyJyRY2A==",
"version": "4.5.10",
"resolved": "https://registry.npmjs.org/@fontsource/fira-sans/-/fira-sans-4.5.10.tgz",
"integrity": "sha512-4Edj+GA0LYSqfXOvdTwVGmCShT8Ycd8bKzdfzM302n+I6Hsg6h3gBkBeNgN19PhkcngDznZyHv3EkyrKqvMTGw==",
"dev": true
},
"node_modules/@fontsource/open-sans": {
"version": "4.5.11",
"resolved": "https://registry.npmjs.org/@fontsource/open-sans/-/open-sans-4.5.11.tgz",
"integrity": "sha512-nG0gmbx4pSr8wltdG/ZdlS6OrsMK40Wt6iyuLTKHEf0TQfzKRMlWaskZHdeuWCwS6WUgqHKMf9KSwGdxPfapOg==",
"version": "4.5.12",
"resolved": "https://registry.npmjs.org/@fontsource/open-sans/-/open-sans-4.5.12.tgz",
"integrity": "sha512-WKCexsVbOECJUSOgG7GnrUxe+3ds4Sa1yhsTjSnszI+0TaJvMZnDnn5YDKwA/KwLbkZqCaV3nvMTH97jJuxWNA==",
"dev": true
},
"node_modules/@fortawesome/fontawesome-free": {
@@ -1222,9 +1222,9 @@
}
},
"node_modules/esbuild-sass-plugin": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/esbuild-sass-plugin/-/esbuild-sass-plugin-2.3.2.tgz",
"integrity": "sha512-bNIV241S0vpy+F9U9oMbmlAD+GDzKLJp2+Z9rSRP8Rq8Nwmxh9roI0s3iB9d6Eii1A5WYgCK7HZeWPokw2rhSw==",
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/esbuild-sass-plugin/-/esbuild-sass-plugin-2.3.3.tgz",
"integrity": "sha512-EegGnUIsP5Y7FbwcGBD524F+cJaIAQU2LSOX9QtjgpqEmwnmfEh5f/aPJ1df5GxD3NgHQJspeRCV7spDHE3N6Q==",
"dev": true,
"dependencies": {
"esbuild": "^0.14.13",
@@ -2316,9 +2316,9 @@
}
},
"node_modules/node-sass": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/node-sass/-/node-sass-7.0.1.tgz",
"integrity": "sha512-uMy+Xt29NlqKCFdFRZyXKOTqGt+QaKHexv9STj2WeLottnlqZEEWx6Bj0MXNthmFRRdM/YwyNo/8Tr46TOM0jQ==",
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/node-sass/-/node-sass-7.0.3.tgz",
"integrity": "sha512-8MIlsY/4dXUkJDYht9pIWBhMil3uHmE8b/AdJPjmFn1nBx9X9BASzfzmsCy0uCCb8eqI3SYYzVPDswWqSx7gjw==",
"dev": true,
"hasInstallScript": true,
"dependencies": {
@@ -2334,7 +2334,7 @@
"node-gyp": "^8.4.1",
"npmlog": "^5.0.0",
"request": "^2.88.0",
"sass-graph": "4.0.0",
"sass-graph": "^4.0.1",
"stdout-stream": "^1.4.0",
"true-case-path": "^1.0.2"
},
@@ -2861,9 +2861,9 @@
"dev": true
},
"node_modules/sass": {
"version": "1.54.8",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.54.8.tgz",
"integrity": "sha512-ib4JhLRRgbg6QVy6bsv5uJxnJMTS2soVcCp9Y88Extyy13A8vV0G1fAwujOzmNkFQbR3LvedudAMbtuNRPbQww==",
"version": "1.55.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.55.0.tgz",
"integrity": "sha512-Pk+PMy7OGLs9WaxZGJMn7S96dvlyVBwwtToX895WmCpAOr5YiJYEUJfiJidMuKb613z2xNWcXCHEuOvjZbqC6A==",
"dev": true,
"dependencies": {
"chokidar": ">=3.0.0 <4.0.0",
@@ -2878,14 +2878,14 @@
}
},
"node_modules/sass-graph": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-4.0.0.tgz",
"integrity": "sha512-WSO/MfXqKH7/TS8RdkCX3lVkPFQzCgbqdGsmSKq6tlPU+GpGEsa/5aW18JqItnqh+lPtcjifqdZ/VmiILkKckQ==",
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-4.0.1.tgz",
"integrity": "sha512-5YCfmGBmxoIRYHnKK2AKzrAkCoQ8ozO+iumT8K4tXJXRVCPf+7s1/9KxTSW3Rbvf+7Y7b4FR3mWyLnQr3PHocA==",
"dev": true,
"dependencies": {
"glob": "^7.0.0",
"lodash": "^4.17.11",
"scss-tokenizer": "^0.3.0",
"scss-tokenizer": "^0.4.3",
"yargs": "^17.2.1"
},
"bin": {
@@ -2896,14 +2896,17 @@
}
},
"node_modules/sass-graph/node_modules/cliui": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
"integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
"dev": true,
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"strip-ansi": "^6.0.1",
"wrap-ansi": "^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/sass-graph/node_modules/wrap-ansi": {
@@ -2933,12 +2936,12 @@
}
},
"node_modules/sass-graph/node_modules/yargs": {
"version": "17.5.1",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz",
"integrity": "sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==",
"version": "17.6.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.0.tgz",
"integrity": "sha512-8H/wTDqlSwoSnScvV2N/JHfLWOKuh5MVla9hqLjK3nsfyy6Y4kDSYSvkU5YCUEPOSnRXfIyx3Sq+B/IWudTo4g==",
"dev": true,
"dependencies": {
"cliui": "^7.0.2",
"cliui": "^8.0.1",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
@@ -2960,19 +2963,19 @@
}
},
"node_modules/scss-tokenizer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.3.0.tgz",
"integrity": "sha512-14Zl9GcbBvOT9057ZKjpz5yPOyUWG2ojd9D5io28wHRYsOrs7U95Q+KNL87+32p8rc+LvDpbu/i9ZYjM9Q+FsQ==",
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.4.3.tgz",
"integrity": "sha512-raKLgf1LI5QMQnG+RxHz6oK0sL3x3I4FN2UDLqgLOGO8hodECNnNh5BXn7fAyBxrA8zVzdQizQ6XjNJQ+uBwMw==",
"dev": true,
"dependencies": {
"js-base64": "^2.4.3",
"source-map": "^0.7.1"
"js-base64": "^2.4.9",
"source-map": "^0.7.3"
}
},
"node_modules/semver": {
"version": "7.3.7",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
"integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
"version": "7.3.8",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
"dev": true,
"dependencies": {
"lru-cache": "^6.0.0"
@@ -3027,9 +3030,9 @@
}
},
"node_modules/socks": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/socks/-/socks-2.7.0.tgz",
"integrity": "sha512-scnOe9y4VuiNUULJN72GrM26BNOjVsfPXI+j+98PkyEfsIXroa5ofyjT+FzGvn/xHs73U2JtoBYAVx9Hl4quSA==",
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz",
"integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==",
"dev": true,
"dependencies": {
"ip": "^2.0.0",
@@ -3525,9 +3528,9 @@
}
},
"@babel/helper-validator-identifier": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz",
"integrity": "sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==",
"version": "7.19.1",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
"integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==",
"dev": true
},
"@babel/highlight": {
@@ -3607,21 +3610,21 @@
"optional": true
},
"@fontsource/fira-mono": {
"version": "4.5.9",
"resolved": "https://registry.npmjs.org/@fontsource/fira-mono/-/fira-mono-4.5.9.tgz",
"integrity": "sha512-DDhkRUjPHwPK/wB7GM/7LzGkcEC5JyTZM93YnFoP2Qfjffq3qX1asnXNqfglgZxXHXVmu3RI8OjRf87I97XCfA==",
"version": "4.5.10",
"resolved": "https://registry.npmjs.org/@fontsource/fira-mono/-/fira-mono-4.5.10.tgz",
"integrity": "sha512-bxUnRP8xptGRo8YXeY073DSpfK74XpSb0ZyRNpHV9WvLnJ7TwPOjZll8hTMin7zLC6iOp59pDZ8EQDj1gzgAQQ==",
"dev": true
},
"@fontsource/fira-sans": {
"version": "4.5.9",
"resolved": "https://registry.npmjs.org/@fontsource/fira-sans/-/fira-sans-4.5.9.tgz",
"integrity": "sha512-wGh4mUHjjWzMwJMCo3z4GOYe9a2QKgvg1bge0gIg8Je6LKNID+/EFmcXuUDyk1KbUKHpWJIquVM9kFFyJyRY2A==",
"version": "4.5.10",
"resolved": "https://registry.npmjs.org/@fontsource/fira-sans/-/fira-sans-4.5.10.tgz",
"integrity": "sha512-4Edj+GA0LYSqfXOvdTwVGmCShT8Ycd8bKzdfzM302n+I6Hsg6h3gBkBeNgN19PhkcngDznZyHv3EkyrKqvMTGw==",
"dev": true
},
"@fontsource/open-sans": {
"version": "4.5.11",
"resolved": "https://registry.npmjs.org/@fontsource/open-sans/-/open-sans-4.5.11.tgz",
"integrity": "sha512-nG0gmbx4pSr8wltdG/ZdlS6OrsMK40Wt6iyuLTKHEf0TQfzKRMlWaskZHdeuWCwS6WUgqHKMf9KSwGdxPfapOg==",
"version": "4.5.12",
"resolved": "https://registry.npmjs.org/@fontsource/open-sans/-/open-sans-4.5.12.tgz",
"integrity": "sha512-WKCexsVbOECJUSOgG7GnrUxe+3ds4Sa1yhsTjSnszI+0TaJvMZnDnn5YDKwA/KwLbkZqCaV3nvMTH97jJuxWNA==",
"dev": true
},
"@fortawesome/fontawesome-free": {
@@ -4349,9 +4352,9 @@
"optional": true
},
"esbuild-sass-plugin": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/esbuild-sass-plugin/-/esbuild-sass-plugin-2.3.2.tgz",
"integrity": "sha512-bNIV241S0vpy+F9U9oMbmlAD+GDzKLJp2+Z9rSRP8Rq8Nwmxh9roI0s3iB9d6Eii1A5WYgCK7HZeWPokw2rhSw==",
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/esbuild-sass-plugin/-/esbuild-sass-plugin-2.3.3.tgz",
"integrity": "sha512-EegGnUIsP5Y7FbwcGBD524F+cJaIAQU2LSOX9QtjgpqEmwnmfEh5f/aPJ1df5GxD3NgHQJspeRCV7spDHE3N6Q==",
"dev": true,
"requires": {
"esbuild": "^0.14.13",
@@ -5181,9 +5184,9 @@
}
},
"node-sass": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/node-sass/-/node-sass-7.0.1.tgz",
"integrity": "sha512-uMy+Xt29NlqKCFdFRZyXKOTqGt+QaKHexv9STj2WeLottnlqZEEWx6Bj0MXNthmFRRdM/YwyNo/8Tr46TOM0jQ==",
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/node-sass/-/node-sass-7.0.3.tgz",
"integrity": "sha512-8MIlsY/4dXUkJDYht9pIWBhMil3uHmE8b/AdJPjmFn1nBx9X9BASzfzmsCy0uCCb8eqI3SYYzVPDswWqSx7gjw==",
"dev": true,
"requires": {
"async-foreach": "^0.1.3",
@@ -5198,7 +5201,7 @@
"node-gyp": "^8.4.1",
"npmlog": "^5.0.0",
"request": "^2.88.0",
"sass-graph": "4.0.0",
"sass-graph": "^4.0.1",
"stdout-stream": "^1.4.0",
"true-case-path": "^1.0.2"
}
@@ -5579,9 +5582,9 @@
"dev": true
},
"sass": {
"version": "1.54.8",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.54.8.tgz",
"integrity": "sha512-ib4JhLRRgbg6QVy6bsv5uJxnJMTS2soVcCp9Y88Extyy13A8vV0G1fAwujOzmNkFQbR3LvedudAMbtuNRPbQww==",
"version": "1.55.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.55.0.tgz",
"integrity": "sha512-Pk+PMy7OGLs9WaxZGJMn7S96dvlyVBwwtToX895WmCpAOr5YiJYEUJfiJidMuKb613z2xNWcXCHEuOvjZbqC6A==",
"dev": true,
"requires": {
"chokidar": ">=3.0.0 <4.0.0",
@@ -5590,25 +5593,25 @@
}
},
"sass-graph": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-4.0.0.tgz",
"integrity": "sha512-WSO/MfXqKH7/TS8RdkCX3lVkPFQzCgbqdGsmSKq6tlPU+GpGEsa/5aW18JqItnqh+lPtcjifqdZ/VmiILkKckQ==",
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-4.0.1.tgz",
"integrity": "sha512-5YCfmGBmxoIRYHnKK2AKzrAkCoQ8ozO+iumT8K4tXJXRVCPf+7s1/9KxTSW3Rbvf+7Y7b4FR3mWyLnQr3PHocA==",
"dev": true,
"requires": {
"glob": "^7.0.0",
"lodash": "^4.17.11",
"scss-tokenizer": "^0.3.0",
"scss-tokenizer": "^0.4.3",
"yargs": "^17.2.1"
},
"dependencies": {
"cliui": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
"integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
"dev": true,
"requires": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"strip-ansi": "^6.0.1",
"wrap-ansi": "^7.0.0"
}
},
@@ -5630,12 +5633,12 @@
"dev": true
},
"yargs": {
"version": "17.5.1",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz",
"integrity": "sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==",
"version": "17.6.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.0.tgz",
"integrity": "sha512-8H/wTDqlSwoSnScvV2N/JHfLWOKuh5MVla9hqLjK3nsfyy6Y4kDSYSvkU5YCUEPOSnRXfIyx3Sq+B/IWudTo4g==",
"dev": true,
"requires": {
"cliui": "^7.0.2",
"cliui": "^8.0.1",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
@@ -5653,19 +5656,19 @@
}
},
"scss-tokenizer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.3.0.tgz",
"integrity": "sha512-14Zl9GcbBvOT9057ZKjpz5yPOyUWG2ojd9D5io28wHRYsOrs7U95Q+KNL87+32p8rc+LvDpbu/i9ZYjM9Q+FsQ==",
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.4.3.tgz",
"integrity": "sha512-raKLgf1LI5QMQnG+RxHz6oK0sL3x3I4FN2UDLqgLOGO8hodECNnNh5BXn7fAyBxrA8zVzdQizQ6XjNJQ+uBwMw==",
"dev": true,
"requires": {
"js-base64": "^2.4.3",
"source-map": "^0.7.1"
"js-base64": "^2.4.9",
"source-map": "^0.7.3"
}
},
"semver": {
"version": "7.3.7",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
"integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
"version": "7.3.8",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
"dev": true,
"requires": {
"lru-cache": "^6.0.0"
@@ -5704,9 +5707,9 @@
"dev": true
},
"socks": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/socks/-/socks-2.7.0.tgz",
"integrity": "sha512-scnOe9y4VuiNUULJN72GrM26BNOjVsfPXI+j+98PkyEfsIXroa5ofyjT+FzGvn/xHs73U2JtoBYAVx9Hl4quSA==",
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz",
"integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==",
"dev": true,
"requires": {
"ip": "^2.0.0",

View File

@@ -42,7 +42,8 @@ defmodule FzHttp.Application do
FzHttp.VpnSessionScheduler,
FzHttp.OIDC.StartProxy,
{DynamicSupervisor, name: FzHttp.RefresherSupervisor, strategy: :one_for_one},
FzHttp.OIDC.RefreshManager
FzHttp.OIDC.RefreshManager,
FzHttp.SAML.StartProxy
]
end
@@ -57,7 +58,8 @@ defmodule FzHttp.Application do
{FzHttp.OIDC.StartProxy, :test},
{Phoenix.PubSub, name: FzHttp.PubSub},
FzHttp.Notifications,
FzHttpWeb.Presence
FzHttpWeb.Presence,
FzHttp.SAML.StartProxy
]
end
end

View File

@@ -15,8 +15,8 @@ defmodule FzHttp.Configurations.Configuration do
field :allow_unprivileged_device_management, :boolean
field :allow_unprivileged_device_configuration, :boolean
field :openid_connect_providers, :map
field :saml_identity_providers, :map
field :disable_vpn_on_oidc_error, :boolean
field :auto_create_oidc_users, :boolean
timestamps(type: :utc_datetime_usec)
end
@@ -29,8 +29,8 @@ defmodule FzHttp.Configurations.Configuration do
:allow_unprivileged_device_management,
:allow_unprivileged_device_configuration,
:openid_connect_providers,
:disable_vpn_on_oidc_error,
:auto_create_oidc_users
:saml_identity_providers,
:disable_vpn_on_oidc_error
])
|> cast_embed(:logo)
end

View File

@@ -0,0 +1,47 @@
defmodule FzHttp.Conf.OIDCConfig do
@moduledoc """
OIDC Config virtual schema
"""
use Ecto.Schema
import Ecto.Changeset
@primary_key false
embedded_schema do
field :id, :string
field :label, :string
field :scope, :string, default: "openid email profile"
field :response_type, :string, default: "code"
field :client_id, :string
field :client_secret, :string
field :discovery_document_uri, :string
field :auto_create_users, :boolean
end
def changeset(data) do
%__MODULE__{}
|> cast(
data,
[
:id,
:label,
:scope,
:response_type,
:client_id,
:client_secret,
:discovery_document_uri,
:auto_create_users
]
)
|> validate_required([
:id,
:label,
:scope,
:response_type,
:client_id,
:client_secret,
:discovery_document_uri,
:auto_create_users
])
end
end

View File

@@ -0,0 +1,22 @@
defmodule FzHttp.Conf.SAMLConfig do
@moduledoc """
SAML Config virtual schema
"""
use Ecto.Schema
import Ecto.Changeset
@primary_key false
embedded_schema do
field :id, :string
field :label, :string
field :metadata, :string
field :auto_create_users, :boolean
end
def changeset(data) do
%__MODULE__{}
|> cast(data, [:id, :label, :metadata, :auto_create_users])
|> validate_required([:id, :label, :metadata, :auto_create_users])
end
end

View File

@@ -14,6 +14,12 @@ defmodule FzHttp.Configurations do
Repo.one!(Configuration)
end
def auto_create_users?(field, provider) do
get!(field)
|> Map.get(provider)
|> Map.get(:auto_create_users)
end
def change_configuration(%Configuration{} = config \\ get_configuration!()) do
Configuration.changeset(config, %{})
end

View File

@@ -10,6 +10,5 @@
</p>
<small>
If the link didn't work, please copy this link and open it in your browser.
<%= @link %>
If the link didn't work, please copy this link and open it in your browser. <%= @link %>
</small>

View File

@@ -0,0 +1,58 @@
defmodule FzHttp.SAML.StartProxy do
@moduledoc """
This proxy starts Samly.Provider with proper configs
(after `FzHttp.Conf.Cache` has started)
"""
alias FzHttp.Configurations, as: Conf
def child_spec(arg) do
%{id: __MODULE__, start: {__MODULE__, :start_link, [arg]}}
end
def start_link(_) do
samly = Samly.Provider.start_link()
Application.fetch_env!(:samly, Samly.Provider)
|> set_service_provider()
|> set_identity_providers()
|> refresh()
samly
end
def set_service_provider(samly_configs) do
keyfile = Application.fetch_env!(:fz_http, :saml_keyfile_path)
certfile = Application.fetch_env!(:fz_http, :saml_certfile_path)
Keyword.put(samly_configs, :service_providers, [
%{
id: "firezone",
entity_id: "urn:firezone.dev:firezone-app",
certfile: certfile,
keyfile: keyfile
}
])
end
def set_identity_providers(samly_configs, providers \\ Conf.get!(:saml_identity_providers)) do
external_url = Application.fetch_env!(:fz_http, :external_url)
identity_providers =
for {id, setting} <- providers do
%{
id: id,
sp_id: "firezone",
metadata: setting["metadata"],
base_url: Path.join(external_url, "/auth/saml")
}
end
Keyword.put(samly_configs, :identity_providers, identity_providers)
end
def refresh(samly_configs) do
Application.put_env(:samly, Samly.Provider, samly_configs)
Samly.Provider.refresh_providers()
end
end

View File

@@ -75,6 +75,9 @@ defmodule FzHttp.Telemetry do
telemetry_module().capture("ping", ping_data())
end
defp count(subject) when is_map(subject), do: count(Map.keys(subject))
defp count(subject) when is_list(subject), do: length(subject)
# How far back to count handshakes as an active device
@active_device_window 86_400
def ping_data do
@@ -83,12 +86,13 @@ defmodule FzHttp.Telemetry do
devices_active_within_24h: Devices.count_active_within(@active_device_window),
admin_count: Users.count(role: :admin),
user_count: Users.count(),
in_docker: in_docker?(),
device_count: Devices.count(),
max_devices_for_users: Devices.max_count_by_user_id(),
users_with_mfa: MFA.count_distinct_by_user_id(),
users_with_mfa_totp: MFA.count_distinct_totp_by_user_id(),
openid_providers: length(Conf.get!(:parsed_openid_connect_providers)),
auto_create_oidc_users: Conf.get!(:auto_create_oidc_users),
openid_providers: count(Conf.get!(:parsed_openid_connect_providers)),
saml_providers: count(Conf.get!(:saml_identity_providers)),
unprivileged_device_management: Conf.get!(:allow_unprivileged_device_management),
unprivileged_device_configuration: Conf.get!(:allow_unprivileged_device_configuration),
local_authentication: Conf.get!(:local_auth_enabled),
@@ -99,6 +103,10 @@ defmodule FzHttp.Telemetry do
]
end
defp in_docker? do
File.exists?("/.dockerenv")
end
defp common_fields do
[
distinct_id: conf(:telemetry_id),

View File

@@ -65,7 +65,10 @@ defmodule FzHttpWeb.Authentication do
end
def sign_out(conn) do
__MODULE__.Plug.sign_out(conn)
conn
|> Plug.Conn.delete_session("samly_assertion")
|> Plug.Conn.delete_session("samly_assertion_key")
|> __MODULE__.Plug.sign_out()
end
def get_current_user(%Plug.Conn{} = conn) do

View File

@@ -51,6 +51,16 @@ defmodule FzHttpWeb.AuthController do
end
end
def callback(conn, %{"provider" => "saml"}) do
key = {idp, _} = get_session(conn, "samly_assertion_key")
assertion = %Samly.Assertion{} = Samly.State.get_assertion(conn, key)
with {:ok, user} <-
UserFromAuth.find_or_create(:saml, idp, %{"email" => assertion.subject.name}) do
maybe_sign_in(conn, user, %{provider: idp})
end
end
def callback(conn, %{"provider" => provider_key, "state" => state} = params) do
openid_connect = Application.fetch_env!(:fz_http, :openid_connect)

View File

@@ -0,0 +1,35 @@
defmodule FzHttpWeb.DebugController do
@moduledoc """
Dev only:
/dev/session
/dev/samly
"""
use FzHttpWeb, :controller
def samly(conn, _params) do
resp = """
Samly.Provider state:
#{pretty(Application.get_env(:samly, Samly.Provider))}
Service Providers:
#{pretty(Application.get_env(:samly, :service_providers))}
Identity Providers:
#{pretty(Application.get_env(:samly, :identity_providers))}
Samly Session:
#{pretty(Samly.get_active_assertion(conn))}
"""
send_resp(conn, :ok, resp)
end
def session(conn, _params) do
send_resp(conn, :ok, pretty(get_session(conn)))
end
defp pretty(stuff) do
inspect(stuff, pretty: true)
end
end

View File

@@ -11,7 +11,8 @@ defmodule FzHttpWeb.RootController do
|> render(
"auth.html",
local_enabled: Conf.get!(:local_auth_enabled),
openid_connect_providers: Conf.get!(:parsed_openid_connect_providers)
openid_connect_providers: Conf.get!(:parsed_openid_connect_providers),
saml_identity_providers: Conf.get!(:saml_identity_providers)
)
end
end

View File

@@ -1,9 +1,10 @@
<%= render(FzHttpWeb.SharedView, "heading.html",
page_subtitle: @page_subtitle,
page_title: @page_title) %>
page_subtitle: @page_subtitle,
page_title: @page_title
) %>
<section class="section is-main-section">
<%= render FzHttpWeb.SharedView, "flash.html", assigns %>
<%= render(FzHttpWeb.SharedView, "flash.html", assigns) %>
<div class="block">
<table class="table is-bordered is-hoverable is-striped is-fullwidth">
@@ -15,23 +16,26 @@
</tr>
</thead>
<tbody>
<%= for connectivity_check <- @connectivity_checks do %>
<tr>
<td id={"connectivity_check-#{connectivity_check.id}"}
<%= for connectivity_check <- @connectivity_checks do %>
<tr>
<td
id={"connectivity_check-#{connectivity_check.id}"}
phx-hook="FormatTimestamp"
data-timestamp={connectivity_check.inserted_at}>
</td>
<td><%= connectivity_check.response_body %></td>
<td>
<span
data-tooltip={"HTTP Response Code: #{connectivity_check.response_code}"}
class={connectivity_check_span_class(connectivity_check.response_code)}>
<i class={connectivity_check_icon_class(connectivity_check.response_code)}></i>
</span>
</td>
</tr>
<% end %>
data-timestamp={connectivity_check.inserted_at}
>
</td>
<td><%= connectivity_check.response_body %></td>
<td>
<span
data-tooltip={"HTTP Response Code: #{connectivity_check.response_code}"}
class={connectivity_check_span_class(connectivity_check.response_code)}
>
<i class={connectivity_check_icon_class(connectivity_check.response_code)}></i>
</span>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>

View File

@@ -1,17 +1,22 @@
<%= render(FzHttpWeb.SharedView, "heading.html",
page_subtitle: @page_subtitle,
page_title: @page_title) %>
page_subtitle: @page_subtitle,
page_title: @page_title
) %>
<section class="section is-main-section">
<%= render FzHttpWeb.SharedView, "flash.html", assigns %>
<%= render(FzHttpWeb.SharedView, "flash.html", assigns) %>
<div class="block is-horizontally-scrollable">
<%= render FzHttpWeb.SharedView, "devices_table.html",
devices: @devices, show_user: true, socket: @socket %>
<%= render(FzHttpWeb.SharedView, "devices_table.html",
devices: @devices,
show_user: true,
socket: @socket
) %>
</div>
<p>
Devices can be added when viewing a User.
<%= live_redirect("Go to users ->", to: Routes.user_index_path(@socket, :index)) %>
Devices can be added when viewing a User. <%= live_redirect("Go to users ->",
to: Routes.user_index_path(@socket, :index)
) %>
</p>
</section>

View File

@@ -1,2 +1,2 @@
<%= render FzHttpWeb.SharedView, "heading.html", page_title: "Devices |> #{@page_title}" %>
<%= render FzHttpWeb.SharedView, "show_device.html", assigns %>
<%= render(FzHttpWeb.SharedView, "heading.html", page_title: "Devices |> #{@page_title}") %>
<%= render(FzHttpWeb.SharedView, "show_device.html", assigns) %>

View File

@@ -1,11 +1,11 @@
<div id="new-device-data"
data-public-key={@device && @device.public_key}
data-device-name={@device && @device.name}
data-config={@config}
phx-hook="RenderConfig">
<div
id="new-device-data"
data-public-key={@device && @device.public_key}
data-device-name={@device && @device.name}
data-config={@config}
phx-hook="RenderConfig"
>
<%= if @device && @config do %>
<%# Device Generated; display config %>
<div class="content">
<p>
@@ -22,7 +22,8 @@
</p>
<p>
<strong>NOTE:</strong> This configuration <strong>WILL NOT</strong>
<strong>NOTE:</strong>
This configuration <strong>WILL NOT</strong>
be viewable again. Please ensure you've downloaded the
configuration file or copied it somewhere safe
before closing this window.
@@ -44,60 +45,65 @@
</canvas>
</p>
<p>
<pre id="wg-conf-container"
class="is-hidden"><code id="wg-conf" class="language-toml"></code></pre>
<pre id="wg-conf-container" class="is-hidden"><code id="wg-conf" class="language-toml"></code></pre>
</p>
</div>
<% else %>
<%# Show form to generate device %>
<div>
<.form let={f} for={@changeset} id="create-device" phx-change="change" phx-target={@myself} phx-submit="save">
<%= hidden_input f, :public_key, id: "device-public-key", phx_hook: "GenerateKeyPair" %>
<%= hidden_input f, :preshared_key %>
<.form
:let={f}
for={@changeset}
id="create-device"
phx-change="change"
phx-target={@myself}
phx-submit="save"
>
<%= hidden_input(f, :public_key, id: "device-public-key", phx_hook: "GenerateKeyPair") %>
<%= hidden_input(f, :preshared_key) %>
<%= if @changeset.action do %>
<div class="notification is-danger">
<div class="flash-error">
<%= error_tag f, :base %>
<%= error_tag(f, :base) %>
</div>
</div>
<% end %>
<div class="field">
<%= label f, :name, class: "label" %>
<%= label(f, :name, class: "label") %>
<div class="control">
<%= text_input f, :name, class: "input #{input_error_class(f, :name)}" %>
<%= text_input(f, :name, class: "input #{input_error_class(f, :name)}") %>
</div>
<p class="help is-danger">
<%= error_tag f, :name %>
<%= error_tag(f, :name) %>
</p>
</div>
<div class="field">
<%= label f, :description, class: "label" %>
<%= label(f, :description, class: "label") %>
<div class="control">
<%= textarea(
f,
:description,
placeholder: "Enter an optional description for this device",
class: "pre-wrapped input #{input_error_class(f, :description)}") %>
f,
:description,
placeholder: "Enter an optional description for this device",
class: "pre-wrapped input #{input_error_class(f, :description)}"
) %>
</div>
<p class="help is-danger">
<%= error_tag f, :description %>
<%= error_tag(f, :description) %>
</p>
</div>
<%= if Conf.get!(:allow_unprivileged_device_configuration) do %>
<div class="field">
<%= label f, :use_site_allowed_ips, "Use Default Allowed IPs", class: "label" %>
<%= label(f, :use_site_allowed_ips, "Use Default Allowed IPs", class: "label") %>
<div class="control">
<label class="radio">
<%= radio_button f, :use_site_allowed_ips, true %>
Yes
<%= radio_button(f, :use_site_allowed_ips, true) %> Yes
</label>
<label class="radio">
<%= radio_button f, :use_site_allowed_ips, false %>
No
<%= radio_button(f, :use_site_allowed_ips, false) %> No
</label>
</div>
<p class="help">
@@ -106,27 +112,26 @@
</div>
<div class="field">
<%= label f, :allowed_ips, "Allowed IPs", class: "label" %>
<%= label(f, :allowed_ips, "Allowed IPs", class: "label") %>
<div class="control">
<%= textarea f, :allowed_ips,
class: "textarea #{input_error_class(f, :allowed_ips)}",
disabled: @use_site_allowed_ips %>
<%= textarea(f, :allowed_ips,
class: "textarea #{input_error_class(f, :allowed_ips)}",
disabled: @use_site_allowed_ips
) %>
</div>
<p class="help is-danger">
<%= error_tag f, :allowed_ips %>
<%= error_tag(f, :allowed_ips) %>
</p>
</div>
<div class="field">
<%= label f, :use_site_dns, "Use Default DNS Servers", class: "label" %>
<%= label(f, :use_site_dns, "Use Default DNS Servers", class: "label") %>
<div class="control">
<label class="radio">
<%= radio_button f, :use_site_dns, true %>
Yes
<%= radio_button(f, :use_site_dns, true) %> Yes
</label>
<label class="radio">
<%= radio_button f, :use_site_dns, false %>
No
<%= radio_button(f, :use_site_dns, false) %> No
</label>
</div>
<p class="help">
@@ -135,26 +140,26 @@
</div>
<div class="field">
<%= label f, :dns, "DNS Servers", class: "label" %>
<%= label(f, :dns, "DNS Servers", class: "label") %>
<div class="control">
<%= text_input f, :dns, class: "input #{input_error_class(f, :dns)}",
disabled: @use_site_dns %>
<%= text_input(f, :dns,
class: "input #{input_error_class(f, :dns)}",
disabled: @use_site_dns
) %>
</div>
<p class="help is-danger">
<%= error_tag f, :dns %>
<%= error_tag(f, :dns) %>
</p>
</div>
<div class="field">
<%= label f, :use_site_endpoint, "Use Default Endpoint", class: "label" %>
<%= label(f, :use_site_endpoint, "Use Default Endpoint", class: "label") %>
<div class="control">
<label class="radio">
<%= radio_button f, :use_site_endpoint, true %>
Yes
<%= radio_button(f, :use_site_endpoint, true) %> Yes
</label>
<label class="radio">
<%= radio_button f, :use_site_endpoint, false %>
No
<%= radio_button(f, :use_site_endpoint, false) %> No
</label>
</div>
<p class="help">
@@ -163,27 +168,27 @@
</div>
<div class="field">
<%= label f, :endpoint, "Server Endpoint", class: "label" %>
<%= label(f, :endpoint, "Server Endpoint", class: "label") %>
<p>The IP of the server this device should connect to.</p>
<div class="control">
<%= text_input f, :endpoint, class: "input #{input_error_class(f, :endpoint)}",
disabled: @use_site_endpoint %>
<%= text_input(f, :endpoint,
class: "input #{input_error_class(f, :endpoint)}",
disabled: @use_site_endpoint
) %>
</div>
<p class="help is-danger">
<%= error_tag f, :endpoint %>
<%= error_tag(f, :endpoint) %>
</p>
</div>
<div class="field">
<%= label f, :use_site_mtu, "Use Default MTU", class: "label" %>
<%= label(f, :use_site_mtu, "Use Default MTU", class: "label") %>
<div class="control">
<label class="radio">
<%= radio_button f, :use_site_mtu, true %>
Yes
<%= radio_button(f, :use_site_mtu, true) %> Yes
</label>
<label class="radio">
<%= radio_button f, :use_site_mtu, false %>
No
<%= radio_button(f, :use_site_mtu, false) %> No
</label>
</div>
<p class="help">
@@ -192,26 +197,29 @@
</div>
<div class="field">
<%= label f, :mtu, "Interface MTU", class: "label" %>
<%= label(f, :mtu, "Interface MTU", class: "label") %>
<p>The WireGuard interface MTU for this Device.</p>
<div class="contro">
<%= text_input f, :mtu, class: "input #{input_error_class(f, :mtu)}", disabled: @use_site_mtu %>
<%= text_input(f, :mtu,
class: "input #{input_error_class(f, :mtu)}",
disabled: @use_site_mtu
) %>
</div>
<p class="help is-danger">
<%= error_tag f, :mtu %>
<%= error_tag(f, :mtu) %>
</p>
</div>
<div class="field">
<%= label f, :use_site_persistent_keepalive, "Use Default Persistent Keepalive", class: "label" %>
<%= label(f, :use_site_persistent_keepalive, "Use Default Persistent Keepalive",
class: "label"
) %>
<div class="control">
<label class="radio">
<%= radio_button f, :use_site_persistent_keepalive, true %>
Yes
<%= radio_button(f, :use_site_persistent_keepalive, true) %> Yes
</label>
<label class="radio">
<%= radio_button f, :use_site_persistent_keepalive, false %>
No
<%= radio_button(f, :use_site_persistent_keepalive, false) %> No
</label>
</div>
<p class="help">
@@ -220,47 +228,52 @@
</div>
<div class="field">
<%= label f, :persistent_keepalive, "Persistent Keepalive", class: "label" %>
<%= label(f, :persistent_keepalive, "Persistent Keepalive", class: "label") %>
<p>
Interval for WireGuard
<a href="https://www.wireguard.com/quickstart/#nat-and-firewall-traversal-persistence">
Interval for WireGuard <a href="https://www.wireguard.com/quickstart/#nat-and-firewall-traversal-persistence">
persistent keepalive</a>. A value of 0 disables this. Leave this disabled
unless you're experiencing NAT or firewall traversal problems.
unless you're experiencing NAT or firewall traversal problems.
</p>
<div class="control">
<%= text_input f, :persistent_keepalive, class: "input #{input_error_class(f, :persistent_keepalive)}",
disabled: @use_site_persistent_keepalive %>
<%= text_input(f, :persistent_keepalive,
class: "input #{input_error_class(f, :persistent_keepalive)}",
disabled: @use_site_persistent_keepalive
) %>
</div>
<p class="help is-danger">
<%= error_tag f, :persistent_keepalive %>
<%= error_tag(f, :persistent_keepalive) %>
</p>
</div>
<div class="field">
<%= label f, :ipv4, "Tunnel IPv4 Address", class: "label" %>
<%= label(f, :ipv4, "Tunnel IPv4 Address", class: "label") %>
<div class="control">
<%= text_input(f,
:ipv4,
placeholder: "Leave blank to let Firezone assign an IPv4 address",
class: "input #{input_error_class(f, :ipv4)}") %>
<%= text_input(
f,
:ipv4,
placeholder: "Leave blank to let Firezone assign an IPv4 address",
class: "input #{input_error_class(f, :ipv4)}"
) %>
</div>
<p class="help is-danger">
<%= error_tag f, :ipv4 %>
<%= error_tag(f, :ipv4) %>
</p>
</div>
<div class="field">
<%= label f, :ipv6, "Tunnel IPv6 Address", class: "label" %>
<%= label(f, :ipv6, "Tunnel IPv6 Address", class: "label") %>
<div class="control">
<%= text_input(f,
:ipv6,
placeholder: "Leave blank to let Firezone assign an IPv6 address",
class: "input #{input_error_class(f, :ipv6)}") %>
<%= text_input(
f,
:ipv6,
placeholder: "Leave blank to let Firezone assign an IPv6 address",
class: "input #{input_error_class(f, :ipv6)}"
) %>
</div>
<p class="help is-danger">
<%= error_tag f, :ipv6 %>
<%= error_tag(f, :ipv6) %>
</p>
</div>
<% end %>

View File

@@ -1,17 +1,18 @@
<%= if @live_action == :new do %>
<%= live_modal(
FzHttpWeb.DeviceLive.NewFormComponent,
return_to: Routes.device_unprivileged_index_path(@socket, :index),
title: "Add Device",
current_user: @current_user,
target_user_id: @current_user.id,
id: "create-device-component",
form: "create-device",
button_text: "Generate Configuration") %>
FzHttpWeb.DeviceLive.NewFormComponent,
return_to: Routes.device_unprivileged_index_path(@socket, :index),
title: "Add Device",
current_user: @current_user,
target_user_id: @current_user.id,
id: "create-device-component",
form: "create-device",
button_text: "Generate Configuration"
) %>
<% end %>
<section class="section is-main-section">
<%= render FzHttpWeb.SharedView, "flash.html", assigns %>
<%= render(FzHttpWeb.SharedView, "flash.html", assigns) %>
<h4 class="title is-4"><%= @page_title %></h4>
<div class="block">
@@ -39,21 +40,27 @@
</tr>
</thead>
<tbody>
<%= for device <- @devices do %>
<tr>
<td>
<.link navigate={Routes.device_unprivileged_show_path(@socket, :show, device)}>
<%= device.name %>
</.link>
</td>
<td class="code">
<span><%= device.ipv4 %></span>
<span><%= device.ipv6 %></span>
</td>
<td class="code"><%= device.public_key %></td>
<td id={"device-#{device.id}-inserted-at"} data-timestamp={device.inserted_at} phx-hook="FormatTimestamp">…</td>
</tr>
<% end %>
<%= for device <- @devices do %>
<tr>
<td>
<.link navigate={Routes.device_unprivileged_show_path(@socket, :show, device)}>
<%= device.name %>
</.link>
</td>
<td class="code">
<span><%= device.ipv4 %></span>
<span><%= device.ipv6 %></span>
</td>
<td class="code"><%= device.public_key %></td>
<td
id={"device-#{device.id}-inserted-at"}
data-timestamp={device.inserted_at}
phx-hook="FormatTimestamp"
>
</td>
</tr>
<% end %>
</tbody>
</table>
<% else %>
@@ -65,7 +72,11 @@
<%= if FzHttpWeb.DeviceView.can_manage_devices?(@current_user) do %>
<div class="block">
<.link navigate={Routes.device_unprivileged_index_path(@socket, :new)} class="button">
<.link
replace={true}
patch={Routes.device_unprivileged_index_path(@socket, :new)}
class="button"
>
Add Device
</.link>
</div>
@@ -88,11 +99,16 @@
<% end %>
<p>
<strong>
<span id="vpn-expires" phx-hook="FormatTimestamp" data-timestamp={vpn_expires_at(@current_user)}>...</span>
<span
id="vpn-expires"
phx-hook="FormatTimestamp"
data-timestamp={vpn_expires_at(@current_user)}
>
...
</span>
</strong>
</p>
<%= link("Reauthenticate", to: Routes.auth_path(@socket, :delete), method: :delete) %>
to renew your VPN session.
<%= link("Reauthenticate", to: Routes.auth_path(@socket, :delete), method: :delete) %> to renew your VPN session.
<% else %>
Your VPN session is active indefinitely.
<% end %>

View File

@@ -3,4 +3,4 @@
&lt;- Back to devices
</.link>
</div>
<%= render FzHttpWeb.SharedView, "show_device.html", assigns %>
<%= render(FzHttpWeb.SharedView, "show_device.html", assigns) %>

View File

@@ -43,7 +43,7 @@ defmodule FzHttpWeb.ModalComponent do
@impl Phoenix.LiveComponent
def handle_event("close", _, socket) do
{:noreply, push_redirect(socket, to: socket.assigns.return_to)}
{:noreply, push_patch(socket, to: socket.assigns.return_to)}
end
@impl Phoenix.LiveComponent

View File

@@ -5,8 +5,8 @@
style: "height: 100%"
) do %>
<%= if @count == 0 do %>
<span class="icon has-text-grey-dark"><i class="mdi mdi-circle-outline"></i></span>
<span class="icon has-text-grey-dark"><i class="mdi mdi-circle-outline"></i></span>
<% else %>
<span class="icon has-text-danger"><i class="mdi mdi-circle"></i><%=@count%></span>
<span class="icon has-text-danger"><i class="mdi mdi-circle"></i><%= @count %></span>
<% end %>
<%end%>
<% end %>

View File

@@ -1,6 +1,7 @@
<%= render(FzHttpWeb.SharedView, "heading.html",
page_subtitle: @page_subtitle,
page_title: @page_title) %>
page_subtitle: @page_subtitle,
page_title: @page_title
) %>
<section class="section is-main-section">
<div class="block">
@@ -26,7 +27,8 @@
style="width: 15%"
id={"notification-time-#{index}"}
phx-hook="FormatTimestamp"
data-timestamp={notification.timestamp}>
data-timestamp={notification.timestamp}
>
...
</td>
<td class="has-text-centered is-vcentered"><%= notification.user %></td>
@@ -36,7 +38,8 @@
title="Dissmiss notification"
class="delete is-medium"
phx-click="clear_notification"
phx-value-index={index}>
phx-value-index={index}
>
</button>
</td>
</tr>

View File

@@ -4,10 +4,12 @@
<h4 class="title is-4">OIDC Connections</h4>
</div>
<div class="level-right">
<button class="button"
data-confirm="Refresh this users' tokens?"
phx-click="refresh"
phx-target={@myself}>
<button
class="button"
data-confirm="Refresh this users' tokens?"
phx-click="refresh"
phx-target={@myself}
>
<span class="icon is-small">
<i class="fas fa-redo"></i>
</span>
@@ -28,37 +30,43 @@
</tr>
</thead>
<tbody>
<%= for conn <- @connections do %>
<tr>
<td>
<%= conn.provider %>
</td>
<td id={"connection-#{conn.id}-refreshed-at"}
<%= for conn <- @connections do %>
<tr>
<td>
<%= conn.provider %>
</td>
<td
id={"connection-#{conn.id}-refreshed-at"}
data-timestamp={conn.refreshed_at}
phx-hook="FormatTimestamp">…</td>
<td>
<%= if match?(%{"error" => _}, conn.refresh_response) do %>
ERROR: <%= conn.refresh_response["error"] %>
<% else %>
OK
<% end %>
</td>
<td>
<button class="button is-warning"
phx-hook="FormatTimestamp"
>
</td>
<td>
<%= if match?(%{"error" => _}, conn.refresh_response) do %>
ERROR: <%= conn.refresh_response["error"] %>
<% else %>
OK
<% end %>
</td>
<td>
<button
class="button is-warning"
data-confirm={delete_warning(conn)}
phx-click="delete"
phx-value-id={conn.id}
phx-target={@myself}>
<span class="icon is-small">
<i class="fas fa-trash"></i>
</span>
<span>
Delete Connection
</span>
</button>
</td>
</tr>
<% end %>
phx-target={@myself}
>
<span class="icon is-small">
<i class="fas fa-trash"></i>
</span>
<span>
Delete Connection
</span>
</button>
</td>
</tr>
<% end %>
</tbody>
</table>
</section>

View File

@@ -1,6 +1,7 @@
<%= render(FzHttpWeb.SharedView, "heading.html",
page_subtitle: @page_subtitle,
page_title: @page_title) %>
page_subtitle: @page_subtitle,
page_title: @page_title
) %>
<section class="section is-main-section">
<div class="block">
@@ -13,23 +14,25 @@
</p>
</div>
<%= render FzHttpWeb.SharedView, "flash.html", assigns %>
<%= render(FzHttpWeb.SharedView, "flash.html", assigns) %>
<div class="tile is-ancestor">
<div class="tile is-parent">
<%= live_component(
FzHttpWeb.RuleLive.RuleListComponent,
title: "Allowlist",
header_icon: "mdi mdi-arrow-decision-outline",
id: :allowlist,
current_user: @current_user) %>
FzHttpWeb.RuleLive.RuleListComponent,
title: "Allowlist",
header_icon: "mdi mdi-arrow-decision-outline",
id: :allowlist,
current_user: @current_user
) %>
</div>
<div class="tile is-parent">
<%= live_component(
FzHttpWeb.RuleLive.RuleListComponent,
title: "Denylist",
header_icon: "mdi mdi-alert-octagon",
id: :denylist,
current_user: @current_user) %>
FzHttpWeb.RuleLive.RuleListComponent,
title: "Denylist",
header_icon: "mdi mdi-alert-octagon",
id: :denylist,
current_user: @current_user
) %>
</div>
</div>
</section>

View File

@@ -6,69 +6,86 @@
</p>
</header>
<div class="card-content">
<.form let={f} for={@changeset} id={"#{@action}-form"} phx-change="change" phx-target={@myself} phx-submit="add_rule">
<%= hidden_input f, :action, value: @action %>
<.form
:let={f}
for={@changeset}
id={"#{@action}-form"}
phx-change="change"
phx-target={@myself}
phx-submit="add_rule"
>
<%= hidden_input(f, :action, value: @action) %>
<div class="field">
<%= label f, :destination, class: "label" %>
<%= label(f, :destination, class: "label") %>
<div class="control">
<%= text_input f,
:destination,
class: "input #{input_error_class(f, :destination)}",
placeholder: "IPv4/6 CIDR range or address" %>
<%= text_input(
f,
:destination,
class: "input #{input_error_class(f, :destination)}",
placeholder: "IPv4/6 CIDR range or address"
) %>
</div>
<p class="help is-danger">
<%= error_tag f, :destination %>
<%= error_tag(f, :destination) %>
</p>
</div>
<div class="field">
<%= label f, "User", class: "label" %>
<%= label(f, "User", class: "label") %>
<div class="select">
<%= select f,
:user_id,
user_options(@users),
prompt: "All users" %>
<%= select(
f,
:user_id,
user_options(@users),
prompt: "All users"
) %>
</div>
</div>
<div class="field">
<%= label f, :port_type, class: "label" %>
<%= label(f, :port_type, class: "label") %>
<div class="select">
<%= select f,
:port_type,
port_type_options(),
prompt: "All protocols",
title: if(!@port_rules_supported, do: "Kernel 5.6.9 required for port-based rules."),
disabled: !@port_rules_supported %>
<%= select(
f,
:port_type,
port_type_options(),
prompt: "All protocols",
title: if(!@port_rules_supported, do: "Kernel 5.6.9 required for port-based rules."),
disabled: !@port_rules_supported
) %>
</div>
<p class="help is-danger">
<%= error_tag f, :port_type %>
<%= error_tag(f, :port_type) %>
</p>
</div>
<div class="field">
<%= label f, :port_range, class: "label" %>
<%= label(f, :port_range, class: "label") %>
<div class="control">
<%= text_input f, :port_range,
class: "input #{input_error_class(f, :port_range)}",
placeholder: "23000-24000",
disabled: @port_type == nil %>
<%= text_input(f, :port_range,
class: "input #{input_error_class(f, :port_range)}",
placeholder: "23000-24000",
disabled: @port_type == nil
) %>
</div>
<p class="help is-danger">
<%= error_tag f, :port_range %>
<%= error_tag(f, :port_range) %>
</p>
</div>
<div class="field">
<div class="control">
<%= submit "Add", class: "button is-primary" %>
<%= submit("Add", class: "button is-primary") %>
</div>
</div>
</.form>
<table id={"#{@action}-rules"} class="mt-4 table is-hoverable is-bordered is-striped is-fullwidth">
<%= if length(@rule_list) > 0 do %>
<table
id={"#{@action}-rules"}
class="mt-4 table is-hoverable is-bordered is-striped is-fullwidth"
>
<%= if length(@rule_list) > 0 do %>
<thead>
<tr>
<th>Destination</th>
@@ -80,7 +97,11 @@
</thead>
<tbody>
<%= for rule <- @rule_list do %>
<tr class={if rule.port_range != nil && !@port_rules_supported, do: "has-background-grey", else: ""}>
<tr class={
if rule.port_range != nil && !@port_rules_supported,
do: "has-background-grey",
else: ""
}>
<td class="has-text-left">
<dd class="code">
<%= rule.destination %>
@@ -93,14 +114,16 @@
<%= port_type_display(rule.port_type) %>
</td>
<td class="has-text-left">
<%= if rule.port_range != nil, do: rule.port_range %>
<%= if rule.port_range != nil, do: rule.port_range %>
</td>
<td class="has-text-right">
<a href="#"
<a
href="#"
phx-click="delete_rule"
phx-value-rule_id={rule.id}
phx-target={@myself}
disabled={!@port_rules_supported && rule.port_range != nil} >
disabled={!@port_rules_supported && rule.port_range != nil}
>
Delete
</a>
</td>
@@ -109,12 +132,12 @@
<!-- This can happen when moving the DB to an OS with an older Kernel or on the strange case of a
kernel downgrade. -->
<%= if !@port_rules_supported && Enum.any?(@rule_list, fn rule -> rule.port_range != nil end) do %>
<p class="help">
Port-based rules are only applied when Linux Kernel is 5.6.9 or greater
</p>
<p class="help">
Port-based rules are only applied when Linux Kernel is 5.6.9 or greater
</p>
<% end %>
</tbody>
<% end %>
<% end %>
</table>
</div>
</div>

View File

@@ -1,27 +1,31 @@
<%= if @live_action == :edit do %>
<%= live_modal(
FzHttpWeb.SettingLive.AccountFormComponent,
return_to: Routes.setting_account_path(@socket, :show),
title: "Edit Account",
id: "user-#{@current_user.id}",
user: @current_user,
action: @live_action,
form: "account-edit") %>
FzHttpWeb.SettingLive.AccountFormComponent,
return_to: Routes.setting_account_path(@socket, :show),
title: "Edit Account",
id: "user-#{@current_user.id}",
user: @current_user,
action: @live_action,
form: "account-edit"
) %>
<% end %>
<%= if @live_action == :register_mfa do %>
<.live_component module={FzHttpWeb.MFA.RegisterComponent}
id="register-mfa"
user={@current_user}
return_to={Routes.setting_account_path(@socket, :show)} />
<.live_component
module={FzHttpWeb.MFA.RegisterComponent}
id="register-mfa"
user={@current_user}
return_to={Routes.setting_account_path(@socket, :show)}
/>
<% end %>
<%= render(FzHttpWeb.SharedView, "heading.html",
page_subtitle: @page_subtitle,
page_title: @page_title) %>
page_subtitle: @page_subtitle,
page_title: @page_title
) %>
<section class="section is-main-section">
<%= render FzHttpWeb.SharedView, "flash.html", assigns %>
<%= render(FzHttpWeb.SharedView, "flash.html", assigns) %>
<div class="level">
<div class="level-left">
@@ -29,7 +33,7 @@
</div>
<div class="level-right">
<.link navigate={Routes.setting_account_path(@socket, :edit)} class="button">
<.link replace={true} patch={Routes.setting_account_path(@socket, :edit)} class="button">
<span class="icon is-small">
<i class="mdi mdi-pencil"></i>
</span>
@@ -38,7 +42,10 @@
</div>
</div>
<%= render FzHttpWeb.SharedView, "user_details.html", user: @current_user, rules_path: @rules_path %>
<%= render(FzHttpWeb.SharedView, "user_details.html",
user: @current_user,
rules_path: @rules_path
) %>
</section>
<section class="section is-main-section">
@@ -66,10 +73,20 @@
<tbody>
<%= for {meta, index} <- Enum.with_index(@metas) do %>
<tr>
<td data-timestamp={meta.online_at}
phx-hook="FormatTimestamp" id={"meta-#{index}-online-at"}>…</td>
<td data-timestamp={meta.last_signed_in_at}
phx-hook="FormatTimestamp" id={"meta-#{index}-last-signed-in-at"}>…</td>
<td
data-timestamp={meta.online_at}
phx-hook="FormatTimestamp"
id={"meta-#{index}-online-at"}
>
</td>
<td
data-timestamp={meta.last_signed_in_at}
phx-hook="FormatTimestamp"
id={"meta-#{index}-last-signed-in-at"}
>
</td>
<td><%= meta.remote_ip || "-" %></td>
<td><%= meta.user_agent %></td>
</tr>
@@ -92,14 +109,13 @@
<div class="block">
<%= if length(@methods) > 0 do %>
<%= render FzHttpWeb.SharedView, "mfa_methods_table.html", methods: @methods %>
<%= render(FzHttpWeb.SharedView, "mfa_methods_table.html", methods: @methods) %>
<% else %>
<div>No MFA methods added.</div>
<% end %>
</div>
<.link navigate={Routes.setting_account_path(@socket, :register_mfa)} class="button">
<.link replace={true} patch={Routes.setting_account_path(@socket, :register_mfa)} class="button">
<span class="icon is-small">
<i class="mdi mdi-plus"></i>
</span>
@@ -114,8 +130,7 @@
<div class="block">
<p>
<%= link("Click here", to: @subscribe_link, target: "_blank") %>
to register for product and security updates.
<%= link("Click here", to: @subscribe_link, target: "_blank") %> to register for product and security updates.
</p>
</div>
</section>
@@ -125,7 +140,6 @@
Danger Zone
</h4>
<%# This is purposefully a synchronous form in order to easily clear the session %>
<%= form_for @changeset, Routes.user_path(@socket, :delete), [id: "delete-account", method: :delete], fn _f -> %>
<%= submit(class: "button is-danger", data: [confirm: "Are you sure?"], disabled: !@allow_delete) do %>
<span class="icon is-small">

View File

@@ -1,46 +1,57 @@
<div>
<.form let={f} for={@changeset} id="account-edit" phx-target={@myself} phx-submit="save" x-autocomplete="off">
<.form
:let={f}
for={@changeset}
id="account-edit"
phx-target={@myself}
phx-submit="save"
x-autocomplete="off"
>
<div class="block">
<p>Change email or enter new password below.</p>
</div>
<div class="field">
<%= label f, :email, class: "label" %>
<%= label(f, :email, class: "label") %>
<div class="control">
<%= text_input f, :email, class: "input #{input_error_class(f, :email)}" %>
<%= text_input(f, :email, class: "input #{input_error_class(f, :email)}") %>
</div>
<p class="help is-danger">
<%= error_tag f, :email %>
<%= error_tag(f, :email) %>
</p>
</div>
<%= render FzHttpWeb.SharedView,
"password_field.html",
context: f,
field: :password,
autocomplete: "new-password",
label: "Password" %>
<%= render(
FzHttpWeb.SharedView,
"password_field.html",
context: f,
field: :password,
autocomplete: "new-password",
label: "Password"
) %>
<%= render FzHttpWeb.SharedView,
"password_field.html",
context: f,
field: :password_confirmation,
autocomplete: "new-password",
label: "Password Confirmation" %>
<%= render(
FzHttpWeb.SharedView,
"password_field.html",
context: f,
field: :password_confirmation,
autocomplete: "new-password",
label: "Password Confirmation"
) %>
<hr>
<hr />
<div class="block">
<p>Enter your current password to make these changes.</p>
</div>
<div class="field">
<%= label f, :current_password, class: "label" %>
<%= password_input f, :current_password, class: "input password" %>
<%= label(f, :current_password, class: "label") %>
<%= password_input(f, :current_password, class: "input password") %>
<p class="help is-danger">
<%= error_tag f, :current_password %>
<%= error_tag(f, :current_password) %>
</p>
</div>
</.form>

View File

@@ -1,9 +1,10 @@
<%= render(FzHttpWeb.SharedView, "heading.html",
page_subtitle: @page_subtitle,
page_title: @page_title) %>
page_subtitle: @page_subtitle,
page_title: @page_title
) %>
<section class="section is-main-section">
<%= render FzHttpWeb.SharedView, "flash.html", assigns %>
<%= render(FzHttpWeb.SharedView, "flash.html", assigns) %>
<h4 class="title is-4">Logo</h4>
@@ -15,16 +16,19 @@
</div>
<div class="block">
<div class="field">
<div class="control">
<%= for type <- Conf.logo_types do %>
<label class="radio">
<input type="radio" name="logo" value={type}
<label class="radio">
<input
type="radio"
name="logo"
value={type}
checked={type == @logo_type}
phx-click={JS.push("choose", value: %{type: type})}>
<span><%= type %></span>
</label>
phx-click={JS.push("choose", value: %{type: type})}
/>
<span><%= type %></span>
</label>
<% end %>
</div>
</div>
@@ -38,38 +42,43 @@
</div>
<%= if @logo_type == "Default" do %>
<form id="default-form" phx-submit="save">
<input type="hidden" name="default" value="true" >
<button class="button" type="submit">Save</button>
</form>
<form id="default-form" phx-submit="save">
<input type="hidden" name="default" value="true" />
<button class="button" type="submit">Save</button>
</form>
<% end %>
<%= if @logo_type == "URL" do %>
<form id="url-form" phx-submit="save">
<div class="field has-addons">
<div class="control">
<input class="input" type="url" name="url"
placeholder="https://my.logo.com/logo.jpg" required>
<form id="url-form" phx-submit="save">
<div class="field has-addons">
<div class="control">
<input
class="input"
type="url"
name="url"
placeholder="https://my.logo.com/logo.jpg"
required
/>
</div>
<div class="control">
<button class="button" type="submit">Save</button>
</div>
</div>
<div class="control">
<button class="button" type="submit">Save</button>
</div>
</div>
</form>
</form>
<% end %>
<%= if @logo_type == "Upload" do %>
<form id="upload-form" phx-submit="save" phx-change="validate">
<%= for entry <- @uploads.logo.entries do %>
<%= for err <- upload_errors(@uploads.logo, entry) do %>
<p class="notification is-warning"><%= error_to_string(err) %></p>
<form id="upload-form" phx-submit="save" phx-change="validate">
<%= for entry <- @uploads.logo.entries do %>
<%= for err <- upload_errors(@uploads.logo, entry) do %>
<p class="notification is-warning"><%= error_to_string(err) %></p>
<% end %>
<% end %>
<% end %>
<%= live_file_input @uploads.logo, class: "button", required: true %>
<%= live_file_input(@uploads.logo, class: "button", required: true) %>
<button class="button" type="submit">Upload</button>
</form>
<button class="button" type="submit">Upload</button>
</form>
<% end %>
</div>
</section>

View File

@@ -0,0 +1,171 @@
defmodule FzHttpWeb.SettingLive.OIDCFormComponent do
@moduledoc """
Form for OIDC configs
"""
use FzHttpWeb, :live_component
alias FzHttp.Configurations, as: Conf
def render(assigns) do
~H"""
<div>
<.form let={f} for={@changeset} autocomplete="off" id="oidc-form" phx-target={@myself} phx-submit="save">
<div class="field">
<%= label f, :id, "Config ID", class: "label" %>
<div class="control">
<%= text_input f, :id,
class: "input #{input_error_class(f, :id)}" %>
</div>
<p class="help is-danger">
<%= error_tag f, :id %>
</p>
</div>
<div class="field">
<%= label f, :label, class: "label" %>
<div class="control">
<%= text_input f, :label,
class: "input #{input_error_class(f, :label)}" %>
</div>
<p class="help is-danger">
<%= error_tag f, :label %>
</p>
</div>
<div class="field">
<%= label f, :scope, class: "label" %>
<div class="control">
<%= text_input f, :scope,
placeholder: "openid email profile",
class: "input #{input_error_class(f, :scope)}" %>
</div>
<p class="help is-danger">
<%= error_tag f, :scope %>
</p>
</div>
<div class="field">
<%= label f, :response_type, class: "label" %>
<div class="control">
<%= text_input f, :response_type,
disabled: true,
class: "input #{input_error_class(f, :response_type)}" %>
</div>
<p class="help is-danger">
<%= error_tag f, :response_type %>
</p>
</div>
<div class="field">
<%= label f, :client_id, "Client ID", class: "label" %>
<div class="control">
<%= text_input f, :client_id,
class: "input #{input_error_class(f, :client_id)}" %>
</div>
<p class="help is-danger">
<%= error_tag f, :client_id %>
</p>
</div>
<div class="field">
<%= label f, :client_secret, class: "label" %>
<div class="control">
<%= text_input f, :client_secret,
class: "input #{input_error_class(f, :client_secret)}" %>
</div>
<p class="help is-danger">
<%= error_tag f, :client_secret %>
</p>
</div>
<div class="field">
<%= label f, :discovery_document_uri, "Discovery Document URI", class: "label" %>
<div class="control">
<%= text_input f, :discovery_document_uri,
placeholder: "https://accounts.google.com/.well-known/openid-configuration",
class: "input #{input_error_class(f, :discovery_document_uri)}" %>
</div>
<p class="help is-danger">
<%= error_tag f, :discovery_document_uri %>
</p>
</div>
<div class="field">
<%= label f, :auto_create_users, class: "label" %>
<div class="control">
<%= checkbox f, :auto_create_users %>
</div>
<p class="help is-danger">
<%= error_tag f, :auto_create_users %>
</p>
</div>
</.form>
</div>
"""
end
def update(assigns, socket) do
changeset =
assigns.providers
|> Map.get(assigns.provider_id, %{})
|> Map.put("id", assigns.provider_id)
|> FzHttp.Conf.OIDCConfig.changeset()
{:ok,
socket
|> assign(assigns)
|> assign(:changeset, changeset)}
end
def handle_event("save", %{"oidc_config" => params}, socket) do
changeset =
params
|> FzHttp.Conf.OIDCConfig.changeset()
|> Map.put(:action, :validate)
update =
case changeset do
%{valid?: true} ->
changeset
|> Ecto.Changeset.apply_changes()
|> Map.from_struct()
|> Map.new(fn {k, v} -> {to_string(k), v} end)
|> then(fn data ->
{id, data} = Map.pop(data, "id")
%{
openid_connect_providers:
socket.assigns.providers
|> Map.delete(socket.assigns.provider_id)
|> Map.put(id, data)
}
end)
|> Conf.update_configuration()
_ ->
{:error, changeset}
end
case update do
{:ok, _config} ->
:ok = Supervisor.terminate_child(FzHttp.Supervisor, FzHttp.OIDC.StartProxy)
{:ok, _pid} = Supervisor.restart_child(FzHttp.Supervisor, FzHttp.OIDC.StartProxy)
{:noreply,
socket
|> put_flash(:info, "Updated successfully.")
|> redirect(to: socket.assigns.return_to)}
{:error, changeset} ->
{:noreply, assign(socket, :changeset, changeset)}
end
end
end

View File

@@ -0,0 +1,122 @@
defmodule FzHttpWeb.SettingLive.SAMLFormComponent do
@moduledoc """
Form for SAML configs
"""
use FzHttpWeb, :live_component
alias FzHttp.Configurations, as: Conf
def render(assigns) do
~H"""
<div>
<.form let={f} for={@changeset} autocomplete="off" id="saml-form" phx-target={@myself} phx-submit="save">
<div class="field">
<%= label f, :id, "Config ID", class: "label" %>
<div class="control">
<%= text_input f, :id,
class: "input #{input_error_class(f, :id)}" %>
</div>
<p class="help is-danger">
<%= error_tag f, :id %>
</p>
</div>
<div class="field">
<%= label f, :label, class: "label" %>
<div class="control">
<%= text_input f, :label,
class: "input #{input_error_class(f, :label)}" %>
</div>
<p class="help is-danger">
<%= error_tag f, :label %>
</p>
</div>
<div class="field">
<%= label f, :metadata, class: "label" %>
<div class="control">
<%= textarea f, :metadata,
rows: 8,
class: "textarea #{input_error_class(f, :metadata)}" %>
</div>
<p class="help is-danger">
<%= error_tag f, :metadata %>
</p>
</div>
<div class="field">
<%= label f, :auto_create_users, class: "label" %>
<div class="control">
<%= checkbox f, :auto_create_users %>
</div>
<p class="help is-danger">
<%= error_tag f, :auto_create_users %>
</p>
</div>
</.form>
</div>
"""
end
def update(assigns, socket) do
changeset =
assigns.providers
|> Map.get(assigns.provider_id, %{})
|> Map.put("id", assigns.provider_id)
|> FzHttp.Conf.SAMLConfig.changeset()
{:ok,
socket
|> assign(assigns)
|> assign(:changeset, changeset)}
end
def handle_event("save", %{"saml_config" => params}, socket) do
changeset =
params
|> FzHttp.Conf.SAMLConfig.changeset()
|> Map.put(:action, :validate)
update =
case changeset do
%{valid?: true} ->
changeset
|> Ecto.Changeset.apply_changes()
|> Map.from_struct()
|> Map.new(fn {k, v} -> {to_string(k), v} end)
|> then(fn data ->
{id, data} = Map.pop(data, "id")
%{
saml_identity_providers:
socket.assigns.providers
|> Map.delete(socket.assigns.provider_id)
|> Map.put(id, data)
}
end)
|> Conf.update_configuration()
_ ->
{:error, changeset}
end
case update do
{:ok, config} ->
Application.fetch_env!(:samly, Samly.Provider)
|> FzHttp.SAML.StartProxy.set_identity_providers(config.saml_identity_providers)
|> FzHttp.SAML.StartProxy.refresh()
{:noreply,
socket
|> put_flash(:info, "Updated successfully.")
|> redirect(to: socket.assigns.return_to)}
{:error, changeset} ->
{:noreply, assign(socket, :changeset, changeset)}
end
end
end

View File

@@ -1,24 +1,55 @@
<%= if @live_action == :edit_oidc do %>
<%= live_modal(
FzHttpWeb.SettingLive.OIDCFormComponent,
return_to: Routes.setting_security_path(@socket, :show),
title: "OIDC Config",
providers: @oidc_configs,
provider_id: @id,
id: "oidc-form-component",
form: "oidc-form"
) %>
<% end %>
<%= if @live_action == :edit_saml do %>
<%= live_modal(
FzHttpWeb.SettingLive.SAMLFormComponent,
return_to: Routes.setting_security_path(@socket, :show),
title: "SAML Config",
providers: @saml_configs,
provider_id: @id,
id: "saml-form-component",
form: "saml-form"
) %>
<% end %>
<%= render(FzHttpWeb.SharedView, "heading.html",
page_subtitle: @page_subtitle,
page_title: @page_title) %>
page_subtitle: @page_subtitle,
page_title: @page_title
) %>
<section class="section is-main-section">
<%= render FzHttpWeb.SharedView, "flash.html", assigns %>
<%= render(FzHttpWeb.SharedView, "flash.html", assigns) %>
<h4 class="title is-4">Authentication</h4>
<div class="block">
<.form let={f} for={@site_changeset} phx-change="change" phx-submit="save_site">
<.form :let={f} for={@site_changeset} phx-change="change" phx-submit="save_site">
<div class="field">
<%= label f, :vpn_session_duration, "Require Authentication For VPN Sessions", class: "label" %>
<%= label(f, :vpn_session_duration, "Require Authentication For VPN Sessions",
class: "label"
) %>
<div class="field has-addons">
<p class="control">
<span class="select">
<%= select f, :vpn_session_duration, @session_duration_options, class: "input" %>
<%= select(f, :vpn_session_duration, @session_duration_options, class: "input") %>
</span>
</p>
<p class="control">
<%= submit "Save", disabled: !@form_changed, phx_disable_with: "Saving...", class: "button is-primary" %>
<%= submit("Save",
disabled: !@form_changed,
phx_disable_with: "Saving...",
class: "button is-primary"
) %>
</p>
</div>
<p class="help">
@@ -30,7 +61,6 @@
</div>
<div class="block" title={@field_titles.local_auth_enabled}>
<strong>Local Auth</strong>
<div class="level">
@@ -39,10 +69,13 @@
</div>
<div class="level-right">
<label class="switch is-medium">
<input type="checkbox" phx-click="toggle"
phx-value-config="local_auth_enabled"
checked={Conf.get!(:local_auth_enabled)}
value={if(!Conf.get!(:local_auth_enabled), do: "on")} />
<input
type="checkbox"
phx-click="toggle"
phx-value-config="local_auth_enabled"
checked={Conf.get!(:local_auth_enabled)}
value={if(!Conf.get!(:local_auth_enabled), do: "on")}
/>
<span class="check"></span>
</label>
</div>
@@ -50,7 +83,6 @@
</div>
<div class="block" title={@field_titles.allow_unprivileged_device_management}>
<strong>Allow unprivileged device management</strong>
<div class="level">
@@ -59,10 +91,13 @@
</div>
<div class="level-right">
<label class="switch is-medium">
<input type="checkbox" phx-click="toggle"
phx-value-config="allow_unprivileged_device_management"
checked={Conf.get!(:allow_unprivileged_device_management)}
value={if(!Conf.get!(:allow_unprivileged_device_management), do: "on")} />
<input
type="checkbox"
phx-click="toggle"
phx-value-config="allow_unprivileged_device_management"
checked={Conf.get!(:allow_unprivileged_device_management)}
value={if(!Conf.get!(:allow_unprivileged_device_management), do: "on")}
/>
<span class="check"></span>
</label>
</div>
@@ -70,7 +105,6 @@
</div>
<div class="block" title={@field_titles.allow_unprivileged_device_configuration}>
<strong>Allow unprivileged device configuration</strong>
<div class="level">
@@ -81,10 +115,13 @@
</div>
<div class="level-right">
<label class="switch is-medium">
<input type="checkbox" phx-click="toggle"
phx-value-config="allow_unprivileged_device_configuration"
checked={Conf.get!(:allow_unprivileged_device_configuration)}
value={if(!Conf.get!(:allow_unprivileged_device_configuration), do: "on")} />
<input
type="checkbox"
phx-click="toggle"
phx-value-config="allow_unprivileged_device_configuration"
checked={Conf.get!(:allow_unprivileged_device_configuration)}
value={if(!Conf.get!(:allow_unprivileged_device_configuration), do: "on")}
/>
<span class="check"></span>
</label>
</div>
@@ -111,62 +148,108 @@
</div>
<div class="level-right">
<label class="switch is-medium">
<input type="checkbox" phx-click="toggle"
phx-value-config="disable_vpn_on_oidc_error"
checked={Conf.get!(:disable_vpn_on_oidc_error)}
value={if(!Conf.get!(:disable_vpn_on_oidc_error), do: "on")} />
<input
type="checkbox"
phx-click="toggle"
phx-value-config="disable_vpn_on_oidc_error"
checked={Conf.get!(:disable_vpn_on_oidc_error)}
value={if(!Conf.get!(:disable_vpn_on_oidc_error), do: "on")}
/>
<span class="check"></span>
</label>
</div>
</div>
</div>
<div class="block" title={@field_titles.auto_create_oidc_users}>
<strong>Auto create OIDC users</strong>
<label class="label">OpenID Connect providers configuration</label>
<div class="level">
<div class="level-left">
<p>Enable or disable auto creation of new users when logging in via OIDC for the first time.</p>
</div>
<div class="level-right">
<label class="switch is-medium">
<input type="checkbox" phx-click="toggle"
phx-value-config="auto_create_oidc_users"
checked={Conf.get!(:auto_create_oidc_users)}
value={if(!Conf.get!(:auto_create_oidc_users), do: "on")} />
<span class="check"></span>
</label>
</div>
</div>
</div>
<table class="table is-bordered is-hoverable is-striped is-fullwidth">
<thead>
<tr>
<th>Config ID</th>
<th>Label</th>
<th>Client ID</th>
<th>Discovery URI</th>
<th>Scope</th>
<th></th>
</tr>
</thead>
<tbody>
<%= for {k, v} <- @oidc_configs do %>
<tr>
<td><%= k %></td>
<td><%= v["label"] %></td>
<td><%= v["client_id"] %></td>
<td><%= v["discovery_document_uri"] %></td>
<td><%= v["scope"] %></td>
<td>
<%= live_patch(to: Routes.setting_security_path(@socket, :edit_oidc, k),
class: "button") do %>
Edit
<% end %>
<button
class="button is-danger"
phx-click="delete"
phx-value-key={k}
phx-value-type="oidc"
data-confirm={"Are you sure about deleting OIDC config #{k}?"}
>
Delete
</button>
</td>
</tr>
<% end %>
</tbody>
</table>
<div class="block" title={@field_titles.openid_connect_providers}>
<.form let={f} for={@config_changeset} id="oidc-config-form" phx-submit="save_oidc_config">
<div class="field">
<%= label f, :openid_connect_providers, "OpenID Connect providers configuration",
class: "label" %>
<p>
Enter a valid JSON string representing the OIDC configuration to apply.
Read more about the format of this field in
<a href="https://docs.firezone.dev/authenticate">
our documentation
</a>.
</p>
<%= live_patch(
to: Routes.setting_security_path(@socket, :edit_oidc, "new-provider-#{rand_string(4)}"),
class: "button mb-4") do %>
Add OpenID Connect Provider
<% end %>
<div class="control">
<%= textarea f,
:openid_connect_providers,
rows: 10,
placeholder: @oidc_placeholder,
class: "textarea #{input_error_class(@config_changeset, :openid_connect_providers)}" %>
</div>
<label class="label">SAML identity providers configuration</label>
<p class="help is-danger">
<%= error_tag @config_changeset, :openid_connect_providers %>
</p>
</div>
<table class="table is-bordered is-hoverable is-striped is-fullwidth">
<thead>
<tr>
<th>Config ID</th>
<th>label</th>
<th>Metadata</th>
<th></th>
</tr>
</thead>
<tbody>
<%= for {k, v} <- @saml_configs do %>
<tr>
<td><%= k %></td>
<td><%= v["label"] %></td>
<td>
<div class="line-clamp"><%= v["metadata"] %></div>
</td>
<td>
<%= live_patch(to: Routes.setting_security_path(@socket, :edit_saml, k),
class: "button") do %>
Edit
<% end %>
<button
class="button is-danger"
phx-click="delete"
phx-value-key={k}
phx-value-type="saml"
data-confirm={"Are you sure about deleting SAML config #{k}?"}
>
Delete
</button>
</td>
</tr>
<% end %>
</tbody>
</table>
<%= Phoenix.View.render(FzHttpWeb.SharedView, "submit_button.html", button_text: "Save Providers") %>
</.form>
</div>
<%= live_patch(
to: Routes.setting_security_path(@socket, :edit_saml, "new-provider-#{rand_string(4)}"),
class: "button mb-4") do %>
Add SAML Identity Provider
<% end %>
</section>

View File

@@ -5,25 +5,13 @@ defmodule FzHttpWeb.SettingLive.Security do
use FzHttpWeb, :live_view
import Ecto.Changeset
import FzCommon.FzCrypto, only: [rand_string: 1]
alias FzHttp.Configurations, as: Conf
alias FzHttp.{Sites, Sites.Site}
@page_title "Security Settings"
@page_subtitle "Configure security-related settings."
@oidc_placeholder """
{
"google": {
"discovery_document_uri": "https://accounts.google.com/.well-known/openid-configuration",
"client_id": "CLIENT_ID",
"client_secret": "CLIENT_SECRET",
"redirect_uri": "https://firezone.example.com/auth/oidc/google/callback/",
"response_type": "code",
"scope:" "openid email profile",
"label": "Google"
}
}
"""
@impl Phoenix.LiveView
def mount(_params, _session, socket) do
@@ -35,12 +23,18 @@ defmodule FzHttpWeb.SettingLive.Security do
|> assign(:session_duration_options, session_duration_options())
|> assign(:site_changeset, site_changeset())
|> assign(:config_changeset, config_changeset)
|> assign(:oidc_configs, config_changeset.data.openid_connect_providers || %{})
|> assign(:saml_configs, config_changeset.data.saml_identity_providers || %{})
|> assign(:field_titles, field_titles(config_changeset))
|> assign(:oidc_placeholder, @oidc_placeholder)
|> assign(:page_subtitle, @page_subtitle)
|> assign(:page_title, @page_title)}
end
@impl Phoenix.LiveView
def handle_params(params, _uri, socket) do
{:noreply, assign(socket, :id, params["id"])}
end
@impl Phoenix.LiveView
def handle_event("change", _params, socket) do
{:noreply,
@@ -77,31 +71,21 @@ defmodule FzHttpWeb.SettingLive.Security do
{:noreply, socket}
end
@impl Phoenix.LiveView
def handle_event(
"save_oidc_config",
%{"configuration" => %{"openid_connect_providers" => config}},
socket
) do
with {:ok, json} <- Jason.decode(config),
{:ok, conf} <- Conf.update_configuration(%{openid_connect_providers: json}) do
:ok = Supervisor.terminate_child(FzHttp.Supervisor, FzHttp.OIDC.StartProxy)
{:ok, _pid} = Supervisor.restart_child(FzHttp.Supervisor, FzHttp.OIDC.StartProxy)
{:noreply, assign(socket, :config_changeset, Conf.change_configuration(conf))}
else
{:error, %Jason.DecodeError{}} ->
{:noreply,
assign(
socket,
:config_changeset,
Conf.change_configuration()
|> put_change(:openid_connect_providers, config)
|> add_error(:openid_connect_providers, "Invalid JSON configuration")
)}
@types %{"oidc" => :openid_connect_providers, "saml" => :saml_identity_providers}
{:error, changeset} ->
{:noreply, assign(socket, :config_changeset, changeset)}
end
@impl Phoenix.LiveView
def handle_event("delete", %{"type" => type, "key" => key}, socket) do
field_key = Map.fetch!(@types, type)
providers =
get_in(socket.assigns.config_changeset, [Access.key!(:data), Access.key!(field_key)])
{:ok, conf} = Conf.update_configuration(%{field_key => Map.delete(providers, key)})
{:noreply,
socket
|> assign(String.to_existing_atom("#{type}_configs"), get_in(conf, [Access.key!(field_key)]))
|> assign(:config_changeset, change(conf))}
end
@hour 3_600
@@ -129,7 +113,6 @@ defmodule FzHttpWeb.SettingLive.Security do
disable_vpn_on_oidc_error
allow_unprivileged_device_management
allow_unprivileged_device_configuration
auto_create_oidc_users
openid_connect_providers
)a
@override_title """

View File

@@ -1,16 +1,18 @@
<%= render(FzHttpWeb.SharedView, "heading.html",
page_subtitle: @page_subtitle,
page_title: @page_title) %>
page_subtitle: @page_subtitle,
page_title: @page_title
) %>
<section class="section is-main-section">
<%= render FzHttpWeb.SharedView, "flash.html", assigns %>
<%= render(FzHttpWeb.SharedView, "flash.html", assigns) %>
<h4 class="title is-4">Site Defaults</h4>
<div class="block">
<%= live_component(
FzHttpWeb.SettingLive.SiteFormComponent,
changeset: @changeset,
id: :site_form_component) %>
FzHttpWeb.SettingLive.SiteFormComponent,
changeset: @changeset,
id: :site_form_component
) %>
</div>
</section>

View File

@@ -1,38 +1,43 @@
<div class="block">
<.form let={f} for={@changeset} id={@id} phx-target={@myself} phx-submit="save">
<.form :let={f} for={@changeset} id={@id} phx-target={@myself} phx-submit="save">
<div class="field">
<%= label f, :allowed_ips, "Allowed IPs", class: "label" %>
<%= label(f, :allowed_ips, "Allowed IPs", class: "label") %>
<div class="control">
<%= textarea f,
:allowed_ips,
placeholder: Sites.default(:allowed_ips),
class: "textarea #{input_error_class(f, :allowed_ips)}" %>
<%= textarea(
f,
:allowed_ips,
placeholder: Sites.default(:allowed_ips),
class: "textarea #{input_error_class(f, :allowed_ips)}"
) %>
</div>
<p class="help is-danger">
<%= error_tag f, :allowed_ips %>
<%= error_tag(f, :allowed_ips) %>
</p>
<p class="help">
Configures the default AllowedIPs setting for devices.
AllowedIPs determines which destination IPs get routed through
Firezone. Specify a comma-separated list of IPs or CIDRs here to achieve split tunneling, or use
<code>0.0.0.0/0, ::/0</code> to route all device traffic through this Firezone server.
<code>0.0.0.0/0, ::/0</code>
to route all device traffic through this Firezone server.
</p>
</div>
<div class="field">
<%= label f, :dns, "DNS Servers", class: "label" %>
<%= label(f, :dns, "DNS Servers", class: "label") %>
<div class="control">
<%= text_input f,
:dns,
placeholder: Sites.default(:dns),
class: "input #{input_error_class(f, :dns)}" %>
<%= text_input(
f,
:dns,
placeholder: Sites.default(:dns),
class: "input #{input_error_class(f, :dns)}"
) %>
</div>
<p class="help is-danger">
<%= error_tag f, :dns %>
<%= error_tag(f, :dns) %>
</p>
<p class="help">
Comma-separated list of DNS servers to use for devices.
@@ -42,16 +47,18 @@
</div>
<div class="field">
<%= label f, :endpoint, "Endpoint", class: "label" %>
<%= label(f, :endpoint, "Endpoint", class: "label") %>
<div class="control">
<%= text_input f,
:endpoint,
placeholder: Sites.default(:endpoint),
class: "input #{input_error_class(f, :endpoint)}" %>
<%= text_input(
f,
:endpoint,
placeholder: Sites.default(:endpoint),
class: "input #{input_error_class(f, :endpoint)}"
) %>
</div>
<p class="help is-danger">
<%= error_tag f, :endpoint %>
<%= error_tag(f, :endpoint) %>
</p>
<p class="help">
IPv4 or IPv6 address that devices will be configured to connect
@@ -60,15 +67,17 @@
</div>
<div class="field">
<%= label f, :persistent_keepalive, "Persistent Keepalive", class: "label" %>
<%= label(f, :persistent_keepalive, "Persistent Keepalive", class: "label") %>
<div class="control">
<%= text_input f,
:persistent_keepalive,
placeholder: Sites.default(:persistent_keepalive),
class: "input #{input_error_class(f, :persistent_keepalive)}" %>
<%= text_input(
f,
:persistent_keepalive,
placeholder: Sites.default(:persistent_keepalive),
class: "input #{input_error_class(f, :persistent_keepalive)}"
) %>
<p class="help is-danger">
<%= error_tag f, :persistent_keepalive %>
<%= error_tag(f, :persistent_keepalive) %>
</p>
<p class="help">
Interval in seconds to send persistent keepalive packets. Most users won't need to change
@@ -78,16 +87,18 @@
</div>
<div class="field">
<%= label f, :mtu, "MTU", class: "label" %>
<%= label(f, :mtu, "MTU", class: "label") %>
<div class="control">
<%= text_input f,
:mtu,
placeholder: Sites.default(:mtu),
class: "input #{input_error_class(f, :mtu)}" %>
<%= text_input(
f,
:mtu,
placeholder: Sites.default(:mtu),
class: "input #{input_error_class(f, :mtu)}"
) %>
</div>
<p class="help is-danger">
<%= error_tag f, :mtu %>
<%= error_tag(f, :mtu) %>
</p>
<p class="help">
WireGuard interface MTU for devices. Defaults to what's set in the configuration file.

View File

@@ -1,28 +1,31 @@
<%= if @live_action == :change_password do %>
<%= live_modal(
FzHttpWeb.SettingLive.Unprivileged.AccountFormComponent,
return_to: Routes.setting_unprivileged_account_path(@socket, :show),
title: "Change password",
id: "account-form-component",
current_user: @current_user,
form: "account-edit") %>
FzHttpWeb.SettingLive.Unprivileged.AccountFormComponent,
return_to: Routes.setting_unprivileged_account_path(@socket, :show),
title: "Change password",
id: "account-form-component",
current_user: @current_user,
form: "account-edit"
) %>
<% end %>
<%= if @live_action == :register_mfa do %>
<.live_component module={FzHttpWeb.MFA.RegisterComponent}
id="register-mfa"
user={@current_user}
return_to={Routes.setting_unprivileged_account_path(@socket, :show)} />
<.live_component
module={FzHttpWeb.MFA.RegisterComponent}
id="register-mfa"
user={@current_user}
return_to={Routes.setting_unprivileged_account_path(@socket, :show)}
/>
<% end %>
<.link navigate={Routes.device_unprivileged_index_path(@socket, :index)}>
&lt;- Back to devices
</.link>
<%= render FzHttpWeb.SharedView, "heading.html", page_title: @page_title %>
<%= render(FzHttpWeb.SharedView, "heading.html", page_title: @page_title) %>
<section class="section is-main-section">
<%= render FzHttpWeb.SharedView, "flash.html", assigns %>
<%= render(FzHttpWeb.SharedView, "flash.html", assigns) %>
<div class="block">
<%= @page_subtitle %>
@@ -35,7 +38,11 @@
<div class="level-right">
<%= if @local_auth_enabled do %>
<.link navigate={Routes.setting_unprivileged_account_path(@socket, :change_password)} class="button">
<.link
replace={true}
patch={Routes.setting_unprivileged_account_path(@socket, :change_password)}
class="button"
>
<span class="icon is-small">
<i class="mdi mdi-pencil"></i>
</span>
@@ -45,7 +52,7 @@
</div>
</div>
<%= render FzHttpWeb.SharedView, "user_details.html", user: @current_user, rules_path: nil %>
<%= render(FzHttpWeb.SharedView, "user_details.html", user: @current_user, rules_path: nil) %>
</section>
<section class="section is-main-section">
@@ -73,10 +80,20 @@
<tbody>
<%= for {meta, index} <- Enum.with_index(@metas) do %>
<tr>
<td data-timestamp={meta.online_at}
phx-hook="FormatTimestamp" id={"meta-#{index}-online-at"}>…</td>
<td data-timestamp={meta.last_signed_in_at}
phx-hook="FormatTimestamp" id={"meta-#{index}-last-signed-in-at"}>…</td>
<td
data-timestamp={meta.online_at}
phx-hook="FormatTimestamp"
id={"meta-#{index}-online-at"}
>
</td>
<td
data-timestamp={meta.last_signed_in_at}
phx-hook="FormatTimestamp"
id={"meta-#{index}-last-signed-in-at"}
>
</td>
<td><%= meta.remote_ip || "-" %></td>
<td><%= meta.user_agent %></td>
</tr>
@@ -99,13 +116,17 @@
<div class="block">
<%= if length(@methods) > 0 do %>
<%= render FzHttpWeb.SharedView, "mfa_methods_table.html", methods: @methods %>
<%= render(FzHttpWeb.SharedView, "mfa_methods_table.html", methods: @methods) %>
<% else %>
<div>No MFA methods added.</div>
<% end %>
</div>
<.link navigate={Routes.setting_unprivileged_account_path(@socket, :register_mfa)} class="button">
<.link
replace={true}
patch={Routes.setting_unprivileged_account_path(@socket, :register_mfa)}
class="button"
>
<span class="icon is-small">
<i class="mdi mdi-plus"></i>
</span>

View File

@@ -1,22 +1,32 @@
<div>
<.form let={f} for={@changeset} x-autocomplete="off" id="account-edit" phx-target={@myself} phx-submit="save">
<.form
:let={f}
for={@changeset}
x-autocomplete="off"
id="account-edit"
phx-target={@myself}
phx-submit="save"
>
<div class="block">
<p>Enter new password below.</p>
</div>
<%= render FzHttpWeb.SharedView,
"password_field.html",
context: f,
field: :password,
autocomplete: "new-password",
label: "Password" %>
<%= render FzHttpWeb.SharedView,
"password_field.html",
context: f,
field: :password_confirmation,
autocomplete: "new-password",
label: "Password Confirmation" %>
<%= render(
FzHttpWeb.SharedView,
"password_field.html",
context: f,
field: :password,
autocomplete: "new-password",
label: "Password"
) %>
<%= render(
FzHttpWeb.SharedView,
"password_field.html",
context: f,
field: :password_confirmation,
autocomplete: "new-password",
label: "Password Confirmation"
) %>
</.form>
</div>

View File

@@ -1,5 +1,12 @@
<div>
<.form let={f} for={@changeset} x-autocomplete="off" id="user-form" phx-target={@myself} phx-submit="save">
<.form
:let={f}
for={@changeset}
x-autocomplete="off"
id="user-form"
phx-target={@myself}
phx-submit="save"
>
<%= if @action == :edit do %>
<div class="block">
<p>Change user email or enter new password below.</p>
@@ -7,30 +14,35 @@
<% end %>
<div class="field">
<%= label f, :email, class: "label" %>
<%= label(f, :email, class: "label") %>
<div class="control">
<%= text_input f, :email,
class: "input #{input_error_class(f, :email)}",
disabled: @user && @user.id == @current_user.id %>
<%= text_input(f, :email,
class: "input #{input_error_class(f, :email)}",
disabled: @user && @user.id == @current_user.id
) %>
</div>
<p class="help is-danger">
<%= error_tag f, :email %>
<%= error_tag(f, :email) %>
</p>
</div>
<%= render FzHttpWeb.SharedView,
"password_field.html",
context: f,
field: :password,
autocomplete: "new-password",
label: "New Password" %>
<%= render(
FzHttpWeb.SharedView,
"password_field.html",
context: f,
field: :password,
autocomplete: "new-password",
label: "New Password"
) %>
<%= render FzHttpWeb.SharedView,
"password_field.html",
context: f,
field: :password_confirmation,
autocomplete: "new-password",
label: "New Password Confirmation" %>
<%= render(
FzHttpWeb.SharedView,
"password_field.html",
context: f,
field: :password_confirmation,
autocomplete: "new-password",
label: "New Password Confirmation"
) %>
</.form>
</div>

View File

@@ -7,13 +7,14 @@
user: nil,
current_user: @current_user,
action: @live_action,
form: "user-form") %>
form: "user-form"
) %>
<% end %>
<%= render FzHttpWeb.SharedView, "heading.html", page_title: @page_title %>
<%= render(FzHttpWeb.SharedView, "heading.html", page_title: @page_title) %>
<section class="section is-main-section">
<%= render FzHttpWeb.SharedView, "flash.html", assigns %>
<%= render(FzHttpWeb.SharedView, "flash.html", assigns) %>
<div class="block">
<table class="table is-hoverable is-bordered is-striped is-fullwidth">
@@ -29,43 +30,49 @@
</tr>
</thead>
<tbody>
<%= for user <- @users do %>
<tr>
<td>
<.link navigate={Routes.user_show_path(@socket, :show, user)}>
<%= user.email %>
</.link>
</td>
<td id={"user-#{user.id}-device-count"}><%= user.device_count %></td>
<td id={"user-#{user.id}-vpn-status"} class="has-text-centered">
<FzHttpWeb.UserLive.VPNStatusComponent.status
user={user}
expired={vpn_expired?(user)}
/>
</td>
<td id={"user-#{user.id}-timestamp"}
<%= for user <- @users do %>
<tr>
<td>
<.link navigate={Routes.user_show_path(@socket, :show, user)}>
<%= user.email %>
</.link>
</td>
<td id={"user-#{user.id}-device-count"}><%= user.device_count %></td>
<td id={"user-#{user.id}-vpn-status"} class="has-text-centered">
<FzHttpWeb.UserLive.VPNStatusComponent.status
user={user}
expired={vpn_expired?(user)}
/>
</td>
<td
id={"user-#{user.id}-timestamp"}
data-timestamp={user.last_signed_in_at}
phx-hook="FormatTimestamp">
</td>
<td><%= user.last_signed_in_method %></td>
<td id={"user-#{user.id}-inserted-at"}
phx-hook="FormatTimestamp"
>
</td>
<td><%= user.last_signed_in_method %></td>
<td
id={"user-#{user.id}-inserted-at"}
data-timestamp={user.inserted_at}
phx-hook="FormatTimestamp">
</td>
<td id={"user-#{user.id}-updated-at"}
phx-hook="FormatTimestamp"
>
</td>
<td
id={"user-#{user.id}-updated-at"}
data-timestamp={user.updated_at}
phx-hook="FormatTimestamp">
</td>
</tr>
<% end %>
phx-hook="FormatTimestamp"
>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
<.link navigate={Routes.user_index_path(@socket, :new)} class="button">
<.link replace={true} patch={Routes.user_index_path(@socket, :new)} class="button">
Add User
</.link>
</section>

View File

@@ -1,37 +1,39 @@
<%= if @live_action == :edit do %>
<%= live_modal(
FzHttpWeb.UserLive.FormComponent,
return_to: Routes.user_show_path(@socket, :show, @user),
title: "Edit #{@user.email}",
id: "user-form-component",
user: @user,
current_user: @current_user,
action: @live_action,
form: "user-form") %>
FzHttpWeb.UserLive.FormComponent,
return_to: Routes.user_show_path(@socket, :show, @user),
title: "Edit #{@user.email}",
id: "user-form-component",
user: @user,
current_user: @current_user,
action: @live_action,
form: "user-form"
) %>
<% end %>
<%= if @live_action == :new_device do %>
<%= live_modal(
FzHttpWeb.DeviceLive.NewFormComponent,
return_to: Routes.user_show_path(@socket, :show, @user.id),
title: "Add Device",
current_user: @current_user,
target_user_id: @user.id,
id: "create-device-component",
form: "create-device",
button_text: "Generate Configuration") %>
FzHttpWeb.DeviceLive.NewFormComponent,
return_to: Routes.user_show_path(@socket, :show, @user.id),
title: "Add Device",
current_user: @current_user,
target_user_id: @user.id,
id: "create-device-component",
form: "create-device",
button_text: "Generate Configuration"
) %>
<% end %>
<%= render FzHttpWeb.SharedView, "heading.html", page_title: "Users |> #{@user.email}" %>
<%= render(FzHttpWeb.SharedView, "heading.html", page_title: "Users |> #{@user.email}") %>
<section class="section is-main-section">
<%= render FzHttpWeb.SharedView, "flash.html", assigns %>
<%= render(FzHttpWeb.SharedView, "flash.html", assigns) %>
<div class="level">
<div class="level-left">
<h4 class="title is-4">Details</h4>
</div>
<div class="level-right">
<.link navigate={Routes.user_show_path(@socket, :edit, @user)} class="button">
<.link patch={Routes.user_show_path(@socket, :edit, @user)} replace={true} class="button">
<span class="icon is-small">
<i class="mdi mdi-pencil"></i>
</span>
@@ -40,14 +42,16 @@
</div>
</div>
<%= render FzHttpWeb.SharedView, "user_details.html", user: @user, rules_path: @rules_path %>
<%= render(FzHttpWeb.SharedView, "user_details.html", user: @user, rules_path: @rules_path) %>
</section>
<%= if length(@connections) > 0 do %>
<.live_component id="connections-table"
module={FzHttpWeb.OIDCLive.ConnectionsTableComponent}
connections={@connections}
user={@user} />
<.live_component
id="connections-table"
module={FzHttpWeb.OIDCLive.ConnectionsTableComponent}
connections={@connections}
user={@user}
/>
<% end %>
<section class="section is-main-section">
@@ -55,17 +59,22 @@
<div class="block is-horizontally-scrollable">
<%= if length(@devices) > 0 do %>
<%= render FzHttpWeb.SharedView, "devices_table.html",
devices: @devices, show_user: false, socket: @socket %>
<%= render(FzHttpWeb.SharedView, "devices_table.html",
devices: @devices,
show_user: false,
socket: @socket
) %>
<% else %>
No devices.
No devices.
<% end %>
</div>
<.link
navigate={Routes.user_show_path(@socket, :new_device, @user)}
id="add-device-button"
class="button">
patch={Routes.user_show_path(@socket, :new_device, @user)}
replace={true}
id="add-device-button"
class="button"
>
Add Device
</.link>
</section>
@@ -78,12 +87,14 @@
<p>Enable or disable this user's VPN connection. Applies to all their devices.</p>
</div>
<div class="level-right">
<.live_component id="allowed-to-connect"
module={FzHttpWeb.UserLive.VPNConnectionComponent}
user={@user} />
<.live_component
id="allowed-to-connect"
module={FzHttpWeb.UserLive.VPNConnectionComponent}
user={@user}
/>
</div>
</div>
<hr/>
<hr />
<strong>Promote or Demote User</strong>
<div class="level">
<div class="level-left">
@@ -94,7 +105,8 @@
class="button is-warning"
data-confirm={"Are you sure? #{mote_message(@user)}"}
phx-click={mote(@user)}
phx-value-user_id={@user.id}>
phx-value-user_id={@user.id}
>
<span class="icon is-small">
<i class="fas fa-user-shield"></i>
</span>
@@ -102,7 +114,7 @@
</button>
</div>
</div>
<hr/>
<hr />
<strong>Delete User</strong>
<div class="level">
<div class="level-left">
@@ -113,7 +125,8 @@
class="button is-danger"
data-confirm="Are you sure? This will permanently delete this user, all associated devices and instantly drop any active VPN sessions associated to this user."
phx-click="delete_user"
phx-value-user_id={@user.id}>
phx-value-user_id={@user.id}
>
<span class="icon is-small">
<i class="fas fa-trash"></i>
</span>

View File

@@ -0,0 +1,13 @@
defmodule FzHttpWeb.Plug.SamlyTargetUrl do
@moduledoc """
Plug to set target url for samly to later on redirect to after auth success
"""
import Plug.Conn
def init(opts), do: opts
def call(conn, _opt) do
put_session(conn, "target_url", "/auth/saml/callback")
end
end

View File

@@ -45,6 +45,11 @@ defmodule FzHttpWeb.Router do
plug FzHttpWeb.Authentication.Pipeline
end
pipeline :samly do
plug :fetch_session
plug FzHttpWeb.Plug.SamlyTargetUrl
end
# Ueberauth routes
scope "/auth", FzHttpWeb do
pipe_through [
@@ -64,6 +69,12 @@ defmodule FzHttpWeb.Router do
get "/oidc/:provider", AuthController, :redirect_oidc_auth_uri, as: :auth_oidc
end
scope "/auth/saml" do
pipe_through :samly
forward "/", Samly.Router
end
# Unauthenticated routes
scope "/", FzHttpWeb do
pipe_through [
@@ -155,7 +166,11 @@ defmodule FzHttpWeb.Router do
live "/devices", DeviceLive.Admin.Index, :index
live "/devices/:id", DeviceLive.Admin.Show, :show
live "/settings/site", SettingLive.Site, :show
live "/settings/security", SettingLive.Security, :show
live "/settings/security/oidc/:id/edit", SettingLive.Security, :edit_oidc
live "/settings/security/saml/:id/edit", SettingLive.Security, :edit_saml
live "/settings/account", SettingLive.Account, :show
live "/settings/account/edit", SettingLive.Account, :edit
live "/settings/account/register_mfa", SettingLive.Account, :register_mfa
@@ -173,6 +188,9 @@ defmodule FzHttpWeb.Router do
forward "/mailbox", Plug.Swoosh.MailboxPreview
live_dashboard "/dashboard"
get "/samly", FzHttpWeb.DebugController, :samly
get "/session", FzHttpWeb.DebugController, :session
end
end
end

View File

@@ -1,9 +1,9 @@
<h3 class="is-3 title">Sign In</h3>
<hr>
<hr />
<div class="block">
<%= link "<- Back to sign in methods", to: Routes.root_path(@conn, :index) %>
<%= link("<- Back to sign in methods", to: Routes.root_path(@conn, :index)) %>
</div>
<div class="block">
@@ -11,7 +11,14 @@
<div class="field">
<label for="email" class="label">Email</label>
<div class="control">
<input class="input" type="email" name="email" id="email" required value={@conn.params["email"]} />
<input
class="input"
type="email"
name="email"
id="email"
required
value={@conn.params["email"]}
/>
</div>
</div>
@@ -26,10 +33,13 @@
<div class="control">
<div class="level">
<div class="level-left">
<%= submit "Sign In", class: "button" %>
<%= submit("Sign In", class: "button") %>
</div>
<div class="level-right">
<%= link "Forgot password", to: Routes.auth_path(@conn, :reset_password), class: "forgot-password" %>
<%= link("Forgot password",
to: Routes.auth_path(@conn, :reset_password),
class: "forgot-password"
) %>
</div>
</div>
</div>

View File

@@ -1,6 +1,6 @@
<h3 class="is-3 title">Reset Password</h3>
<hr>
<hr />
<div class="block">
<%= link("<- Back to sign in methods", to: Routes.root_path(@conn, :index)) %>
@@ -18,7 +18,7 @@
<div class="field">
<div class="control">
<%= submit "Send", class: "button" %>
<%= submit("Send", class: "button") %>
</div>
</div>
<% end %>

View File

@@ -1,102 +1,132 @@
<!DOCTYPE html>
<html lang="en" class="has-aside-left has-aside-mobile-transition has-navbar-fixed-top has-aside-expanded">
<html
lang="en"
class="has-aside-left has-aside-mobile-transition has-navbar-fixed-top has-aside-expanded"
>
<head>
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<.live_title prefix="Firezone • ">
<%= assigns[:page_title] %>
</.live_title>
<link phx-track-static rel="stylesheet" href={Routes.static_path(@conn, "/dist/admin.css")} />
<script defer phx-track-static type="text/javascript" src={Routes.static_path(@conn, "/dist/admin.js")}></script>
<script
defer
phx-track-static
type="text/javascript"
src={Routes.static_path(@conn, "/dist/admin.js")}
>
</script>
<!-- Favicon -->
<link rel="apple-touch-icon" sizes="180x180" href="/images/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/images/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/images/favicon-16x16.png">
<link rel="apple-touch-icon" sizes="180x180" href="/images/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/images/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/images/favicon-16x16.png" />
<meta name="msapplication-config" content="/browserconfig.xml" />
<meta name="msapplication-TileColor" content="331700">
<meta name="theme-color" content="331700">
<%= render(FzHttpWeb.SharedView, "socket_token_headers.html", conn: @conn, current_user: @current_user) %>
<meta name="msapplication-TileColor" content="331700" />
<meta name="theme-color" content="331700" />
<%= render(FzHttpWeb.SharedView, "socket_token_headers.html",
conn: @conn,
current_user: @current_user
) %>
</head>
<body>
<div id="app">
<nav id="navbar-main" class="navbar is-fixed-top">
<div class="navbar-brand">
<a class="navbar-item is-hidden-desktop jb-aside-mobile-toggle">
<span class="icon"><i class="mdi mdi-forwardburger mdi-24px"></i></span>
</a>
</div>
<div class="navbar-brand is-right">
<a class="navbar-item is-hidden-desktop jb-navbar-menu-toggle" data-target="navbar-menu">
<span class="icon"><i class="mdi mdi-dots-vertical"></i></span>
</a>
</div>
<div class="navbar-menu fadeIn animated faster" id="navbar-menu">
<div class="navbar-end">
<div class="navbar-item has-dropdown has-dropdown-with-icons has-divider is-hoverable">
<a class="navbar-link is-arrowless">
<div class="is-user-name"><span><%= @current_user.email %></span></div>
<span class="icon"><i class="mdi mdi-chevron-down"></i></span>
<div id="app">
<nav id="navbar-main" class="navbar is-fixed-top">
<div class="navbar-brand">
<a class="navbar-item is-hidden-desktop jb-aside-mobile-toggle">
<span class="icon"><i class="mdi mdi-forwardburger mdi-24px"></i></span>
</a>
<div class="navbar-dropdown">
<%= link(to: Routes.setting_account_path(@conn, :show), class: "navbar-item") do %>
<span class="icon"><i class="mdi mdi-account"></i></span>
<span>Account Settings</span>
<% end %>
<hr class="navbar-divider">
<%= link(to: Routes.auth_path(@conn, :delete), method: :delete, class: "navbar-item") do %>
<span class="icon"><i class="mdi mdi-logout"></i></span>
<span>Log Out</span>
<% end %>
</div>
</div>
<%= Phoenix.Component.live_render(@conn, FzHttpWeb.NotificationsLive.Badge, router: FzHttpWeb.Router) %>
<a target="_blank" href="https://docs.firezone.dev/?utm_source=product" title="Documentation" class="navbar-item has-divider is-desktop-icon-only">
<span class="icon"><i class="mdi mdi-help-circle-outline"></i></span>
</a>
<a id="web-ui-connect-success" href="#" title="Secure websocket connected." class="navbar-item has-divider is-desktop-icon-only">
<span class="icon has-text-success"><i class="mdi mdi-wifi"></i></span>
</a>
<a id="web-ui-connect-error" href="#" title="Secure websocket not connected! Check docs.firezone.dev/administer/troubleshoot for help." class="is-hidden navbar-item has-divider is-desktop-icon-only">
<span class="icon has-text-danger"><i class="mdi mdi-wifi-off"></i></span>
</a>
</div>
</div>
</nav>
<%= @inner_content %>
<footer class="footer">
<div class="container-fluid">
<div class="level">
<div class="level-left">
<div class="level-item">
🎉
&nbsp;
<strong>
0.5.0 is here!
<a href={"https://www.firezone.dev/blog/release-0-5-0/?utm_source=product&uid=#{Application.fetch_env!(:fz_http, :telemetry_id)}"}>
Click here to read more.
<div class="navbar-brand is-right">
<a class="navbar-item is-hidden-desktop jb-navbar-menu-toggle" data-target="navbar-menu">
<span class="icon"><i class="mdi mdi-dots-vertical"></i></span>
</a>
</div>
<div class="navbar-menu fadeIn animated faster" id="navbar-menu">
<div class="navbar-end">
<div class="navbar-item has-dropdown has-dropdown-with-icons has-divider is-hoverable">
<a class="navbar-link is-arrowless">
<div class="is-user-name"><span><%= @current_user.email %></span></div>
<span class="icon"><i class="mdi mdi-chevron-down"></i></span>
</a>
</strong>
</div>
</div>
<div class="level-right">
<div class="level-item">
<a href={"https://github.com/firezone/firezone/tree/#{git_sha()}"}>
Version <%= application_version() %>
<div class="navbar-dropdown">
<%= link(to: Routes.setting_account_path(@conn, :show), class: "navbar-item") do %>
<span class="icon"><i class="mdi mdi-account"></i></span>
<span>Account Settings</span>
<% end %>
<hr class="navbar-divider" />
<%= link(to: Routes.auth_path(@conn, :delete), method: :delete, class: "navbar-item") do %>
<span class="icon"><i class="mdi mdi-logout"></i></span>
<span>Log Out</span>
<% end %>
</div>
</div>
<%= Phoenix.Component.live_render(@conn, FzHttpWeb.NotificationsLive.Badge,
router: FzHttpWeb.Router
) %>
<a
target="_blank"
href="https://docs.firezone.dev/?utm_source=product"
title="Documentation"
class="navbar-item has-divider is-desktop-icon-only"
>
<span class="icon"><i class="mdi mdi-help-circle-outline"></i></span>
</a>
<a
id="web-ui-connect-success"
href="#"
title="Secure websocket connected."
class="navbar-item has-divider is-desktop-icon-only"
>
<span class="icon has-text-success"><i class="mdi mdi-wifi"></i></span>
</a>
<a
id="web-ui-connect-error"
href="#"
title="Secure websocket not connected! Check docs.firezone.dev/administer/troubleshoot for help."
class="is-hidden navbar-item has-divider is-desktop-icon-only"
>
<span class="icon has-text-danger"><i class="mdi mdi-wifi-off"></i></span>
</a>
</div>
<div class="level-item">
<div class="logo">
<a href="https://firezone.dev"><img src="/images/logo.svg" alt="firezone.dev"></a>
</div>
</nav>
<%= @inner_content %>
<footer class="footer">
<div class="container-fluid">
<div class="level">
<div class="level-left">
<div class="level-item">
🎉
&nbsp;
<strong>
0.6.0 is here!
<a href={"https://blog.firezone.dev/release-0-6-0/?utm_source=product&uid=#{Application.fetch_env!(:fz_http, :telemetry_id)}"}>
Click here to read more.
</a>
</strong>
</div>
</div>
<div class="level-right">
<div class="level-item">
<a href={"https://github.com/firezone/firezone/tree/#{git_sha()}"}>
Version <%= application_version() %>
</a>
</div>
<div class="level-item">
<div class="logo">
<a href="https://firezone.dev">
<img src="/images/logo.svg" alt="firezone.dev" />
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</footer>
</div>
</footer>
</div>
</body>
</body>
</html>

View File

@@ -1,6 +1,6 @@
<html>
<head>
<link rel="stylesheet" href={Routes.static_path(FzHttpWeb.Endpoint, "/css/email.css")}>
<link rel="stylesheet" href={Routes.static_path(FzHttpWeb.Endpoint, "/css/email.css")} />
</head>
<body>
<%= @inner_content %>

View File

@@ -1,42 +1,49 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<%= csrf_meta_tag() %>
<.live_title>
<%= assigns[:page_title] || "Firezone" %>
</.live_title>
<link phx-track-static rel="stylesheet" href={Routes.static_path(@conn, "/dist/root.css")} />
<script defer phx-track-static type="text/javascript" src={Routes.static_path(@conn, "/dist/root.js")}></script>
<script
defer
phx-track-static
type="text/javascript"
src={Routes.static_path(@conn, "/dist/root.js")}
>
</script>
<!-- Favicon -->
<link rel="apple-touch-icon" sizes="180x180" href="/images/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/images/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/images/favicon-16x16.png">
<link rel="apple-touch-icon" sizes="180x180" href="/images/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/images/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/images/favicon-16x16.png" />
<meta name="msapplication-config" content="/browserconfig.xml" />
<meta name="msapplication-TileColor" content="331700">
<meta name="theme-color" content="331700">
<meta name="msapplication-TileColor" content="331700" />
<meta name="theme-color" content="331700" />
<!-- CSRF -->
<%= csrf_meta_tag() %>
</head>
<body>
<section class="section hero is-fullheight is-error-section">
<div id="app" class="hero-body">
<div class="container">
<div class="columns is-centered">
<div class="column is-two-thirds-tablet is-half-desktop is-one-third-widescreen">
<div class="block">
<div class="has-text-centered">
<%= FzHttpWeb.LogoComponent.render(FzHttp.Configurations.get_configuration!().logo) %>
<body>
<section class="section hero is-fullheight is-error-section">
<div id="app" class="hero-body">
<div class="container">
<div class="columns is-centered">
<div class="column is-two-thirds-tablet is-half-desktop is-one-third-widescreen">
<div class="block">
<div class="has-text-centered">
<%= FzHttpWeb.LogoComponent.render(
FzHttp.Configurations.get_configuration!().logo
) %>
</div>
</div>
<%= @inner_content %>
</div>
</div>
<%= @inner_content %>
</div>
</div>
</div>
</div>
</section>
</body>
</section>
</body>
</html>

View File

@@ -1,42 +1,56 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<.live_title>
<%= assigns[:page_title] || "Firezone" %>
</.live_title>
<link phx-track-static rel="stylesheet" href={Routes.static_path(@conn, "/dist/unprivileged.css")} />
<script defer phx-track-static type="text/javascript" src={Routes.static_path(@conn, "/dist/unprivileged.js")}></script>
<link
phx-track-static
rel="stylesheet"
href={Routes.static_path(@conn, "/dist/unprivileged.css")}
/>
<script
defer
phx-track-static
type="text/javascript"
src={Routes.static_path(@conn, "/dist/unprivileged.js")}
>
</script>
<!-- Favicon -->
<link rel="apple-touch-icon" sizes="180x180" href="/images/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/images/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/images/favicon-16x16.png">
<link rel="apple-touch-icon" sizes="180x180" href="/images/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/images/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/images/favicon-16x16.png" />
<meta name="msapplication-config" content="/browserconfig.xml" />
<meta name="msapplication-TileColor" content="331700">
<meta name="theme-color" content="331700">
<%= render(FzHttpWeb.SharedView, "socket_token_headers.html", current_user: @current_user, conn: @conn) %>
<meta name="msapplication-TileColor" content="331700" />
<meta name="theme-color" content="331700" />
<%= render(FzHttpWeb.SharedView, "socket_token_headers.html",
current_user: @current_user,
conn: @conn
) %>
</head>
<body>
<section class="section hero is-fullheight is-error-section">
<div id="app" class="hero-body">
<div class="container">
<div class="columns is-centered">
<div class="column">
<div class="block">
<div class="has-text-centered">
<img src={Routes.static_path(@conn, "/images/logo-text.svg")} alt="firezone.dev">
<body>
<section class="section hero is-fullheight is-error-section">
<div id="app" class="hero-body">
<div class="container">
<div class="columns is-centered">
<div class="column">
<div class="block">
<div class="has-text-centered">
<img
src={Routes.static_path(@conn, "/images/logo-text.svg")}
alt="firezone.dev"
/>
</div>
</div>
<%= @inner_content %>
</div>
</div>
<%= @inner_content %>
</div>
</div>
</div>
</div>
</section>
</body>
</section>
</body>
</html>

View File

@@ -1,6 +1,6 @@
<h3 class="is-3 title">Sign In</h3>
<hr>
<hr />
<div class="content">
<p>
@@ -9,20 +9,31 @@
<%= for {provider, config} <- @openid_connect_providers do %>
<p>
<%=
link(
"Sign in with #{config[:label]}",
to: Routes.auth_oidc_path(@conn, :redirect_oidc_auth_uri, provider),
class: "button") %>
<%= link(
"Sign in with #{config[:label]}",
to: Routes.auth_oidc_path(@conn, :redirect_oidc_auth_uri, provider),
class: "button"
) %>
</p>
<% end %>
<%= for {provider, config} <- @saml_identity_providers do %>
<p>
<%= link(
"Sign in with #{config["label"]}",
to: "/auth/saml/auth/signin/#{provider}/",
class: "button"
) %>
</p>
<% end %>
<%= if @local_enabled do %>
<p>
<%= link(
"Sign in with email",
to: Routes.auth_path(@conn, :request, "identity"),
class: "button") %>
"Sign in with email",
to: Routes.auth_path(@conn, :request, "identity"),
class: "button"
) %>
</p>
<% end %>
</div>

View File

@@ -1,10 +1,11 @@
<table class="table is-bordered is-hoverable is-striped is-fullwidth">
<tbody>
<%= if has_role?(@current_user, :admin) do %>
<tr>
<td><strong>User</strong></td>
<td><%= live_redirect(@user.email, to: Routes.user_show_path(@socket, :show, @user)) %></td>
<td>
<%= live_redirect(@user.email, to: Routes.user_show_path(@socket, :show, @user)) %>
</td>
</tr>
<% end %>
@@ -39,9 +40,13 @@
<tr>
<td><strong>Latest Handshake</strong></td>
<td id={"device-#{@device.id}-latest-handshake"}
data-timestamp={@device.latest_handshake}
phx-hook="FormatTimestamp">…</td>
<td
id={"device-#{@device.id}-latest-handshake"}
data-timestamp={@device.latest_handshake}
phx-hook="FormatTimestamp"
>
</td>
</tr>
<tr>

View File

@@ -2,7 +2,9 @@
<thead>
<tr>
<th>Name</th>
<%= if @show_user do %><th>User</th><% end %>
<%= if @show_user do %>
<th>User</th>
<% end %>
<th>WireGuard IP</th>
<th>Remote IP</th>
<th>Latest Handshake</th>
@@ -13,36 +15,55 @@
</tr>
</thead>
<tbody>
<%= for device <- @devices do %>
<tr>
<td>
<.link navigate={Routes.device_admin_show_path(@socket, :show, device)}>
<%= device.name %>
</.link>
</td>
<%= if @show_user do %>
<%= for device <- @devices do %>
<tr>
<td>
<%= live_redirect(device.user.email, to: Routes.user_show_path(@socket, :show, device.user)) %>
<.link navigate={Routes.device_admin_show_path(@socket, :show, device)}>
<%= device.name %>
</.link>
</td>
<% end %>
<td class="code">
<%= device.ipv4 %>
<br>
<%= device.ipv6 %>
</td>
<td class="code">
<%= device.remote_ip %>
</td>
<td id={"device-#{device.id}-latest-handshake"} data-timestamp={device.latest_handshake} phx-hook="FormatTimestamp">…</td>
<td class="code">
<%= FzCommon.FzInteger.to_human_bytes(device.rx_bytes) %> received
<br>
<%= FzCommon.FzInteger.to_human_bytes(device.tx_bytes) %> sent
</td>
<td class="code"><%= device.public_key %></td>
<td id={"device-#{device.id}-inserted-at"} data-timestamp={device.inserted_at} phx-hook="FormatTimestamp">…</td>
<td id={"device-#{device.id}-updated-at"} data-timestamp={device.updated_at} phx-hook="FormatTimestamp">…</td>
</tr>
<% end %>
<%= if @show_user do %>
<td>
<%= live_redirect(device.user.email,
to: Routes.user_show_path(@socket, :show, device.user)
) %>
</td>
<% end %>
<td class="code">
<%= device.ipv4 %>
<br />
<%= device.ipv6 %>
</td>
<td class="code">
<%= device.remote_ip %>
</td>
<td
id={"device-#{device.id}-latest-handshake"}
data-timestamp={device.latest_handshake}
phx-hook="FormatTimestamp"
>
</td>
<td class="code">
<%= FzCommon.FzInteger.to_human_bytes(device.rx_bytes) %> received <br />
<%= FzCommon.FzInteger.to_human_bytes(device.tx_bytes) %> sent
</td>
<td class="code"><%= device.public_key %></td>
<td
id={"device-#{device.id}-inserted-at"}
data-timestamp={device.inserted_at}
phx-hook="FormatTimestamp"
>
</td>
<td
id={"device-#{device.id}-updated-at"}
data-timestamp={device.updated_at}
phx-hook="FormatTimestamp"
>
</td>
</tr>
<% end %>
</tbody>
</table>

View File

@@ -2,21 +2,25 @@
<div class="content flash-squeeze">
<%= if live_flash(@flash, :info) do %>
<div class="notification is-info">
<button title="Dismiss notification"
class="delete"
phx-click="lv:clear-flash"
phx-value-key="info"
></button>
<button
title="Dismiss notification"
class="delete"
phx-click="lv:clear-flash"
phx-value-key="info"
>
</button>
<div class="flash-info"><%= live_flash(@flash, :info) %></div>
</div>
<% end %>
<%= if live_flash(@flash, :error) do %>
<div class="notification is-danger">
<button title="Dismiss notification"
class="delete"
phx-click="lv:clear-flash"
phx-value-key="error"
></button>
<button
title="Dismiss notification"
class="delete"
phx-click="lv:clear-flash"
phx-value-key="error"
>
</button>
<div class="flash-error"><%= live_flash(@flash, :error) %></div>
</div>
<% end %>

View File

@@ -1,9 +1,9 @@
<section class="hero is-hero-bar">
<div class="hero-body">
<div class="block">
<h1 class="title">
<%= @page_title %>
</h1>
<h1 class="title">
<%= @page_title %>
</h1>
</div>
<%= if assigns[:page_subtitle] do %>
<div class="block">

View File

@@ -8,28 +8,35 @@
</tr>
</thead>
<tbody>
<%= for method <- @methods do %>
<tr>
<td>
<%= method.name %>
</td>
<td>
<%= method.type %>
</td>
<td id={"method-#{method.id}-last-used-at"} data-timestamp={method.last_used_at} phx-hook="FormatTimestamp">…</td>
<td>
<button
class="button is-warning"
data-confirm={"Are you sure about deleting this authenticator <#{method.name}>?"}
phx-click="delete_authenticator"
phx-value-id={method.id}>
<span class="icon is-small">
<i class="fas fa-trash"></i>
</span>
<span>Delete</span>
</button>
</td>
</tr>
<% end %>
<%= for method <- @methods do %>
<tr>
<td>
<%= method.name %>
</td>
<td>
<%= method.type %>
</td>
<td
id={"method-#{method.id}-last-used-at"}
data-timestamp={method.last_used_at}
phx-hook="FormatTimestamp"
>
</td>
<td>
<button
class="button is-warning"
data-confirm={"Are you sure about deleting this authenticator <#{method.name}>?"}
phx-click="delete_authenticator"
phx-value-id={method.id}
>
<span class="icon is-small">
<i class="fas fa-trash"></i>
</span>
<span>Delete</span>
</button>
</td>
</tr>
<% end %>
</tbody>
</table>

View File

@@ -1,22 +1,20 @@
<div class="field">
<%= label @context, @field, @label, class: "label" %>
<%= label(@context, @field, @label, class: "label") %>
<div class="control">
<%= password_input @context,
@field,
class: "input password",
id: "#{@field}-field",
autocomplete: "new-password",
data_target: "#{@field}-progress",
phx_hook: "PasswordStrength" %>
<%= password_input(
@context,
@field,
class: "input password",
id: "#{@field}-field",
autocomplete: "new-password",
data_target: "#{@field}-progress",
phx_hook: "PasswordStrength"
) %>
</div>
<p class="help is-danger">
<%= error_tag @context, @field %>
<%= error_tag(@context, @field) %>
</p>
<progress
id={"#{@field}-progress"}
class="is-hidden"
value="0"
max="100">0%</progress>
<progress id={"#{@field}-progress"} class="is-hidden" value="0" max="100">0%</progress>
</div>

View File

@@ -1,5 +1,5 @@
<section class="section is-main-section">
<%= render FzHttpWeb.SharedView, "flash.html", assigns %>
<%= render(FzHttpWeb.SharedView, "flash.html", assigns) %>
<h4 class="title is-4">Details</h4>
@@ -12,11 +12,13 @@
Danger Zone
</h4>
<button class="button is-danger"
<button
class="button is-danger"
id="delete-device-button"
phx-click="delete_device"
phx-value-device_id={@device.id}
data-confirm="Are you sure? This will immediately disconnect this device and remove all associated data.">
data-confirm="Are you sure? This will immediately disconnect this device and remove all associated data."
>
<span class="icon is-small">
<i class="fas fa-trash"></i>
</span>

View File

@@ -1,8 +1,12 @@
<!-- User Socket -->
<%= tag :meta, name: "user-token", content: Phoenix.Token.sign(@conn, "user auth", @current_user.id) %>
<%= tag(:meta,
name: "user-token",
content: Phoenix.Token.sign(@conn, "user auth", @current_user.id)
) %>
<!-- Notification Channel -->
<%= tag :meta, name: "channel-token", content: Phoenix.Token.sign(@conn, "channel auth", @current_user.id) %>
<%= tag(:meta,
name: "channel-token",
content: Phoenix.Token.sign(@conn, "channel auth", @current_user.id)
) %>
<!-- CSRF -->
<%= csrf_meta_tag() %>

View File

@@ -3,8 +3,11 @@
<div class="level">
<div class="level-left"></div>
<div class="level-right">
<%= submit assigns[:button_text] || "Save",
phx_disable_with: "Saving...", form: assigns[:form], class: "button is-primary" %>
<%= submit(assigns[:button_text] || "Save",
phx_disable_with: "Saving...",
form: assigns[:form],
class: "button is-primary"
) %>
</div>
</div>
</div>

View File

@@ -15,7 +15,8 @@
<td
id="last-signed-in-at"
data-timestamp={@user.last_signed_in_at}
phx-hook="FormatTimestamp">
phx-hook="FormatTimestamp"
>
</td>
</tr>
@@ -25,7 +26,8 @@
<td
id={"user-#{@user.id}-created-at"}
data-timestamp={@user.inserted_at}
phx-hook="FormatTimestamp">
phx-hook="FormatTimestamp"
>
</td>
</tr>
@@ -35,7 +37,8 @@
<td
id={"user-#{@user.id}-updated-at"}
data-timestamp={@user.updated_at}
phx-hook="FormatTimestamp">
phx-hook="FormatTimestamp"
>
</td>
</tr>

View File

@@ -18,15 +18,24 @@ defmodule FzHttpWeb.UserFromAuth do
Users.get_by_email(email) |> Authentication.authenticate(password)
end
def find_or_create(_provider, %{"email" => email, "sub" => _sub}) do
# SAML
def find_or_create(:saml, provider_key, %{"email" => email}) do
case Users.get_by_email(email) do
nil -> maybe_create_user(email)
nil -> maybe_create_user(:saml_identity_providers, provider_key, email)
user -> {:ok, user}
end
end
defp maybe_create_user(email) do
if Conf.get!(:auto_create_oidc_users) do
# OIDC
def find_or_create(provider_key, %{"email" => email, "sub" => _sub}) do
case Users.get_by_email(email) do
nil -> maybe_create_user(:openid_connect_providers, provider_key, email)
user -> {:ok, user}
end
end
defp maybe_create_user(idp_field, provider_key, email) do
if Conf.auto_create_users?(idp_field, provider_key) do
Users.create_unprivileged_user(%{email: email})
else
{:error, "not found"}

View File

@@ -65,6 +65,7 @@ defmodule FzHttp.MixProject do
{:guardian, "~> 2.0"},
{:guardian_db, "~> 2.0"},
{:openid_connect, "~> 0.2.2"},
{:samly, github: "dropbox/samly"},
{:ueberauth, "~> 0.7"},
{:ueberauth_identity, "~> 0.4"},
{:httpoison, "~> 1.8"},

View File

@@ -0,0 +1,21 @@
-----BEGIN CERTIFICATE-----
MIIDfTCCAmWgAwIBAgIJAJYYUPJ1xkGhMA0GCSqGSIb3DQEBCwUAMEMxGjAYBgNV
BAoMEVBob2VuaXggRnJhbWV3b3JrMSUwIwYDVQQDDBxTZWxmLXNpZ25lZCB0ZXN0
IGNlcnRpZmljYXRlMB4XDTIyMTAwNTAwMDAwMFoXDTIzMTAwNTAwMDAwMFowQzEa
MBgGA1UECgwRUGhvZW5peCBGcmFtZXdvcmsxJTAjBgNVBAMMHFNlbGYtc2lnbmVk
IHRlc3QgY2VydGlmaWNhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
AQC8FSQs7KYacs5lTg0+7/NmbSnwJHZc6W7di9tnzWJPiReBwEVWLj82Bn4mbQIZ
nQMgbckQUA3V8LLGHC3nBxqy6xqt0h/69OhpvKFWHcakmzv/+eOXj7ruQ42uzaba
AGXkTWqyRHpqbqYfF45XQEMQau2Fw+9AQZuBtU+Sz98Im5n5DV6S/BTaLNIbszlg
MEhJYmG19hI/XZ45Dj439M1Hg9D1U1N5vMxcLcnpgBSBLAoBupyq98wme3OU/eAt
BkmQDzNKESESNSj6fw+8CI9V26TXXrf1ELmJRFLv34ZAj4edLBLoU/z0n/vT3xTd
r558NZVTG2IvkBKF7BUjrcFFAgMBAAGjdDByMAwGA1UdEwEB/wQCMAAwDgYDVR0P
AQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAdBgNVHQ4E
FgQU1xmmLeuXQFix8LaBuXlhEGshBJIwFAYDVR0RBA0wC4IJbG9jYWxob3N0MA0G
CSqGSIb3DQEBCwUAA4IBAQCYOy6x0+M4lkl5c4GDBDyQc3CXsyqKBGu92LH2ybaU
uIX6C6I329nkP7PRvJIewYQhuiAoL/KiaQJItZRkObRkXrrtsnnbZiEvnCvinMK0
Tueq5/eixQqZFuOkKaLEf8PpJAEqSjiyZy8etWbGd53/OPY0Swwsd9V2+fx48cFL
0MlaBFQR6qervAO84a2Las0dAHhXOYSQObOokGt/8b5eairi1w/dT6+iHyKM2dUM
QQfNt3QOTKJUf6Xq31ytLmlnfsGe5rKsBUqJuUH5NBeGu4pOoywG+9U9Bc5GHBMq
r3Pk9vaGkdEtmZehscgdD2+dYA+FBs0eCzOrzT08h7MW
-----END CERTIFICATE-----

View File

@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAvBUkLOymGnLOZU4NPu/zZm0p8CR2XOlu3YvbZ81iT4kXgcBF
Vi4/NgZ+Jm0CGZ0DIG3JEFAN1fCyxhwt5wcasusardIf+vToabyhVh3GpJs7//nj
l4+67kONrs2m2gBl5E1qskR6am6mHxeOV0BDEGrthcPvQEGbgbVPks/fCJuZ+Q1e
kvwU2izSG7M5YDBISWJhtfYSP12eOQ4+N/TNR4PQ9VNTebzMXC3J6YAUgSwKAbqc
qvfMJntzlP3gLQZJkA8zShEhEjUo+n8PvAiPVduk11639RC5iURS79+GQI+HnSwS
6FP89J/7098U3a+efDWVUxtiL5AShewVI63BRQIDAQABAoIBAB+N4HLVBQz849ml
HZ3IfepaOCX8yArQcvQiSZ4BnBPB6TqwejF6MsqqjjF+KlMHv4WKRahB9gBFkIii
I6VV0Mnhnak5znm46uEKb3rWJgRpsshAMUm1KGRe2v9Pq0V5uZ5yyoq76FnA1If0
2MGUm2u+tLizZYk/OIqrU31K+J0lzCaiSxIji9QVD68eIQ9M3+wNC7IZr3LLtwSl
xuJNDl1aQXyz6fB7mGiasjlYcmZgRgyXfnCQimd3EuF7hDYpGD8Qo8ZyZRCN5KQh
V8dWowezzmmmjeabcJzDOWgpZihUtEdycM4iRfLxpEvnCoZZVF/urhyLgzsq6FRq
qybAE4ECgYEA4k0u/X48615SkJ72UdaR73VNczDZ0HN+UX7SdbDgXXb41UNjOuxC
R7FeQuNT+Eldcx4bssYbHRM7rfVqvWlYd266VveE7nNYE46WjFuUYiZr+zNZwyot
J3t2Ks3egGlZHWYAvIMlaMwC0LAM3qbxl6rcMhJVXh8QvblRqhd5CqECgYEA1MP0
4LXIc+3fnc0prhB6HOwWbYD1AwliCjLYiZdPSA98jR+jtjHYp7os2deFLMDWPV4x
2phyWYg8kvgcp3amkfsgUF1M8sdmnRfDQxNgWpRxZ/6RpPk88zXhMtWLzqRpXiys
z2Kec//Uw3JZDwrPYQ7K5JAznZofLtEM45NcOCUCgYEAisKa8pKKViQS6lyeWtX/
y92YbO5iUH/Qz7W85K9dE9JUh6f3W3TsuzsVulvb7B1IMMMgZsE0dOKLMIKQPa4v
saPynErPdsrBEdTXmR66YGiAw5ncC2B8KX55mYt8SC7Qlscp4m1j7dtSSpX4fjnN
X5tDw2wcbkcMI9lTKsGT1aECgYEAp+R1oLhxlGF52qjhofRol9gInqJrNNk7nvae
fnyC2Ec4Lphv9D6DS1+TMtdpxHXq2QQybN9tJI9n1UWqPs9XA8zZo/Dr3oxQwdfV
gmGQ4AlRMBHm1frDCNxUd2uhZg/BAcpZF1En3jtbplreQgtyt5EXs6LCyDOtNaFK
/W30EG0CgYAdHjgvmXMsUo52w0E4Qer7eKJiZxxsxW3ucelPW1PYyD3GEWBeZ0IE
UECGyYjhh1Igb4bPRWD6OIxZfF32swMEiM1wpx6gW5GS2mMTNrpXOChNscBKZrtd
eccM/L8vdwYw5INerYkKlo4hNhqwsc4rXOQ31mT/nPJ0DT3cL292AA==
-----END RSA PRIVATE KEY-----

View File

@@ -0,0 +1,9 @@
defmodule FzHttp.Repo.Migrations.AddSamlIdentityProvidersToConfiguration do
use Ecto.Migration
def change do
alter table(:configurations) do
add :saml_identity_providers, :map
end
end
end

View File

@@ -0,0 +1,77 @@
defmodule FzHttp.Repo.Migrations.MoveAutoCreateUsersToProviders do
@moduledoc """
I know this migration is hacky, but doing this in pure SQL is non-trivial
for my level of Postgres-fu, so this will have to do.
"""
use Ecto.Migration
import Ecto.Query
# Returns data like:
# [
# %{
# openid_connect_providers: %{
# "new-provider-H2Ch" => %{
# "client_id" => "0oa3yiq0ahKwW2MhH5d7",
# "client_secret" => "3knL8CefL0RsoPVA6PukfoJxItdDz5a5Z8w6D618",
# "discovery_document_uri" => "https://okta-devok12.okta.com/.well-known/openid-configuration",
# "label" => "okta",
# "response_type" => "code",
# "scope" => "openid email profile"
# }
# }
# }
# ]
defp oid_provider_keys do
FzHttp.Repo.all(from("configurations", select: [:openid_connect_providers]))
# only one configuration at this point
|> List.first()
|> Map.get(:openid_connect_providers)
|> keys()
end
defp saml_provider_keys do
FzHttp.Repo.all(from("configurations", select: [:saml_identity_providers]))
# only one configuration at this point
|> List.first()
|> Map.get(:saml_identity_providers)
|> keys()
end
defp keys(nil), do: []
defp keys(map), do: Map.keys(map)
defp cur_oidc_create_users do
FzHttp.Repo.all(from("configurations", select: [:auto_create_oidc_users]))
|> List.first()
|> Map.get(:auto_create_oidc_users)
end
def change do
cur_oidc = cur_oidc_create_users() || System.get_env("AUTO_CREATE_OIDC_USERS", "true")
for key <- oid_provider_keys() do
execute """
UPDATE configurations
SET openid_connect_providers = jsonb_insert(
(SELECT openid_connect_providers FROM configurations),
'{#{key}, auto_create_users}', '#{cur_oidc}'
) WHERE 1 = 1;
"""
end
for key <- saml_provider_keys() do
execute """
UPDATE configurations
SET saml_identity_providers = jsonb_insert(
(SELECT saml_identity_providers FROM configurations),
'{#{key}, auto_create_users}', 'true'
) WHERE 1 = 1;
"""
end
alter table(:configurations) do
remove :auto_create_oidc_users
end
end
end

View File

@@ -55,20 +55,6 @@ defmodule FzHttp.TelemetryTest do
assert ping_data[:openid_providers] == 2
end
@tag config: {:auto_create_oidc_users, true}
test "auto create oidc users enabled" do
ping_data = Telemetry.ping_data()
assert ping_data[:auto_create_oidc_users]
end
@tag config: {:auto_create_oidc_users, false}
test "auto create oidc users disabled" do
ping_data = Telemetry.ping_data()
refute ping_data[:auto_create_oidc_users]
end
@tag config: {:disable_vpn_on_oidc_error, true}
test "disable vpn on oidc error enabled" do
ping_data = Telemetry.ping_data()

View File

@@ -8,12 +8,14 @@ defmodule FzHttpWeb.AuthControllerTest do
test "unauthed: loads the sign in form", %{unauthed_conn: conn} do
expect(OpenIDConnect.Mock, :authorization_uri, fn _, _ -> "https://auth.url" end)
test_conn = get(conn, Routes.root_path(conn, :index))
# Assert that we email, OIDC and Oauth2 buttons provided
for expected <- [
"Sign in with email",
"Sign in with OIDC Google"
"Sign in with OIDC Google",
"Sign in with SAML"
] do
assert html_response(test_conn, 200) =~ expected
end
@@ -173,7 +175,7 @@ defmodule FzHttpWeb.AuthControllerTest do
test "sends a magic link in email", %{unauthed_conn: conn, user: user} do
post(conn, Routes.auth_path(conn, :magic_link), %{"email" => user.email})
Process.sleep(100)
Process.sleep(10)
assert_email_sent(subject: "Firezone Magic Link", to: [{"", user.email}])
end
end

View File

@@ -134,7 +134,7 @@ defmodule FzHttpWeb.DeviceLive.Unprivileged.IndexTest do
|> element("a", "Add Device")
|> render_click()
assert_redirected(view, Routes.device_unprivileged_index_path(conn, :new))
assert_patched(view, Routes.device_unprivileged_index_path(conn, :new))
end
test "creates device", %{unprivileged_conn: conn} do

View File

@@ -85,7 +85,10 @@ defmodule FzHttpWeb.SettingLive.AccountTest do
|> element("button.delete")
|> render_click()
assert_redirected(view, Routes.setting_account_path(conn, :show))
# Intermittent failure unless we wait a bit
Process.sleep(10)
assert_patched(view, Routes.setting_account_path(conn, :show))
end
end
end

View File

@@ -63,12 +63,10 @@ defmodule FzHttpWeb.SettingLive.SecurityTest do
{:allow_unprivileged_device_management, true},
{:allow_unprivileged_device_configuration, true},
{:disable_vpn_on_oidc_error, true},
{:auto_create_oidc_users, true},
{:local_auth_enabled, nil},
{:allow_unprivileged_device_management, nil},
{:allow_unprivileged_device_configuration, nil},
{:disable_vpn_on_oidc_error, nil},
{:auto_create_oidc_users, nil}
{:disable_vpn_on_oidc_error, nil}
] do
@tag [config: t, config_val: val]
test "toggle #{t} when value in db is #{val}", %{admin_conn: conn, path: path} do
@@ -91,34 +89,115 @@ defmodule FzHttpWeb.SettingLive.SecurityTest do
describe "oidc configuration" do
setup %{admin_conn: conn} do
Conf.update_configuration(%{
openid_connect_providers: %{"test" => %{"label" => "test123"}},
saml_identity_providers: %{}
})
path = Routes.setting_security_path(conn, :show)
{:ok, view, _html} = live(conn, path)
[view: view]
end
test "fails if not proper json", %{view: view} do
test "click add button", %{view: view} do
html =
render_submit(view, "save_oidc_config", %{
"configuration" => %{"openid_connect_providers" => "{"}
})
view
|> element("a", "Add OpenID Connect Provider")
|> render_click()
assert html =~ "Invalid JSON configuration"
assert html =~ ~s|<p class="modal-card-title">OIDC Config</p>|
end
test "saves proper json", %{view: view} do
render_submit(view, "save_oidc_config", %{
"configuration" => %{"openid_connect_providers" => ~s|{"google": {"key": "value"}}|}
})
test "click edit button", %{view: view} do
html =
view
|> element("a", "Edit")
|> render_click()
assert Conf.get!(:openid_connect_providers) == %{"google" => %{"key" => "value"}}
assert html =~ ~s|<p class="modal-card-title">OIDC Config</p>|
assert html =~ ~s|value="test123"|
end
test "updates parsed config", %{view: view} do
render_submit(view, "save_oidc_config", %{
"configuration" => %{"openid_connect_providers" => ~s|{"firezone": {"key": "value"}}|}
test "validate", %{view: view} do
view
|> element("a", "Edit")
|> render_click()
html =
view
|> element("#oidc-form")
|> render_submit(%{"label" => "updated"})
# stays on the modal
assert html =~ ~s|<p class="modal-card-title">OIDC Config</p>|
# not updated
assert Conf.get!(:openid_connect_providers) == %{"test" => %{"label" => "test123"}}
end
test "delete", %{view: view} do
view
|> element("button", "Delete")
|> render_click()
assert Conf.get!(:openid_connect_providers) == %{}
end
end
describe "saml configuration" do
setup %{admin_conn: conn} do
Conf.update_configuration(%{
openid_connect_providers: %{},
saml_identity_providers: %{"test" => %{"metadata" => "<test></test>"}}
})
assert [firezone: _] = Conf.get!(:parsed_openid_connect_providers)
path = Routes.setting_security_path(conn, :show)
{:ok, view, _html} = live(conn, path)
[view: view]
end
test "click add button", %{view: view} do
html =
view
|> element("a", "Add SAML Identity Provider")
|> render_click()
assert html =~ ~s|<p class="modal-card-title">SAML Config</p>|
end
test "click edit button", %{view: view} do
html =
view
|> element("a", "Edit")
|> render_click()
assert html =~ ~s|<p class="modal-card-title">SAML Config</p>|
assert html =~ ~s|&amp;amp;amp;lt;test&amp;amp;amp;gt;&amp;amp;amp;lt;/test&amp;amp;amp;gt;|
end
test "validate", %{view: view} do
view
|> element("a", "Edit")
|> render_click()
html =
view
|> element("#saml-form")
|> render_submit(%{"metadata" => "updated"})
# stays on the modal
assert html =~ ~s|<p class="modal-card-title">SAML Config</p>|
# not updated
assert Conf.get!(:saml_identity_providers) == %{"test" => %{"metadata" => "<test></test>"}}
end
test "delete", %{view: view} do
view
|> element("button", "Delete")
|> render_click()
assert Conf.get!(:saml_identity_providers) == %{}
end
end
end

View File

@@ -71,7 +71,9 @@ defmodule FzHttpWeb.SettingLive.Unprivileged.AccountTest do
|> element("button.delete")
|> render_click()
assert_redirected(view, Routes.setting_unprivileged_account_path(conn, :show))
Process.sleep(10)
assert_patched(view, Routes.setting_unprivileged_account_path(conn, :show))
end
end
end

View File

@@ -113,7 +113,7 @@ defmodule FzHttpWeb.UserLive.IndexTest do
|> element("a", "Add User")
|> render_click()
assert_redirected(view, Routes.user_index_path(conn, :new))
assert_patched(view, Routes.user_index_path(conn, :new))
end
end
end

View File

@@ -159,7 +159,7 @@ defmodule FzHttpWeb.UserLive.ShowTest do
|> element("#add-device-button")
|> render_click()
assert_redirected(view, Routes.user_show_path(conn, :new_device, user.id))
assert_patched(view, Routes.user_show_path(conn, :new_device, user.id))
end
test "allows name changes", %{admin_conn: conn, admin_user: user} do

View File

@@ -1,10 +1,13 @@
defmodule FzHttpWeb.UserFromAuthTest do
use FzHttp.DataCase, async: true
alias FzHttp.Configurations, as: Conf
alias FzHttp.Users
alias FzHttpWeb.UserFromAuth
alias Ueberauth.Auth
@moduletag email: "sso@test"
describe "find_or_create/1 via identity provider" do
setup :create_user
@@ -23,28 +26,50 @@ defmodule FzHttpWeb.UserFromAuthTest do
end
describe "find_or_create/2 via OIDC with auto create enabled" do
@email "oidc@test"
@tag config: %{"oidc_test" => %{auto_create_users: true}}
test "sign in creates user", %{config: config, email: email} do
restore_env(:openid_connect_providers, config, &on_exit/1)
test "sign in creates user" do
assert {:ok, result} =
UserFromAuth.find_or_create(:noop, %{"email" => @email, "sub" => :noop})
UserFromAuth.find_or_create("oidc_test", %{"email" => email, "sub" => :noop})
assert result.email == @email
assert result.email == email
end
end
describe "find_or_create/2 via OIDC with auto create disabled" do
@email "oidc@test"
@tag config: %{"oidc_test" => %{auto_create_users: false}}
test "sign in returns error", %{email: email, config: config} do
restore_env(:openid_connect_providers, config, &on_exit/1)
setup do
restore_env(:auto_create_oidc_users, false, &on_exit/1)
end
test "sign in returns error" do
assert {:error, "not found"} =
UserFromAuth.find_or_create(:noop, %{"email" => @email, "sub" => :noop})
UserFromAuth.find_or_create("oidc_test", %{"email" => email, "sub" => :noop})
assert Users.get_by_email(@email) == nil
assert Users.get_by_email(email) == nil
end
end
describe "find_or_create/2 via SAML with auto create enabled" do
@tag config: %{"saml_test" => %{auto_create_users: true}}
test "sign in creates user", %{config: config, email: email} do
restore_env(:saml_identity_providers, config, &on_exit/1)
assert {:ok, result} =
UserFromAuth.find_or_create(:saml, "saml_test", %{"email" => email, "sub" => :noop})
assert result.email == email
end
end
describe "find_or_create/2 via SAML with auto create disabled" do
@tag config: %{"saml_test" => %{auto_create_users: false}}
test "sign in returns error", %{email: email, config: config} do
restore_env(:saml_identity_providers, config, &on_exit/1)
assert {:error, "not found"} =
UserFromAuth.find_or_create(:saml, "saml_test", %{"email" => email, "sub" => :noop})
assert Users.get_by_email(email) == nil
end
end
end

View File

@@ -56,7 +56,6 @@ config :fz_http,
external_trusted_proxies: [],
private_clients: [],
disable_vpn_on_oidc_error: true,
auto_create_oidc_users: true,
sandbox: true,
allow_unprivileged_device_management: true,
allow_unprivileged_device_configuration: true,
@@ -88,6 +87,9 @@ config :fz_http,
default_admin_password: "firezone1234",
server_process_opts: [name: {:global, :fz_http_server}],
openid_connect_providers: "{}",
saml_identity_providers: %{},
saml_certfile_path: "apps/fz_http/priv/cert/saml_selfsigned.pem",
saml_keyfile_path: "apps/fz_http/priv/cert/saml_selfsigned_key.pem",
openid_connect: OpenIDConnect
config :fz_wall,
@@ -104,7 +106,7 @@ config :hammer,
# This will be changed per-env
config :fz_vpn,
wireguard_private_key_path: "tmp/dummy",
wireguard_private_key_path: "priv/wg_dev_private_key",
stats_push_service_enabled: true,
wireguard_interface_name: "wg-firezone",
wireguard_port: 51_820,
@@ -141,6 +143,13 @@ config :fz_http, FzHttp.Vault,
config :fz_http, FzHttp.Mailer, adapter: FzHttp.Mailer.NoopAdapter
config :samly, Samly.State, store: Samly.State.Session
config :samly, Samly.Provider,
idp_id_from: :path_segment,
service_providers: [],
identity_providers: []
# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{Mix.env()}.exs"

View File

@@ -22,7 +22,7 @@ end
# For development, we disable any cache and enable
# debugging and code reloading.
config :fz_http, FzHttpWeb.Endpoint,
http: [port: 4000],
http: [port: 13000],
debug_errors: true,
code_reloader: true,
check_origin: ["//127.0.0.1", "//localhost"],

View File

@@ -23,51 +23,68 @@ config :fz_wall,
# Formerly releases.exs - Only evaluated in production
if config_env() == :prod do
# For releases, require that all these are set
database_name = System.fetch_env!("DATABASE_NAME")
database_user = System.fetch_env!("DATABASE_USER")
database_host = System.fetch_env!("DATABASE_HOST")
database_port = String.to_integer(System.fetch_env!("DATABASE_PORT"))
database_pool = String.to_integer(System.fetch_env!("DATABASE_POOL"))
database_ssl = FzString.to_boolean(System.fetch_env!("DATABASE_SSL"))
database_ssl_opts = Jason.decode!(System.fetch_env!("DATABASE_SSL_OPTS"))
database_parameters = Jason.decode!(System.fetch_env!("DATABASE_PARAMETERS"))
phoenix_listen_address = System.fetch_env!("PHOENIX_LISTEN_ADDRESS")
phoenix_port = String.to_integer(System.fetch_env!("PHOENIX_PORT"))
external_trusted_proxies = Jason.decode!(System.fetch_env!("EXTERNAL_TRUSTED_PROXIES"))
private_clients = Jason.decode!(System.fetch_env!("PRIVATE_CLIENTS"))
admin_email = System.fetch_env!("ADMIN_EMAIL")
default_admin_password = System.fetch_env!("DEFAULT_ADMIN_PASSWORD")
wireguard_private_key_path = System.fetch_env!("WIREGUARD_PRIVATE_KEY_PATH")
wireguard_interface_name = System.fetch_env!("WIREGUARD_INTERFACE_NAME")
wireguard_port = String.to_integer(System.fetch_env!("WIREGUARD_PORT"))
nft_path = System.fetch_env!("NFT_PATH")
egress_interface = System.fetch_env!("EGRESS_INTERFACE")
wireguard_dns = System.get_env("WIREGUARD_DNS")
wireguard_allowed_ips = System.fetch_env!("WIREGUARD_ALLOWED_IPS")
wireguard_persistent_keepalive = System.fetch_env!("WIREGUARD_PERSISTENT_KEEPALIVE")
wireguard_ipv4_enabled = FzString.to_boolean(System.fetch_env!("WIREGUARD_IPV4_ENABLED"))
wireguard_ipv4_masquerade = FzString.to_boolean(System.fetch_env!("WIREGUARD_IPV4_MASQUERADE"))
wireguard_ipv6_masquerade = FzString.to_boolean(System.fetch_env!("WIREGUARD_IPV6_MASQUERADE"))
wireguard_ipv4_network = System.fetch_env!("WIREGUARD_IPV4_NETWORK")
wireguard_ipv4_address = System.fetch_env!("WIREGUARD_IPV4_ADDRESS")
wireguard_ipv6_enabled = FzString.to_boolean(System.fetch_env!("WIREGUARD_IPV6_ENABLED"))
wireguard_ipv6_network = System.fetch_env!("WIREGUARD_IPV6_NETWORK")
wireguard_ipv6_address = System.fetch_env!("WIREGUARD_IPV6_ADDRESS")
wireguard_mtu = System.fetch_env!("WIREGUARD_MTU")
wireguard_endpoint = System.get_env("WIREGUARD_ENDPOINT", host)
telemetry_enabled = FzString.to_boolean(System.fetch_env!("TELEMETRY_ENABLED"))
telemetry_id = System.fetch_env!("TELEMETRY_ID")
guardian_secret_key = System.fetch_env!("GUARDIAN_SECRET_KEY")
disable_vpn_on_oidc_error = FzString.to_boolean(System.fetch_env!("DISABLE_VPN_ON_OIDC_ERROR"))
auto_create_oidc_users = FzString.to_boolean(System.fetch_env!("AUTO_CREATE_OIDC_USERS"))
secure = FzString.to_boolean(System.get_env("SECURE_COOKIES", "true"))
encryption_key = System.fetch_env!("DATABASE_ENCRYPTION_KEY")
secret_key_base = System.fetch_env!("SECRET_KEY_BASE")
live_view_signing_salt = System.fetch_env!("LIVE_VIEW_SIGNING_SALT")
cookie_signing_salt = System.fetch_env!("COOKIE_SIGNING_SALT")
cookie_encryption_salt = System.fetch_env!("COOKIE_ENCRYPTION_SALT")
telemetry_id = System.fetch_env!("TELEMETRY_ID")
# OPTIONAL
wireguard_private_key_path =
System.get_env("WIREGUARD_PRIVATE_KEY_PATH", "/var/firezone/private_key")
saml_keyfile_path = System.get_env("SAML_KEYFILE_PATH", "/var/firezone/saml.key")
saml_certfile_path = System.get_env("SAML_CERTFILE_PATH", "/var/firezone/saml.crt")
database_name = System.get_env("DATABASE_NAME", "firezone")
database_user = System.get_env("DATABASE_USER", "postgres")
database_host = System.get_env("DATABASE_HOST", "postgres")
database_port = String.to_integer(System.get_env("DATABASE_PORT", "5432"))
database_pool = String.to_integer(System.get_env("DATABASE_POOL", "10"))
database_ssl = FzString.to_boolean(System.get_env("DATABASE_SSL", "false"))
database_ssl_opts = Jason.decode!(System.get_env("DATABASE_SSL_OPTS", "{}"))
database_parameters = Jason.decode!(System.get_env("DATABASE_PARAMETERS", "{}"))
phoenix_listen_address = System.get_env("PHOENIX_LISTEN_ADDRESS", "0.0.0.0")
phoenix_port = String.to_integer(System.get_env("PHOENIX_PORT", "13000"))
external_trusted_proxies = Jason.decode!(System.get_env("EXTERNAL_TRUSTED_PROXIES", "[]"))
private_clients = Jason.decode!(System.get_env("PRIVATE_CLIENTS", "[]"))
wireguard_interface_name = System.get_env("WIREGUARD_INTERFACE_NAME", "wg-firezone")
wireguard_port = String.to_integer(System.get_env("WIREGUARD_PORT", "51820"))
nft_path = System.get_env("NFT_PATH", "nft")
egress_interface = System.get_env("EGRESS_INTERFACE", "eth0")
wireguard_dns = System.get_env("WIREGUARD_DNS", "1.1.1.1, 1.0.0.1")
wireguard_allowed_ips = System.get_env("WIREGUARD_ALLOWED_IPS", "0.0.0.0/0, ::/0")
wireguard_persistent_keepalive = System.get_env("WIREGUARD_PERSISTENT_KEEPALIVE", "0")
wireguard_ipv4_enabled = FzString.to_boolean(System.get_env("WIREGUARD_IPV4_ENABLED", "true"))
wireguard_ipv4_masquerade =
FzString.to_boolean(System.get_env("WIREGUARD_IPV4_MASQUERADE", "true"))
wireguard_ipv6_masquerade =
FzString.to_boolean(System.get_env("WIREGUARD_IPV6_MASQUERADE", "true"))
wireguard_ipv4_network = System.get_env("WIREGUARD_IPV4_NETWORK", "10.3.2.0/24")
wireguard_ipv4_address = System.get_env("WIREGUARD_IPV4_ADDRESS", "10.3.2.1")
wireguard_ipv6_enabled = FzString.to_boolean(System.get_env("WIREGUARD_IPV6_ENABLED", "true"))
wireguard_ipv6_network = System.get_env("WIREGUARD_IPV6_NETWORK", "fd00::3:2:0/120")
wireguard_ipv6_address = System.get_env("WIREGUARD_IPV6_ADDRESS", "fd00::3:2:1")
wireguard_mtu = System.get_env("WIREGUARD_MTU", "1280")
wireguard_endpoint = System.get_env("WIREGUARD_ENDPOINT", host)
telemetry_enabled = FzString.to_boolean(System.get_env("TELEMETRY_ENABLED", "true"))
disable_vpn_on_oidc_error =
FzString.to_boolean(System.get_env("DISABLE_VPN_ON_OIDC_ERROR", "false"))
cookie_secure = FzString.to_boolean(System.get_env("SECURE_COOKIES", "true"))
allow_unprivileged_device_management =
FzString.to_boolean(System.fetch_env!("ALLOW_UNPRIVILEGED_DEVICE_MANAGEMENT"))
FzString.to_boolean(System.get_env("ALLOW_UNPRIVILEGED_DEVICE_MANAGEMENT", "true"))
allow_unprivileged_device_configuration =
FzString.to_boolean(System.fetch_env!("ALLOW_UNPRIVILEGED_DEVICE_CONFIGURATION"))
FzString.to_boolean(System.get_env("ALLOW_UNPRIVILEGED_DEVICE_CONFIGURATION", "true"))
# Outbound Email
from_email = System.get_env("OUTBOUND_EMAIL_FROM")
@@ -81,10 +98,10 @@ if config_env() == :prod do
end
# Local auth
local_auth_enabled = FzString.to_boolean(System.fetch_env!("LOCAL_AUTH_ENABLED"))
local_auth_enabled = FzString.to_boolean(System.get_env("LOCAL_AUTH_ENABLED", "true"))
max_devices_per_user =
System.fetch_env!("MAX_DEVICES_PER_USER")
System.get_env("MAX_DEVICES_PER_USER", "10")
|> String.to_integer()
|> FzInteger.clamp(0, 100)
@@ -96,22 +113,14 @@ if config_env() == :prod do
end
connectivity_checks_enabled =
FzString.to_boolean(System.fetch_env!("CONNECTIVITY_CHECKS_ENABLED")) &&
FzString.to_boolean(System.get_env("CONNECTIVITY_CHECKS_ENABLED", "true")) &&
System.get_env("CI") != "true"
connectivity_checks_interval =
System.fetch_env!("CONNECTIVITY_CHECKS_INTERVAL")
System.get_env("CONNECTIVITY_CHECKS_INTERVAL", "3600")
|> String.to_integer()
|> FzInteger.clamp(60, 86_400)
# secrets
encryption_key = System.fetch_env!("DATABASE_ENCRYPTION_KEY")
secret_key_base = System.fetch_env!("SECRET_KEY_BASE")
live_view_signing_salt = System.fetch_env!("LIVE_VIEW_SIGNING_SALT")
cookie_signing_salt = System.fetch_env!("COOKIE_SIGNING_SALT")
cookie_encryption_salt = System.fetch_env!("COOKIE_ENCRYPTION_SALT")
cookie_secure = secure
# Password is not needed if using bundled PostgreSQL, so use nil if it's not set.
database_password = System.get_env("DATABASE_PASSWORD")
@@ -205,10 +214,11 @@ if config_env() == :prod do
secret_key: guardian_secret_key
config :fz_http,
saml_certfile_path: saml_certfile_path,
saml_keyfile_path: saml_keyfile_path,
external_trusted_proxies: external_trusted_proxies,
private_clients: private_clients,
disable_vpn_on_oidc_error: disable_vpn_on_oidc_error,
auto_create_oidc_users: auto_create_oidc_users,
cookie_signing_salt: cookie_signing_salt,
cookie_encryption_salt: cookie_encryption_salt,
cookie_secure: cookie_secure,
@@ -255,7 +265,7 @@ if config_env() == :prod do
end
# OIDC Auth
auth_oidc_env = System.get_env("AUTH_OIDC")
auth_oidc_env = System.get_env("AUTH_OIDC_JSON", "{}")
if config_env() != :test && auth_oidc_env do
config :fz_http, :openid_connect_providers, auth_oidc_env

View File

@@ -78,6 +78,8 @@ config :fz_http, :openid_connect_providers, """
}
"""
config :fz_http, :saml_identity_providers, %{"test" => %{"label" => "SAML"}}
# Provide mock for HTTPClient
config :fz_http, :openid_connect, OpenIDConnect.Mock

View File

@@ -15,11 +15,11 @@ services:
caddy:
image: caddy:2
volumes:
- /data/caddy:/data/caddy
- /data/firezone/caddy:/data/caddy
ports:
- 80:80
- 443:443
command: caddy reverse-proxy --to firezone:4000 --from ${EXTERNAL_URL?err}
command: caddy reverse-proxy --to firezone:13000 --from ${EXTERNAL_URL?err}
deploy:
<<: *default-deploy
@@ -27,15 +27,21 @@ services:
image: firezone/firezone
ports:
- 51820:51820/udp
volumes:
# Persist private key through containers, the parent path to WIREGUARD_PRIVATE_KEY_PATH.
- /data/firezone:/var/firezone
env_file:
# This should contain a list of env vars for configuring Firezone.
# See https://docs.firezone.dev/reference/env-vars for more info.
- .env
volumes:
# IMPORTANT: Persists WireGuard private key and other data. If
# /var/firezone/private_key exists when Firezone starts, it is
# used as the WireGuard private. Otherwise, one is generated.
- /data/firezone/firezone:/var/firezone
cap_add:
# Needed for WireGuard and firewall support.
- NET_ADMIN
- SYS_MODULE
sysctls:
# Needed for masquerading and NAT.
- net.ipv6.conf.all.disable_ipv6=0
- net.ipv4.ip_forward=1
- net.ipv6.conf.all.forwarding=1
@@ -45,11 +51,10 @@ services:
<<: *default-deploy
postgres:
image: postgres:15rc1
image: postgres:15rc2
volumes:
- /data/postgres:/var/lib/postgresql/data
- /data/firezone/postgres:/var/lib/postgresql/data
environment:
# same value as ## DB section above
POSTGRES_DB: ${DATABASE_NAME:-firezone}
POSTGRES_USER: ${DATABASE_USER:-postgres}
POSTGRES_PASSWORD: ${DATABASE_PASSWORD:?err}

3
docs/.gitignore vendored
View File

@@ -1,6 +1,9 @@
# Dependencies
/node_modules
# Generated OpenAPI docs
/docs/reference/REST\ API/
# Production
/build

View File

@@ -1,53 +0,0 @@
---
title: Overview
sidebar_position: 1
---
[Firezone](https://firezone.dev) is an open source, self-hosted VPN server and
egress firewall for Linux. Use it to **quickly and easily** secure access to
your private network and internal applications from a simple Web UI.
![Architecture](https://user-images.githubusercontent.com/52545545/183804397-ae81ca4e-6972-41f9-80d4-b431a077119d.png)
These docs explain how to deploy, configure, and use Firezone.
## Quick Start
1. [Deploy](./deploy): A step-by-step walkthrough of
setting up Firezone. Start here if you are new.
1. [Administer](./administer/): This section relates
directly to configuring the server instance.
1. [User Guides](./user-guides): Useful guides to help you
learn how to use Firezone and troubleshoot common issues. Consult this section
after you successfully deploy the Firezone server.
## Common Configuration Guides
1. [Split Tunneling](./user-guides/use-cases/split-tunnel):
Only route traffic to certain IP ranges through the VPN.
1. [Whitelisting with VPN](./user-guides/use-cases/nat-gateway):
Configure a VPN server with a static IP address.
1. [Reverse Tunnels](./user-guides/use-cases/reverse-tunnel):
Establish tunnels between multiple peers.
## Get Help
If you're looking for help installing, configuring, or using Firezone, we're
happy to help.
1. [Discussion Forums](https://discourse.firez.one/): Ask questions, report
bugs, and suggest features.
1. [Public Slack Group](https://join.slack.com/t/firezone-users/shared_invite/zt-111043zus-j1lP_jP5ohv52FhAayzT6w):
Join live discussions, meet other users, and get to know the contributors.
1. [Email Us](mailto:team@firezone.dev): We read every email and respond as soon
as we can.
## Contribute to Firezone
We deeply appreciate any and all contributions to the project and do our best to
ensure your contribution is included. To get started, see
[CONTRIBUTING.md](https://github.com/firezone/firezone/blob/master/CONTRIBUTING.md).
<!-- Leaving these disabled until they're ready -->
<!-- <feedback /> -->
<!-- <newsletter /> -->

61
docs/docs/README.mdx Normal file
View File

@@ -0,0 +1,61 @@
---
title: Overview
sidebar_position: 1
---
[Firezone](https://firezone.dev) is an open-source secure remote access
platform that can be deployed on your own infrastructure in minutes.
Use it to **quickly and easily** secure access to
your private network and internal applications from an intuitive web UI.
![Architecture](https://user-images.githubusercontent.com/52545545/183804397-ae81ca4e-6972-41f9-80d4-b431a077119d.png)
These docs explain how to deploy, configure, and use Firezone.
## Quick Start
1. [Deploy](deploy): A step-by-step walk-through setting up Firezone.
Start here if you are new.
1. [Authenticate](authenticate): Set up authentication using local
email/password, OpenID Connect, or SAML 2.0 and optionally enable
TOTP-based MFA.
1. [Administer](administer): Day to day administration of the Firezone
server.
1. [User Guides](user-guides): Useful guides to help you learn how to use
Firezone and troubleshoot common issues. Consult this section
after you successfully deploy the Firezone server.
## Common Configuration Guides
1. [Split Tunneling](./user-guides/use-cases/split-tunnel):
Only route traffic to certain IP ranges through the VPN.
1. [Setting up a NAT Gateway with a Static IP](./user-guides/use-cases/nat-gateway):
Configure Firezone with a static IP address to provide
a single egress IP for your team's traffic.
1. [Reverse Tunnels](./user-guides/use-cases/reverse-tunnel):
Establish tunnels between multiple peers.
## Get Help
If you're looking for help installing, configuring, or using Firezone, check our
community support options:
1. [Discussion Forums](https://discourse.firez.one/): Ask questions, report
bugs, and suggest features.
1. [Public Slack Group](https://join.slack.com/t/firezone-users/shared_invite/zt-111043zus-j1lP_jP5ohv52FhAayzT6w):
Join live discussions, meet other users, and get to know the contributors.
1. [Open a PR](https://github.com/firezone/firezone/issues): Contribute a bugfix
or make a contribution to Firezone.
If you need help deploying or maintaining Firezone for your business, consider
[contacting us about our paid support plan](https://firezone.dev/contact/sales).
## Contribute to Firezone
We deeply appreciate any and all contributions to the project and do our best to
ensure your contribution is included. To get started, see [CONTRIBUTING.md
](https://github.com/firezone/firezone/blob/master/CONTRIBUTING.md).
<!-- Leaving these disabled until they're ready -->
<!-- <feedback /> -->
<!-- <newsletter /> -->

View File

@@ -1,15 +0,0 @@
---
title: Configure
sidebar_position: 1
---
Firezone leverages [Chef Omnibus](https://github.com/chef/omnibus) to handle
release packaging, process supervision, log management, and more.
The main configuration file is written in [Ruby](https://ruby-lang.org) and can
be found at `/etc/firezone/firezone.rb`. Changing this file **requires
re-running** `sudo firezone-ctl reconfigure` which triggers Chef to pick up the
changes and apply them to the running system.
For an exhaustive list of configuration variables and their descriptions, see the
[configuration file reference](../reference/configuration-file).

View File

@@ -1,65 +0,0 @@
---
title: Manage Installation
sidebar_position: 2
---
Your Firezone installation can be managed via the `firezone-ctl` command, as
shown below. Most subcommands require prefixing with `sudo`.
```text
root@demo:~# firezone-ctl
I don't know that command.
omnibus-ctl: command (subcommand)
General Commands:
cleanse
Delete *all* firezone data, and start from scratch.
create-or-reset-admin
Resets the password for admin with email specified by default['firezone']['admin_email'] or creates a new admin if that email doesn't exist.
help
Print this help message.
reconfigure
Reconfigure the application.
reset-network
Resets nftables, WireGuard interface, and routing table back to Firezone defaults.
show-config
Show the configuration that would be generated by reconfigure.
teardown-network
Removes WireGuard interface and firezone nftables table.
force-cert-renewal
Force certificate renewal now even if it hasn\'t expired.
stop-cert-renewal
Removes cronjob that renews certificates.
uninstall
Kill all processes and uninstall the process supervisor (data will be preserved).
version
Display current version of Firezone
Service Management Commands:
graceful-kill
Attempt a graceful stop, then SIGKILL the entire process group.
hup
Send the services a HUP.
int
Send the services an INT.
kill
Send the services a KILL.
once
Start the services if they are down. Do not restart them if they stop.
restart
Stop the services if they are running, then start them again.
service-list
List all the services (enabled services appear with a *.)
start
Start services if they are down, and restart them if they stop.
status
Show the status of all the services.
stop
Stop the services, and do not restart them.
tail
Watch the service logs of all enabled services.
term
Send the services a TERM.
usr1
Send the services a USR1.
usr2
Send the services a USR2.
```

View File

@@ -0,0 +1,74 @@
---
title: Migrate to Docker
sidebar_position: 2
---
Chef Infra Client, the configuration system Chef Omnibus relies on, has been
[scheduled for End-of-Life in 2024](https://docs.chef.io/versions/#supported-commercial-distributions).
As such, Omnibus-based deployments
will be deprecated in a future version of Firezone.
Follow this guide to migrate from an Omnibus-based deployment to a Docker-based
deployment. In most cases this can be done with minimal downtime and without
requiring you to regenerate WireGuard configurations for each device.
Estimated time to complete: **2 hours**.
## Steps to Migrate
1. **Back up** your server. This ensures you have a working state to roll back to
in case anything goes wrong. At a _bare minimum_ you'll want to back up the
[file and directories Firezone uses
](../reference/file-and-directory-locations), but we recommend taking a full
snapshot if possible.
1. Ensure you're running the latest version of Firezone. See our [upgrade guide
](upgrade) if not.
1. Install the latest version of [**Docker Server**
](https://docs.docker.com/engine/install/#server) and [Docker Compose
](https://docs.docker.com/compose/install/linux/#install-compose)
for your distro. We highly recommend using Docker Server for Linux. Docker
Desktop will probably work too, but is discouraged at this time
because it rewrites packets under some conditions and may cause unexpected
issues with Firezone.
1. Stop Firezone:
```bash
sudo firezone-ctl stop
```
1. Download and run the migration script:
```bash
sudo -E bash -c "$(curl -fsSL https://github.com/firezone/firezone/raw/master/scripts/docker_migrate.sh)"
```
This will ask you a few questions, then attempt to migrate your installation to
Docker.
1. If all goes well, you should now be able to bring the Docker services up:
```bash
docker-compose up -d
```
## Rolling Back
If anything goes wrong, you can abort the migration by simply bringing the Docker
services down and the Omnibus ones back up:
```bash
docker-compose down
sudo firezone-ctl start
```
If you've found a bug, please [open a GitHub issue](
https://github.com/firezone/firezone/issues) with the error output and
any steps needed to reproduce.
## Get Help
If you need help migrating from Omnibus to Docker, check our community
support options:
1. [Discussion Forums](https://discourse.firez.one/): Ask questions, report
bugs, and suggest features.
1. [Public Slack Group](https://join.slack.com/t/firezone-users/shared_invite/zt-111043zus-j1lP_jP5ohv52FhAayzT6w):
Join live discussions, meet other users, and get to know the contributors.
If you'd like dedicated support migrating your installation from Omnibus
to Docker, consider [contacting us about our paid support plan
](https://firezone.dev/contact/sales).

View File

@@ -0,0 +1,80 @@
---
title: Regenerate Secret Keys
sidebar_position: 7
---
When you install Firezone, secrets are generated for encrypting database
fields, securing WireGuard tunnels, securing cookie sessions, and more.
If you're looking to regenerate one or more of these secrets, it's possible
to do so using the same bootstrap scripts that were used when installing
Firezone.
## Regenerate Secrets
:::warning
Replacing the `DATABASE_ENCRYPTION_KEY` will render all encrypted data in the
database useless. This **will** break your Firezone install unless you are
starting with an empty database. You have been warned.
:::
:::caution
Replacing `GUARDIAN_SECRET_KEY`, `SECRET_KEY_BASE`, `LIVE_VIEW_SIGNING_SALT`,
`COOKIE_SIGNING_SALT`, or `COOKIE_ENCRYPTION_SALT`
will render all browser sessions and JWTs useless.
:::
Use the procedure below to regenerate secrets:
<Tabs>
<TabItem value="docker" label="Docker" default>
Navigate to the Firezone installation directory, then:
```bash
mv .env .env.bak
docker run firezone/firezone bin/gen-env > .env
```
</TabItem>
<TabItem value="omnibus" label="Omnibus">
```bash
mv /etc/firezone/secrets.json /etc/firezone/secrets.bak.json
sudo firezone-ctl reconfigure
```
</TabItem>
</Tabs>
## Regenerate WireGuard Private Key
:::warning
Replacing the WireGuard private key will render all existing device configs
useless. Only do so if you're prepared to also regenerate device configs
after regenerating the WireGuard private key.
:::
To regenerate WireGuard private key, simply move or rename the private key file.
Firezone will generate a new one on next start.
<Tabs>
<TabItem value="docker" label="Docker" default>
```bash
docker-compose stop firezone
sudo mv /data/firezone/private_key /data/firezone/private_key.bak
docker-compose start firezone
```
</TabItem>
<TabItem value="omnibus" label="Omnibus">
```bash
sudo firezone-ctl stop phoenix
sudo mv /var/opt/firezone/cache/wg_private_key /var/opt/firezone/cache/wg_private_key.bak
sudo firezone-ctl start phoenix
```
</TabItem>
</Tabs>

View File

@@ -1,78 +0,0 @@
---
title: Running SQL Queries
sidebar_position: 7
---
Firezone bundles a Postgresql server and matching `psql` utility that can be
used from the local shell like so:
```shell
/opt/firezone/embedded/bin/psql \
-U firezone \
-d firezone \
-h localhost \
-p 15432 \
-c "SQL_STATEMENT"
```
This can be useful for debugging or troubleshooting purposes. It can also be
used to modify Firezone configuration data, but **this can have unintended
consequences**. We recommend using the UI (or upcoming API) <!-- XXX: Remove
"upcoming API" when API is implemented --> whenever possible.
Some examples of common tasks:
* [Listing all users](#listing-all-users)
* [Listing all devices](#listing-all-devices)
* [Changing a user's role](#changing-a-users-role)
* [Backing up the DB](#backing-up-the-db)
#### Listing all users
```shell
/opt/firezone/embedded/bin/psql \
-U firezone \
-d firezone \
-h localhost \
-p 15432 \
-c "SELECT * FROM users;"
```
#### Listing all devices
```shell
/opt/firezone/embedded/bin/psql \
-U firezone \
-d firezone \
-h localhost \
-p 15432 \
-c "SELECT * FROM devices;"
```
#### Changing a user's role
Set role to `'admin'` or `'unprivileged'`:
```shell
/opt/firezone/embedded/bin/psql \
-U firezone \
-d firezone \
-h localhost \
-p 15432 \
-c "UPDATE users SET role = 'admin' WHERE email = 'user@example.com';"
```
#### Backing up the DB
The `pg_dump` utility is also bundled; this can be used to take
consistent backups of the database. To dump a copy of the database in the
standard SQL query format execute it like this (replace `/path/to/backup.sql`
with the location to create the SQL file):
```shell
/opt/firezone/embedded/bin/pg_dump \
-U firezone \
-d firezone \
-h localhost \
-p 15432 > /path/to/backup.sql
```

View File

@@ -1,63 +0,0 @@
---
title: Security Considerations
sidebar_position: 6
---
**Disclaimer**: Firezone is still beta software. The codebase has not yet
received a formal security audit. For highly sensitive and mission-critical
production deployments, we recommend limiting access to the web interface, as
detailed [below](#production-deployments).
## List of services and ports
Shown below is a table of ports used by Firezone services.
| Service | Default port | Listen address | Description |
| ------ | --------- | ------- | --------- |
| Nginx | `443` | `all` | Public HTTPS port for administering Firezone and facilitating authentication. |
| Nginx | `80` | `all` | Public HTTP port used for ACME. Disabled when ACME is disabled. |
| WireGuard | `51820` | `all` | Public WireGuard port used for VPN sessions. |
| Postgresql | `15432` | `127.0.0.1` | Local-only port used for bundled Postgresql server. |
| Phoenix | `13000` | `127.0.0.1` | Local-only port used by upstream elixir app server. |
## Production deployments
For production and public-facing deployments where a single administrator
will be responsible for generating and distributing device configurations to
end users, we advise you to consider limiting access to Firezone's publicly
exposed web UI (by default ports `443/tcp` and `80/tcp`)
and instead use the WireGuard tunnel itself to manage Firezone.
For example, assuming an administrator has generated a device configuration and
established a tunnel with local WireGuard address `10.3.2.2`, the following `ufw`
configuration would allow the administrator the ability to reach the Firezone web
UI on the default `10.3.2.1` tunnel address for the server's `wg-firezone` interface:
```text
root@demo:~# ufw status verbose
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), allow (routed)
New profiles: skip
To Action From
-- ------ ----
22/tcp ALLOW IN Anywhere
51820/udp ALLOW IN Anywhere
Anywhere ALLOW IN 10.3.2.2
22/tcp (v6) ALLOW IN Anywhere (v6)
51820/udp (v6) ALLOW IN Anywhere (v6)
```
This would leave only `22/tcp` exposed for SSH access to manage the server (optional),
and `51820/udp` exposed in order to establish WireGuard tunnels.
:::note
This type of configuration has not been fully tested with SSO
authentication and may it to break or behave unexpectedly.
:::
## Reporting Security Issues
To report any security-related bugs, see [our security bug reporting policy
](https://github.com/firezone/firezone/blob/master/SECURITY.md).

Some files were not shown because too many files have changed in this diff Show More