mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 10:18:54 +00:00
0.6.0 (#1004)
* 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:
@@ -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, []},
|
||||
|
||||
#
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
localhost {
|
||||
log
|
||||
|
||||
reverse_proxy * firezone:4000
|
||||
reverse_proxy * firezone:13000
|
||||
|
||||
encode gzip
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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/*"]
|
||||
]
|
||||
|
||||
1
.github/workflows/publish_docs.yml
vendored
1
.github/workflows/publish_docs.yml
vendored
@@ -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
|
||||
|
||||
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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 ./
|
||||
|
||||
16
README.md
16
README.md
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
187
apps/fz_http/assets/package-lock.json
generated
187
apps/fz_http/assets/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
47
apps/fz_http/lib/fz_http/conf/oidc_config.ex
Normal file
47
apps/fz_http/lib/fz_http/conf/oidc_config.ex
Normal 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
|
||||
22
apps/fz_http/lib/fz_http/conf/saml_config.ex
Normal file
22
apps/fz_http/lib/fz_http/conf/saml_config.ex
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
58
apps/fz_http/lib/fz_http/saml/start_proxy.ex
Normal file
58
apps/fz_http/lib/fz_http/saml/start_proxy.ex
Normal 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
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
35
apps/fz_http/lib/fz_http_web/controllers/debug_controller.ex
Normal file
35
apps/fz_http/lib/fz_http_web/controllers/debug_controller.ex
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) %>
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
<- Back to devices
|
||||
</.link>
|
||||
</div>
|
||||
<%= render FzHttpWeb.SharedView, "show_device.html", assigns %>
|
||||
<%= render(FzHttpWeb.SharedView, "show_device.html", assigns) %>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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>
|
||||
|
||||
@@ -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 """
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)}>
|
||||
<- 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
13
apps/fz_http/lib/fz_http_web/plug/samly_target_url.ex
Normal file
13
apps/fz_http/lib/fz_http_web/plug/samly_target_url.ex
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
@@ -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">
|
||||
🎉
|
||||
|
||||
<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">
|
||||
🎉
|
||||
|
||||
<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>
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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() %>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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"},
|
||||
|
||||
21
apps/fz_http/priv/cert/saml_selfsigned.pem
Normal file
21
apps/fz_http/priv/cert/saml_selfsigned.pem
Normal 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-----
|
||||
27
apps/fz_http/priv/cert/saml_selfsigned_key.pem
Normal file
27
apps/fz_http/priv/cert/saml_selfsigned_key.pem
Normal 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-----
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;lt;test&amp;amp;gt;&amp;amp;lt;/test&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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
3
docs/.gitignore
vendored
@@ -1,6 +1,9 @@
|
||||
# Dependencies
|
||||
/node_modules
|
||||
|
||||
# Generated OpenAPI docs
|
||||
/docs/reference/REST\ API/
|
||||
|
||||
# Production
|
||||
/build
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
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
61
docs/docs/README.mdx
Normal 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.
|
||||
|
||||

|
||||
|
||||
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 /> -->
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
```
|
||||
74
docs/docs/administer/migrate.mdx
Normal file
74
docs/docs/administer/migrate.mdx
Normal 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).
|
||||
80
docs/docs/administer/regen-keys.mdx
Normal file
80
docs/docs/administer/regen-keys.mdx
Normal 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>
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
Reference in New Issue
Block a user