mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-24 11:16:38 +00:00
Compare commits
31 Commits
update-get
...
v0.22.7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac0b7dc8cb | ||
|
|
e586eca16c | ||
|
|
892db25021 | ||
|
|
da75a76d41 | ||
|
|
3ac32fd78a | ||
|
|
3aa657599b | ||
|
|
d4e9087f94 | ||
|
|
da8447a67d | ||
|
|
8e3bcd57a2 | ||
|
|
4572c6c1f8 | ||
|
|
01f2b0ecb7 | ||
|
|
442ba7cbc8 | ||
|
|
6c2b364966 | ||
|
|
0f0c7ec2ed | ||
|
|
2dec016201 | ||
|
|
06125acb8d | ||
|
|
a9b9b3fa0a | ||
|
|
cdf57275b7 | ||
|
|
e5e69b1f75 | ||
|
|
8eca83f3cb | ||
|
|
973316d194 | ||
|
|
a0a6ced148 | ||
|
|
0fc6c477a9 | ||
|
|
401a462398 | ||
|
|
a3839a6ef7 | ||
|
|
8aa4f240c7 | ||
|
|
d9686bae92 | ||
|
|
24e19ae287 | ||
|
|
74fde0ea2c | ||
|
|
890e09b787 | ||
|
|
48098c994d |
35
.github/workflows/install-script-test.yml
vendored
Normal file
35
.github/workflows/install-script-test.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
name: Test installation
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- "release_files/install.sh"
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
jobs:
|
||||||
|
test-install-script:
|
||||||
|
strategy:
|
||||||
|
max-parallel: 2
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, macos-latest]
|
||||||
|
skip_ui_mode: [true, false]
|
||||||
|
install_binary: [true, false]
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: run install script
|
||||||
|
env:
|
||||||
|
SKIP_UI_APP: ${{ matrix.skip_ui_mode }}
|
||||||
|
USE_BIN_INSTALL: ${{ matrix.install_binary }}
|
||||||
|
run: |
|
||||||
|
[ "$SKIP_UI_APP" == "false" ] && export XDG_CURRENT_DESKTOP="none"
|
||||||
|
cat release_files/install.sh | sh -x
|
||||||
|
|
||||||
|
- name: check cli binary
|
||||||
|
run: command -v netbird
|
||||||
60
.github/workflows/install-test-darwin.yml
vendored
60
.github/workflows/install-test-darwin.yml
vendored
@@ -1,60 +0,0 @@
|
|||||||
name: Test installation Darwin
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- "release_files/install.sh"
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
jobs:
|
|
||||||
install-cli-only:
|
|
||||||
runs-on: macos-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Rename brew package
|
|
||||||
if: ${{ matrix.check_bin_install }}
|
|
||||||
run: mv /opt/homebrew/bin/brew /opt/homebrew/bin/brew.bak
|
|
||||||
|
|
||||||
- name: Run install script
|
|
||||||
run: |
|
|
||||||
sh ./release_files/install.sh
|
|
||||||
env:
|
|
||||||
SKIP_UI_APP: true
|
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
run: |
|
|
||||||
if ! command -v netbird &> /dev/null; then
|
|
||||||
echo "Error: netbird is not installed"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
install-all:
|
|
||||||
runs-on: macos-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Rename brew package
|
|
||||||
if: ${{ matrix.check_bin_install }}
|
|
||||||
run: mv /opt/homebrew/bin/brew /opt/homebrew/bin/brew.bak
|
|
||||||
|
|
||||||
- name: Run install script
|
|
||||||
run: |
|
|
||||||
sh ./release_files/install.sh
|
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
run: |
|
|
||||||
if ! command -v netbird &> /dev/null; then
|
|
||||||
echo "Error: netbird is not installed"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ $(mdfind "kMDItemContentType == 'com.apple.application-bundle' && kMDItemFSName == '*NetBird UI.app'") ]]; then
|
|
||||||
echo "Error: NetBird UI is not installed"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
38
.github/workflows/install-test-linux.yml
vendored
38
.github/workflows/install-test-linux.yml
vendored
@@ -1,38 +0,0 @@
|
|||||||
name: Test installation Linux
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- "release_files/install.sh"
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
jobs:
|
|
||||||
install-cli-only:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
check_bin_install: [true, false]
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Rename apt package
|
|
||||||
if: ${{ matrix.check_bin_install }}
|
|
||||||
run: |
|
|
||||||
sudo mv /usr/bin/apt /usr/bin/apt.bak
|
|
||||||
sudo mv /usr/bin/apt-get /usr/bin/apt-get.bak
|
|
||||||
|
|
||||||
- name: Run install script
|
|
||||||
run: |
|
|
||||||
sh ./release_files/install.sh
|
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
run: |
|
|
||||||
if ! command -v netbird &> /dev/null; then
|
|
||||||
echo "Error: netbird is not installed"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
10
.github/workflows/release.yml
vendored
10
.github/workflows/release.yml
vendored
@@ -7,6 +7,16 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
pull_request:
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'go.mod'
|
||||||
|
- 'go.sum'
|
||||||
|
- '.goreleaser.yml'
|
||||||
|
- '.goreleaser_ui.yaml'
|
||||||
|
- '.goreleaser_ui_darwin.yaml'
|
||||||
|
- '.github/workflows/release.yml'
|
||||||
|
- 'release_files/**'
|
||||||
|
- '**/Dockerfile'
|
||||||
|
- '**/Dockerfile.*'
|
||||||
|
|
||||||
env:
|
env:
|
||||||
SIGN_PIPE_VER: "v0.0.8"
|
SIGN_PIPE_VER: "v0.0.8"
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
name: Test Docker Compose Linux
|
name: Test Infrastructure files
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
pull_request:
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'infrastructure_files/**'
|
||||||
|
- '.github/workflows/test-infrastructure-files.yml'
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
|
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test-docker-compose:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Install jq
|
- name: Install jq
|
||||||
@@ -35,7 +37,7 @@ jobs:
|
|||||||
${{ runner.os }}-go-
|
${{ runner.os }}-go-
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: cp setup.env
|
- name: cp setup.env
|
||||||
run: cp infrastructure_files/tests/setup.env infrastructure_files/
|
run: cp infrastructure_files/tests/setup.env infrastructure_files/
|
||||||
@@ -121,3 +123,28 @@ jobs:
|
|||||||
count=$(docker compose ps --format json | jq '.[] | select(.Project | contains("infrastructure_files")) | .State' | grep -c running)
|
count=$(docker compose ps --format json | jq '.[] | select(.Project | contains("infrastructure_files")) | .State' | grep -c running)
|
||||||
test $count -eq 4
|
test $count -eq 4
|
||||||
working-directory: infrastructure_files
|
working-directory: infrastructure_files
|
||||||
|
|
||||||
|
test-getting-started-script:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Install jq
|
||||||
|
run: sudo apt-get install -y jq
|
||||||
|
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: run script
|
||||||
|
run: NETBIRD_DOMAIN=use-ip bash -x infrastructure_files/getting-started-with-zitadel.sh
|
||||||
|
|
||||||
|
- name: test Caddy file gen
|
||||||
|
run: test -f Caddyfile
|
||||||
|
- name: test docker-compose file gen
|
||||||
|
run: test -f docker-compose.yml
|
||||||
|
- name: test management.json file gen
|
||||||
|
run: test -f management.json
|
||||||
|
- name: test turnserver.conf file gen
|
||||||
|
run: test -f turnserver.conf
|
||||||
|
- name: test zitadel.env file gen
|
||||||
|
run: test -f zitadel.env
|
||||||
|
- name: test dashboard.env file gen
|
||||||
|
run: test -f dashboard.env
|
||||||
@@ -377,3 +377,13 @@ uploads:
|
|||||||
target: https://pkgs.wiretrustee.com/yum/{{ .Arch }}{{ if .Arm }}{{ .Arm }}{{ end }}
|
target: https://pkgs.wiretrustee.com/yum/{{ .Arch }}{{ if .Arm }}{{ .Arm }}{{ end }}
|
||||||
username: dev@wiretrustee.com
|
username: dev@wiretrustee.com
|
||||||
method: PUT
|
method: PUT
|
||||||
|
|
||||||
|
checksum:
|
||||||
|
extra_files:
|
||||||
|
- glob: ./infrastructure_files/getting-started-with-zitadel.sh
|
||||||
|
- glob: ./release_files/install.sh
|
||||||
|
|
||||||
|
release:
|
||||||
|
extra_files:
|
||||||
|
- glob: ./infrastructure_files/getting-started-with-zitadel.sh
|
||||||
|
- glob: ./release_files/install.sh
|
||||||
93
README.md
93
README.md
@@ -1,6 +1,6 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<strong>:hatching_chick: New Release! Peer expiration.</strong>
|
<strong>:hatching_chick: New Release! Self-hosting in under 5 min.</strong>
|
||||||
<a href="https://github.com/netbirdio/netbird/releases">
|
<a href="https://github.com/netbirdio/netbird#quickstart-with-self-hosted-netbird">
|
||||||
Learn more
|
Learn more
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<strong>
|
<strong>
|
||||||
Start using NetBird at <a href="https://app.netbird.io/">app.netbird.io</a>
|
Start using NetBird at <a href="https://netbird.io/pricing">netbird.io</a>
|
||||||
<br/>
|
<br/>
|
||||||
See <a href="https://netbird.io/docs/">Documentation</a>
|
See <a href="https://netbird.io/docs/">Documentation</a>
|
||||||
<br/>
|
<br/>
|
||||||
@@ -36,47 +36,62 @@
|
|||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
**NetBird is an open-source VPN management platform built on top of WireGuard® making it easy to create secure private networks for your organization or home.**
|
**NetBird combines a configuration-free peer-to-peer private network and a centralized access control system in a single platform, making it easy to create secure private networks for your organization or home.**
|
||||||
|
|
||||||
It requires zero configuration effort leaving behind the hassle of opening ports, complex firewall rules, VPN gateways, and so forth.
|
**Connect.** NetBird creates a WireGuard-based overlay network that automatically connects your machines over an encrypted tunnel, leaving behind the hassle of opening ports, complex firewall rules, VPN gateways, and so forth.
|
||||||
|
|
||||||
NetBird uses [NAT traversal techniques](https://en.wikipedia.org/wiki/Interactive_Connectivity_Establishment) to automatically create an overlay peer-to-peer network connecting machines regardless of location (home, office, data center, container, cloud, or edge environments), unifying virtual private network management experience.
|
**Secure.** NetBird enables secure remote access by applying granular access policies, while allowing you to manage them intuitively from a single place. Works universally on any infrastructure.
|
||||||
|
|
||||||
**Key features:**
|
|
||||||
- \[x] Automatic IP allocation and network management with a Web UI ([separate repo](https://github.com/netbirdio/dashboard))
|
|
||||||
- \[x] Automatic WireGuard peer (machine) discovery and configuration.
|
|
||||||
- \[x] Encrypted peer-to-peer connections without a central VPN gateway.
|
|
||||||
- \[x] Connection relay fallback in case a peer-to-peer connection is not possible.
|
|
||||||
- \[x] Desktop client applications for Linux, MacOS, and Windows (systray).
|
|
||||||
- \[x] Multiuser support - sharing network between multiple users.
|
|
||||||
- \[x] SSO and MFA support.
|
|
||||||
- \[x] Multicloud and hybrid-cloud support.
|
|
||||||
- \[x] Kernel WireGuard usage when possible.
|
|
||||||
- \[x] Access Controls - groups & rules.
|
|
||||||
- \[x] Remote SSH access without managing SSH keys.
|
|
||||||
- \[x] Network Routes.
|
|
||||||
- \[x] Private DNS.
|
|
||||||
- \[x] Network Activity Monitoring.
|
|
||||||
|
|
||||||
**Coming soon:**
|
|
||||||
- \[ ] Mobile clients.
|
|
||||||
|
|
||||||
### Secure peer-to-peer VPN with SSO and MFA in minutes
|
### Secure peer-to-peer VPN with SSO and MFA in minutes
|
||||||
|
|
||||||
https://user-images.githubusercontent.com/700848/197345890-2e2cded5-7b7a-436f-a444-94e80dd24f46.mov
|
https://user-images.githubusercontent.com/700848/197345890-2e2cded5-7b7a-436f-a444-94e80dd24f46.mov
|
||||||
|
|
||||||
**Note**: The `main` branch may be in an *unstable or even broken state* during development.
|
### Key features
|
||||||
For stable versions, see [releases](https://github.com/netbirdio/netbird/releases).
|
|
||||||
|
|
||||||
### Start using NetBird
|
| Connectivity | Management | Automation | Platforms |
|
||||||
- Hosted version: [https://app.netbird.io/](https://app.netbird.io/).
|
|-------------------------------------------------------------------|--------------------------------------------------------------------------|----------------------------------------------------------------------------|---------------------------------------|
|
||||||
- See our documentation for [Quickstart Guide](https://docs.netbird.io/how-to/getting-started).
|
| <ul><li> - \[x] Kernel WireGuard </ul></li> | <ul><li> - \[x] [Admin Web UI](https://github.com/netbirdio/dashboard) </ul></li> | <ul><li> - \[x] [Public API](https://docs.netbird.io/api) </ul></li> | <ul><li> - \[x] Linux </ul></li> |
|
||||||
- If you are looking to self-host NetBird, check our [Self-Hosting Guide](https://docs.netbird.io/selfhosted/selfhosted-guide).
|
| <ul><li> - \[x] Peer-to-peer connections </ul></li> | <ul><li> - \[x] Auto peer discovery and configuration </ul></li> | <ul><li> - \[x] [Setup keys for bulk network provisioning](https://docs.netbird.io/how-to/register-machines-using-setup-keys) </ul></li> | <ul><li> - \[x] Mac </ul></li> |
|
||||||
- Step-by-step [Installation Guide](https://docs.netbird.io/how-to/getting-started#installation) for different platforms.
|
| <ul><li> - \[x] Peer-to-peer encryption </ul></li> | <ul><li> - \[x] [IdP integrations](https://docs.netbird.io/selfhosted/identity-providers) </ul></li> | <ul><li> - \[x] [Self-hosting quickstart script](https://docs.netbird.io/selfhosted/selfhosted-quickstart) </ul></li> | <ul><li> - \[x] Windows </ul></li> |
|
||||||
- Web UI [repository](https://github.com/netbirdio/dashboard).
|
| <ul><li> - \[x] Connection relay fallback </ul></li> | <ul><li> - \[x] [SSO & MFA support](https://docs.netbird.io/how-to/installation#running-net-bird-with-sso-login) </ul></li> | <ul><li> - \[x] IdP groups sync with JWT </ul></li> | <ul><li> - \[x] Android </ul></li> |
|
||||||
- 5 min [demo video](https://youtu.be/Tu9tPsUWaY0) on YouTube.
|
| <ul><li> - \[x] [Routes to external networks](https://docs.netbird.io/how-to/routing-traffic-to-private-networks) </ul></li> | <ul><li> - \[x] [Access control - groups & rules](https://docs.netbird.io/how-to/manage-network-access) </ul></li> | | <ul><li> - \[ ] iOS </ul></li> |
|
||||||
|
| <ul><li> - \[x] NAT traversal with BPF </ul></li> | <ul><li> - \[x] [Private DNS](https://docs.netbird.io/how-to/manage-dns-in-your-network) </ul></li> | | <ul><li> - \[x] Docker </ul></li> |
|
||||||
|
| | <ul><li> - \[x] [Multiuser support](https://docs.netbird.io/how-to/add-users-to-your-network) </ul></li> | | <ul><li> - \[x] OpenWRT </ul></li> |
|
||||||
|
| | <ul><li> - \[x] [Activity logging](https://docs.netbird.io/how-to/monitor-system-and-network-activity) </ul></li> | | |
|
||||||
|
| | <ul><li> - \[x] SSH access management </ul></li> | | |
|
||||||
|
|
||||||
|
|
||||||
|
### Quickstart with NetBird Cloud
|
||||||
|
|
||||||
|
- Download and install NetBird at [https://app.netbird.io/install](https://app.netbird.io/install)
|
||||||
|
- Follow the steps to sign-up with Google, Microsoft, GitHub or your email address.
|
||||||
|
- Check NetBird [admin UI](https://app.netbird.io/).
|
||||||
|
- Add more machines.
|
||||||
|
|
||||||
|
### Quickstart with self-hosted NetBird
|
||||||
|
|
||||||
|
> This is the quickest way to try self-hosted NetBird. It should take around 5 minutes to get started if you already have a public domain and a VM.
|
||||||
|
Follow the [Advanced guide with a custom identity provider](https://docs.netbird.io/selfhosted/selfhosted-guide#advanced-guide-with-a-custom-identity-provider) for installations with different IDPs.
|
||||||
|
|
||||||
|
**Infrastructure requirements:**
|
||||||
|
- A Linux VM with at least **1CPU** and **2GB** of memory.
|
||||||
|
- The VM should be publicly accessible on TCP ports **80** and **443** and UDP ports: **3478**, **49152-65535**.
|
||||||
|
- **Public domain** name pointing to the VM.
|
||||||
|
|
||||||
|
**Software requirements:**
|
||||||
|
- Docker installed on the VM with the docker compose plugin ([Docker installation guide](https://docs.docker.com/engine/install/)) or docker with docker-compose in version 2 or higher.
|
||||||
|
- [jq](https://jqlang.github.io/jq/) installed. In most distributions
|
||||||
|
Usually available in the official repositories and can be installed with `sudo apt install jq` or `sudo yum install jq`
|
||||||
|
- [curl](https://curl.se/) installed.
|
||||||
|
Usually available in the official repositories and can be installed with `sudo apt install curl` or `sudo yum install curl`
|
||||||
|
|
||||||
|
**Steps**
|
||||||
|
- Download and run the installation script:
|
||||||
|
```bash
|
||||||
|
export NETBIRD_DOMAIN=netbird.example.com; curl -fsSL https://github.com/netbirdio/netbird/releases/latest/download/getting-started-with-zitadel.sh | bash
|
||||||
|
```
|
||||||
|
- Once finished, you can manage the resources via `docker-compose`
|
||||||
|
|
||||||
### A bit on NetBird internals
|
### A bit on NetBird internals
|
||||||
- Every machine in the network runs [NetBird Agent (or Client)](client/) that manages WireGuard.
|
- Every machine in the network runs [NetBird Agent (or Client)](client/) that manages WireGuard.
|
||||||
- Every agent connects to [Management Service](management/) that holds network state, manages peer IPs, and distributes network updates to agents (peers).
|
- Every agent connects to [Management Service](management/) that holds network state, manages peer IPs, and distributes network updates to agents (peers).
|
||||||
@@ -88,18 +103,18 @@ For stable versions, see [releases](https://github.com/netbirdio/netbird/release
|
|||||||
[Coturn](https://github.com/coturn/coturn) is the one that has been successfully used for STUN and TURN in NetBird setups.
|
[Coturn](https://github.com/coturn/coturn) is the one that has been successfully used for STUN and TURN in NetBird setups.
|
||||||
|
|
||||||
<p float="left" align="middle">
|
<p float="left" align="middle">
|
||||||
<img src="https://netbird.io/docs/img/architecture/high-level-dia.png" width="700"/>
|
<img src="https://docs.netbird.io/docs-static/img/architecture/high-level-dia.png" width="700"/>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
See a complete [architecture overview](https://docs.netbird.io/about-netbird/how-netbird-works#architecture) for details.
|
See a complete [architecture overview](https://docs.netbird.io/about-netbird/how-netbird-works#architecture) for details.
|
||||||
|
|
||||||
### Roadmap
|
|
||||||
- [Public Roadmap](https://github.com/netbirdio/netbird/projects/2)
|
|
||||||
|
|
||||||
### Community projects
|
### Community projects
|
||||||
- [NetBird on OpenWRT](https://github.com/messense/openwrt-netbird)
|
- [NetBird on OpenWRT](https://github.com/messense/openwrt-netbird)
|
||||||
- [NetBird installer script](https://github.com/physk/netbird-installer)
|
- [NetBird installer script](https://github.com/physk/netbird-installer)
|
||||||
|
|
||||||
|
**Note**: The `main` branch may be in an *unstable or even broken state* during development.
|
||||||
|
For stable versions, see [releases](https://github.com/netbirdio/netbird/releases).
|
||||||
|
|
||||||
### Support acknowledgement
|
### Support acknowledgement
|
||||||
|
|
||||||
In November 2022, NetBird joined the [StartUpSecure program](https://www.forschung-it-sicherheit-kommunikationssysteme.de/foerderung/bekanntmachungen/startup-secure) sponsored by The Federal Ministry of Education and Research of The Federal Republic of Germany. Together with [CISPA Helmholtz Center for Information Security](https://cispa.de/en) NetBird brings the security best practices and simplicity to private networking.
|
In November 2022, NetBird joined the [StartUpSecure program](https://www.forschung-it-sicherheit-kommunikationssysteme.de/foerderung/bekanntmachungen/startup-secure) sponsored by The Federal Ministry of Education and Research of The Federal Republic of Germany. Together with [CISPA Helmholtz Center for Information Security](https://cispa.de/en) NetBird brings the security best practices and simplicity to private networking.
|
||||||
@@ -107,7 +122,7 @@ In November 2022, NetBird joined the [StartUpSecure program](https://www.forschu
|
|||||||

|

|
||||||
|
|
||||||
### Testimonials
|
### Testimonials
|
||||||
We use open-source technologies like [WireGuard®](https://www.wireguard.com/), [Pion ICE (WebRTC)](https://github.com/pion/ice), and [Coturn](https://github.com/coturn/coturn). We very much appreciate the work these guys are doing and we'd greatly appreciate if you could support them in any way (e.g. giving a star or a contribution).
|
We use open-source technologies like [WireGuard®](https://www.wireguard.com/), [Pion ICE (WebRTC)](https://github.com/pion/ice), [Coturn](https://github.com/coturn/coturn), and [Rosenpass](https://rosenpass.eu). We very much appreciate the work these guys are doing and we'd greatly appreciate if you could support them in any way (e.g. giving a star or a contribution).
|
||||||
|
|
||||||
### Legal
|
### Legal
|
||||||
_WireGuard_ and the _WireGuard_ logo are [registered trademarks](https://www.wireguard.com/trademark-policy/) of Jason A. Donenfeld.
|
_WireGuard_ and the _WireGuard_ logo are [registered trademarks](https://www.wireguard.com/trademark-policy/) of Jason A. Donenfeld.
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
FROM gcr.io/distroless/base:debug
|
FROM alpine:3
|
||||||
|
RUN apk add --no-cache ca-certificates iptables ip6tables
|
||||||
ENV NB_FOREGROUND_MODE=true
|
ENV NB_FOREGROUND_MODE=true
|
||||||
ENV PATH=/sbin:/usr/sbin:/bin:/usr/bin:/busybox
|
|
||||||
SHELL ["/busybox/sh","-c"]
|
|
||||||
RUN sed -i -E 's/(^root:.+)\/sbin\/nologin/\1\/busybox\/sh/g' /etc/passwd
|
|
||||||
ENTRYPOINT [ "/go/bin/netbird","up"]
|
ENTRYPOINT [ "/go/bin/netbird","up"]
|
||||||
COPY netbird /go/bin/netbird
|
COPY netbird /go/bin/netbird
|
||||||
@@ -55,7 +55,6 @@ type Client struct {
|
|||||||
ctxCancelLock *sync.Mutex
|
ctxCancelLock *sync.Mutex
|
||||||
deviceName string
|
deviceName string
|
||||||
routeListener routemanager.RouteListener
|
routeListener routemanager.RouteListener
|
||||||
onHostDnsFn func([]string)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClient instantiate a new Client
|
// NewClient instantiate a new Client
|
||||||
@@ -97,7 +96,30 @@ func (c *Client) Run(urlOpener URLOpener, dns *DNSList, dnsReadyListener DnsRead
|
|||||||
|
|
||||||
// todo do not throw error in case of cancelled context
|
// todo do not throw error in case of cancelled context
|
||||||
ctx = internal.CtxInitState(ctx)
|
ctx = internal.CtxInitState(ctx)
|
||||||
c.onHostDnsFn = func([]string) {}
|
return internal.RunClientMobile(ctx, cfg, c.recorder, c.tunAdapter, c.iFaceDiscover, c.routeListener, dns.items, dnsReadyListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunWithoutLogin we apply this type of run function when the backed has been started without UI (i.e. after reboot).
|
||||||
|
// In this case make no sense handle registration steps.
|
||||||
|
func (c *Client) RunWithoutLogin(dns *DNSList, dnsReadyListener DnsReadyListener) error {
|
||||||
|
cfg, err := internal.UpdateOrCreateConfig(internal.ConfigInput{
|
||||||
|
ConfigPath: c.cfgFile,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.recorder.UpdateManagementAddress(cfg.ManagementURL.String())
|
||||||
|
|
||||||
|
var ctx context.Context
|
||||||
|
//nolint
|
||||||
|
ctxWithValues := context.WithValue(context.Background(), system.DeviceNameCtxKey, c.deviceName)
|
||||||
|
c.ctxCancelLock.Lock()
|
||||||
|
ctx, c.ctxCancel = context.WithCancel(ctxWithValues)
|
||||||
|
defer c.ctxCancel()
|
||||||
|
c.ctxCancelLock.Unlock()
|
||||||
|
|
||||||
|
// todo do not throw error in case of cancelled context
|
||||||
|
ctx = internal.CtxInitState(ctx)
|
||||||
return internal.RunClientMobile(ctx, cfg, c.recorder, c.tunAdapter, c.iFaceDiscover, c.routeListener, dns.items, dnsReadyListener)
|
return internal.RunClientMobile(ctx, cfg, c.recorder, c.tunAdapter, c.iFaceDiscover, c.routeListener, dns.items, dnsReadyListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,21 +3,21 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/netbirdio/netbird/client/internal/auth"
|
"os"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/skratchdot/open-golang/open"
|
"github.com/skratchdot/open-golang/open"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
gstatus "google.golang.org/grpc/status"
|
gstatus "google.golang.org/grpc/status"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/util"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/internal"
|
"github.com/netbirdio/netbird/client/internal"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/auth"
|
||||||
"github.com/netbirdio/netbird/client/proto"
|
"github.com/netbirdio/netbird/client/proto"
|
||||||
"github.com/netbirdio/netbird/client/system"
|
"github.com/netbirdio/netbird/client/system"
|
||||||
|
"github.com/netbirdio/netbird/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
var loginCmd = &cobra.Command{
|
var loginCmd = &cobra.Command{
|
||||||
@@ -191,17 +191,63 @@ func foregroundGetTokenInfo(ctx context.Context, cmd *cobra.Command, config *int
|
|||||||
|
|
||||||
func openURL(cmd *cobra.Command, verificationURIComplete, userCode string) {
|
func openURL(cmd *cobra.Command, verificationURIComplete, userCode string) {
|
||||||
var codeMsg string
|
var codeMsg string
|
||||||
if userCode != "" {
|
if userCode != "" && !strings.Contains(verificationURIComplete, userCode) {
|
||||||
if !strings.Contains(verificationURIComplete, userCode) {
|
|
||||||
codeMsg = fmt.Sprintf("and enter the code %s to authenticate.", userCode)
|
codeMsg = fmt.Sprintf("and enter the code %s to authenticate.", userCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
browserAuthMsg := "Please do the SSO login in your browser. \n" +
|
||||||
|
"If your browser didn't open automatically, use this URL to log in:\n\n" +
|
||||||
|
verificationURIComplete + " " + codeMsg
|
||||||
|
|
||||||
|
setupKeyAuthMsg := "\nAlternatively, you may want to use a setup key, see:\n\n" +
|
||||||
|
"https://docs.netbird.io/how-to/register-machines-using-setup-keys"
|
||||||
|
|
||||||
|
authenticateUsingBrowser := func() {
|
||||||
|
cmd.Println(browserAuthMsg)
|
||||||
|
if err := open.Run(verificationURIComplete); err != nil {
|
||||||
|
cmd.Println(setupKeyAuthMsg)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err := open.Run(verificationURIComplete)
|
switch runtime.GOOS {
|
||||||
cmd.Printf("Please do the SSO login in your browser. \n" +
|
case "windows", "darwin":
|
||||||
"If your browser didn't open automatically, use this URL to log in:\n\n" +
|
authenticateUsingBrowser()
|
||||||
" " + verificationURIComplete + " " + codeMsg + " \n\n")
|
case "linux":
|
||||||
if err != nil {
|
if isLinuxRunningDesktop() {
|
||||||
cmd.Printf("Alternatively, you may want to use a setup key, see:\n\n https://www.netbird.io/docs/overview/setup-keys\n")
|
authenticateUsingBrowser()
|
||||||
|
} else {
|
||||||
|
// If current flow is PKCE, it implies the server is anticipating the redirect to localhost.
|
||||||
|
// Devices lacking browser support are incompatible with this flow.Therefore,
|
||||||
|
// these devices will need to resort to setup keys instead.
|
||||||
|
if isPKCEFlow(verificationURIComplete) {
|
||||||
|
cmd.Println("Please proceed with setting up this device using setup keys, see:\n\n" +
|
||||||
|
"https://docs.netbird.io/how-to/register-machines-using-setup-keys")
|
||||||
|
} else {
|
||||||
|
cmd.Println(browserAuthMsg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// isLinuxRunningDesktop checks if a Linux OS is running desktop environment.
|
||||||
|
func isLinuxRunningDesktop() bool {
|
||||||
|
for _, env := range os.Environ() {
|
||||||
|
values := strings.Split(env, "=")
|
||||||
|
if len(values) == 2 {
|
||||||
|
key, value := values[0], values[1]
|
||||||
|
if key == "XDG_CURRENT_DESKTOP" && value != "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// isPKCEFlow determines if the PKCE flow is active or not,
|
||||||
|
// by checking the existence of redirect_uri inside the verification URL.
|
||||||
|
func isPKCEFlow(verificationURL string) bool {
|
||||||
|
if verificationURL == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return strings.Contains(verificationURL, "redirect_uri")
|
||||||
|
}
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ func statusFunc(cmd *cobra.Command, args []string) error {
|
|||||||
" netbird up \n\n"+
|
" netbird up \n\n"+
|
||||||
"If you are running a self-hosted version and no SSO provider has been configured in your Management Server,\n"+
|
"If you are running a self-hosted version and no SSO provider has been configured in your Management Server,\n"+
|
||||||
"you can use a setup-key:\n\n netbird up --management-url <YOUR_MANAGEMENT_URL> --setup-key <YOUR_SETUP_KEY>\n\n"+
|
"you can use a setup-key:\n\n netbird up --management-url <YOUR_MANAGEMENT_URL> --setup-key <YOUR_SETUP_KEY>\n\n"+
|
||||||
"More info: https://www.netbird.io/docs/overview/setup-keys\n\n",
|
"More info: https://docs.netbird.io/how-to/register-machines-using-setup-keys\n\n",
|
||||||
resp.GetStatus(),
|
resp.GetStatus(),
|
||||||
)
|
)
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -59,19 +59,17 @@ func (t TokenInfo) GetTokenToUse() string {
|
|||||||
|
|
||||||
// NewOAuthFlow initializes and returns the appropriate OAuth flow based on the management configuration.
|
// NewOAuthFlow initializes and returns the appropriate OAuth flow based on the management configuration.
|
||||||
func NewOAuthFlow(ctx context.Context, config *internal.Config) (OAuthFlow, error) {
|
func NewOAuthFlow(ctx context.Context, config *internal.Config) (OAuthFlow, error) {
|
||||||
log.Debug("getting device authorization flow info")
|
log.Debug("loading pkce authorization flow info")
|
||||||
|
|
||||||
// Try to initialize the Device Authorization Flow
|
pkceFlowInfo, err := internal.GetPKCEAuthorizationFlowInfo(ctx, config.PrivateKey, config.ManagementURL)
|
||||||
deviceFlowInfo, err := internal.GetDeviceAuthorizationFlowInfo(ctx, config.PrivateKey, config.ManagementURL)
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return NewDeviceAuthorizationFlow(deviceFlowInfo.ProviderConfig)
|
return NewPKCEAuthorizationFlow(pkceFlowInfo.ProviderConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("getting device authorization flow info failed with error: %v", err)
|
log.Debugf("loading pkce authorization flow info failed with error: %v", err)
|
||||||
log.Debugf("falling back to pkce authorization flow info")
|
log.Debugf("falling back to device authorization flow info")
|
||||||
|
|
||||||
// If Device Authorization Flow failed, try the PKCE Authorization Flow
|
deviceFlowInfo, err := internal.GetDeviceAuthorizationFlowInfo(ctx, config.PrivateKey, config.ManagementURL)
|
||||||
pkceFlowInfo, err := internal.GetPKCEAuthorizationFlowInfo(ctx, config.PrivateKey, config.ManagementURL)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s, ok := gstatus.FromError(err)
|
s, ok := gstatus.FromError(err)
|
||||||
if ok && s.Code() == codes.NotFound {
|
if ok && s.Code() == codes.NotFound {
|
||||||
@@ -82,9 +80,9 @@ func NewOAuthFlow(ctx context.Context, config *internal.Config) (OAuthFlow, erro
|
|||||||
return nil, fmt.Errorf("the management server, %s, does not support SSO providers, "+
|
return nil, fmt.Errorf("the management server, %s, does not support SSO providers, "+
|
||||||
"please update your server or use Setup Keys to login", config.ManagementURL)
|
"please update your server or use Setup Keys to login", config.ManagementURL)
|
||||||
} else {
|
} else {
|
||||||
return nil, fmt.Errorf("getting pkce authorization flow info failed with error: %v", err)
|
return nil, fmt.Errorf("getting device authorization flow info failed with error: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return NewPKCEAuthorizationFlow(pkceFlowInfo.ProviderConfig)
|
return NewDeviceAuthorizationFlow(deviceFlowInfo.ProviderConfig)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ var _ OAuthFlow = &PKCEAuthorizationFlow{}
|
|||||||
const (
|
const (
|
||||||
queryState = "state"
|
queryState = "state"
|
||||||
queryCode = "code"
|
queryCode = "code"
|
||||||
|
queryError = "error"
|
||||||
|
queryErrorDesc = "error_description"
|
||||||
defaultPKCETimeoutSeconds = 300
|
defaultPKCETimeoutSeconds = 300
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -141,9 +143,13 @@ func (p *PKCEAuthorizationFlow) startServer(tokenChan chan<- *oauth2.Token, errC
|
|||||||
tokenValidatorFunc := func() (*oauth2.Token, error) {
|
tokenValidatorFunc := func() (*oauth2.Token, error) {
|
||||||
query := req.URL.Query()
|
query := req.URL.Query()
|
||||||
|
|
||||||
state := query.Get(queryState)
|
if authError := query.Get(queryError); authError != "" {
|
||||||
|
authErrorDesc := query.Get(queryErrorDesc)
|
||||||
|
return nil, fmt.Errorf("%s.%s", authError, authErrorDesc)
|
||||||
|
}
|
||||||
|
|
||||||
// Prevent timing attacks on state
|
// Prevent timing attacks on state
|
||||||
if subtle.ConstantTimeCompare([]byte(p.state), []byte(state)) == 0 {
|
if state := query.Get(queryState); subtle.ConstantTimeCompare([]byte(p.state), []byte(state)) == 0 {
|
||||||
return nil, fmt.Errorf("invalid state")
|
return nil, fmt.Errorf("invalid state")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,12 +167,13 @@ func (p *PKCEAuthorizationFlow) startServer(tokenChan chan<- *oauth2.Token, errC
|
|||||||
|
|
||||||
token, err := tokenValidatorFunc()
|
token, err := tokenValidatorFunc()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errChan <- fmt.Errorf("PKCE authorization flow failed: %v", err)
|
|
||||||
renderPKCEFlowTmpl(w, err)
|
renderPKCEFlowTmpl(w, err)
|
||||||
|
errChan <- fmt.Errorf("PKCE authorization flow failed: %v", err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tokenChan <- token
|
|
||||||
renderPKCEFlowTmpl(w, nil)
|
renderPKCEFlowTmpl(w, nil)
|
||||||
|
tokenChan <- token
|
||||||
})
|
})
|
||||||
|
|
||||||
if err := server.ListenAndServe(); err != nil {
|
if err := server.ListenAndServe(); err != nil {
|
||||||
|
|||||||
@@ -179,8 +179,6 @@ func runClient(ctx context.Context, config *Config, statusRecorder *peer.Status,
|
|||||||
log.Print("Netbird engine started, my IP is: ", peerConfig.Address)
|
log.Print("Netbird engine started, my IP is: ", peerConfig.Address)
|
||||||
state.Set(StatusConnected)
|
state.Set(StatusConnected)
|
||||||
|
|
||||||
statusRecorder.ClientStart()
|
|
||||||
|
|
||||||
<-engineCtx.Done()
|
<-engineCtx.Done()
|
||||||
statusRecorder.ClientTeardown()
|
statusRecorder.ClientTeardown()
|
||||||
|
|
||||||
@@ -201,6 +199,7 @@ func runClient(ctx context.Context, config *Config, statusRecorder *peer.Status,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
statusRecorder.ClientStart()
|
||||||
err = backoff.Retry(operation, backOff)
|
err = backoff.Retry(operation, backOff)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debugf("exiting client retry loop due to unrecoverable error: %s", err)
|
log.Debugf("exiting client retry loop due to unrecoverable error: %s", err)
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ const (
|
|||||||
fileGeneratedResolvConfSearchBeginContent = "search "
|
fileGeneratedResolvConfSearchBeginContent = "search "
|
||||||
fileGeneratedResolvConfContentFormat = fileGeneratedResolvConfContentHeader +
|
fileGeneratedResolvConfContentFormat = fileGeneratedResolvConfContentHeader +
|
||||||
"\n# If needed you can restore the original file by copying back %s\n\nnameserver %s\n" +
|
"\n# If needed you can restore the original file by copying back %s\n\nnameserver %s\n" +
|
||||||
fileGeneratedResolvConfSearchBeginContent + "%s\n"
|
fileGeneratedResolvConfSearchBeginContent + "%s\n\n" +
|
||||||
|
"%s\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -91,7 +92,12 @@ func (f *fileConfigurator) applyDNSConfig(config hostDNSConfig) error {
|
|||||||
searchDomains += " " + dConf.domain
|
searchDomains += " " + dConf.domain
|
||||||
appendedDomains++
|
appendedDomains++
|
||||||
}
|
}
|
||||||
content := fmt.Sprintf(fileGeneratedResolvConfContentFormat, fileDefaultResolvConfBackupLocation, config.serverIP, searchDomains)
|
|
||||||
|
originalContent, err := os.ReadFile(fileDefaultResolvConfBackupLocation)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Could not read existing resolv.conf")
|
||||||
|
}
|
||||||
|
content := fmt.Sprintf(fileGeneratedResolvConfContentFormat, fileDefaultResolvConfBackupLocation, config.serverIP, searchDomains, string(originalContent))
|
||||||
err = writeDNSConfig(content, defaultResolvConfPath, f.originalPerms)
|
err = writeDNSConfig(content, defaultResolvConfPath, f.originalPerms)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = f.restore()
|
err = f.restore()
|
||||||
|
|||||||
@@ -182,12 +182,11 @@ func (s *systemConfigurator) addDNSState(state, domains, dnsServer string, port
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *systemConfigurator) addDNSSetupForAll(dnsServer string, port int) error {
|
func (s *systemConfigurator) addDNSSetupForAll(dnsServer string, port int) error {
|
||||||
primaryServiceKey := s.getPrimaryService()
|
primaryServiceKey, existingNameserver := s.getPrimaryService()
|
||||||
if primaryServiceKey == "" {
|
if primaryServiceKey == "" {
|
||||||
return fmt.Errorf("couldn't find the primary service key")
|
return fmt.Errorf("couldn't find the primary service key")
|
||||||
}
|
}
|
||||||
|
err := s.addDNSSetup(getKeyWithInput(primaryServiceSetupKeyFormat, primaryServiceKey), dnsServer, port, existingNameserver)
|
||||||
err := s.addDNSSetup(getKeyWithInput(primaryServiceSetupKeyFormat, primaryServiceKey), dnsServer, port)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -196,27 +195,32 @@ func (s *systemConfigurator) addDNSSetupForAll(dnsServer string, port int) error
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *systemConfigurator) getPrimaryService() string {
|
func (s *systemConfigurator) getPrimaryService() (string, string) {
|
||||||
line := buildCommandLine("show", globalIPv4State, "")
|
line := buildCommandLine("show", globalIPv4State, "")
|
||||||
stdinCommands := wrapCommand(line)
|
stdinCommands := wrapCommand(line)
|
||||||
b, err := runSystemConfigCommand(stdinCommands)
|
b, err := runSystemConfigCommand(stdinCommands)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("got error while sending the command: ", err)
|
log.Error("got error while sending the command: ", err)
|
||||||
return ""
|
return "", ""
|
||||||
}
|
}
|
||||||
scanner := bufio.NewScanner(bytes.NewReader(b))
|
scanner := bufio.NewScanner(bytes.NewReader(b))
|
||||||
|
primaryService := ""
|
||||||
|
router := ""
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
text := scanner.Text()
|
text := scanner.Text()
|
||||||
if strings.Contains(text, "PrimaryService") {
|
if strings.Contains(text, "PrimaryService") {
|
||||||
return strings.TrimSpace(strings.Split(text, ":")[1])
|
primaryService = strings.TrimSpace(strings.Split(text, ":")[1])
|
||||||
|
}
|
||||||
|
if strings.Contains(text, "Router") {
|
||||||
|
router = strings.TrimSpace(strings.Split(text, ":")[1])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ""
|
return primaryService, router
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *systemConfigurator) addDNSSetup(setupKey, dnsServer string, port int) error {
|
func (s *systemConfigurator) addDNSSetup(setupKey, dnsServer string, port int, existingDNSServer string) error {
|
||||||
lines := buildAddCommandLine(keySupplementalMatchDomainsNoSearch, digitSymbol+strconv.Itoa(0))
|
lines := buildAddCommandLine(keySupplementalMatchDomainsNoSearch, digitSymbol+strconv.Itoa(0))
|
||||||
lines += buildAddCommandLine(keyServerAddresses, arraySymbol+dnsServer)
|
lines += buildAddCommandLine(keyServerAddresses, arraySymbol+dnsServer+" "+existingDNSServer)
|
||||||
lines += buildAddCommandLine(keyServerPort, digitSymbol+strconv.Itoa(port))
|
lines += buildAddCommandLine(keyServerPort, digitSymbol+strconv.Itoa(port))
|
||||||
addDomainCommand := buildCreateStateWithOperation(setupKey, lines)
|
addDomainCommand := buildCreateStateWithOperation(setupKey, lines)
|
||||||
stdinCommands := wrapCommand(addDomainCommand)
|
stdinCommands := wrapCommand(addDomainCommand)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ package dns
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -59,7 +60,11 @@ func (r *resolvconf) applyDNSConfig(config hostDNSConfig) error {
|
|||||||
appendedDomains++
|
appendedDomains++
|
||||||
}
|
}
|
||||||
|
|
||||||
content := fmt.Sprintf(fileGeneratedResolvConfContentFormat, fileDefaultResolvConfBackupLocation, config.serverIP, searchDomains)
|
originalContent, err := os.ReadFile(fileDefaultResolvConfBackupLocation)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Could not read existing resolv.conf")
|
||||||
|
}
|
||||||
|
content := fmt.Sprintf(fileGeneratedResolvConfContentFormat, fileDefaultResolvConfBackupLocation, config.serverIP, searchDomains, string(originalContent))
|
||||||
|
|
||||||
err = r.applyConfig(content)
|
err = r.applyConfig(content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -238,7 +238,7 @@ func (s *DefaultServer) applyConfiguration(update nbdns.Config) error {
|
|||||||
hostUpdate := s.currentConfig
|
hostUpdate := s.currentConfig
|
||||||
if s.service.RuntimePort() != defaultPort && !s.hostManager.supportCustomPort() {
|
if s.service.RuntimePort() != defaultPort && !s.hostManager.supportCustomPort() {
|
||||||
log.Warnf("the DNS manager of this peer doesn't support custom port. Disabling primary DNS setup. " +
|
log.Warnf("the DNS manager of this peer doesn't support custom port. Disabling primary DNS setup. " +
|
||||||
"Learn more at: https://netbird.io/docs/how-to-guides/nameservers#local-resolver")
|
"Learn more at: https://docs.netbird.io/how-to/manage-dns-in-your-network#local-resolver")
|
||||||
hostUpdate.routeAll = false
|
hostUpdate.routeAll = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ type notifier struct {
|
|||||||
listener Listener
|
listener Listener
|
||||||
currentClientState bool
|
currentClientState bool
|
||||||
lastNotification int
|
lastNotification int
|
||||||
|
lastNumberOfPeers int
|
||||||
}
|
}
|
||||||
|
|
||||||
func newNotifier() *notifier {
|
func newNotifier() *notifier {
|
||||||
@@ -29,6 +30,7 @@ func (n *notifier) setListener(listener Listener) {
|
|||||||
|
|
||||||
n.serverStateLock.Lock()
|
n.serverStateLock.Lock()
|
||||||
n.notifyListener(listener, n.lastNotification)
|
n.notifyListener(listener, n.lastNotification)
|
||||||
|
listener.OnPeersListChanged(n.lastNumberOfPeers)
|
||||||
n.serverStateLock.Unlock()
|
n.serverStateLock.Unlock()
|
||||||
|
|
||||||
n.listener = listener
|
n.listener = listener
|
||||||
@@ -59,7 +61,7 @@ func (n *notifier) clientStart() {
|
|||||||
n.serverStateLock.Lock()
|
n.serverStateLock.Lock()
|
||||||
defer n.serverStateLock.Unlock()
|
defer n.serverStateLock.Unlock()
|
||||||
n.currentClientState = true
|
n.currentClientState = true
|
||||||
n.lastNotification = stateConnected
|
n.lastNotification = stateConnecting
|
||||||
n.notify(n.lastNotification)
|
n.notify(n.lastNotification)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,7 +114,7 @@ func (n *notifier) calculateState(managementConn, signalConn bool) int {
|
|||||||
return stateConnected
|
return stateConnected
|
||||||
}
|
}
|
||||||
|
|
||||||
if !managementConn && !signalConn {
|
if !managementConn && !signalConn && !n.currentClientState {
|
||||||
return stateDisconnected
|
return stateDisconnected
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,6 +126,7 @@ func (n *notifier) calculateState(managementConn, signalConn bool) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (n *notifier) peerListChanged(numOfPeers int) {
|
func (n *notifier) peerListChanged(numOfPeers int) {
|
||||||
|
n.lastNumberOfPeers = numOfPeers
|
||||||
n.listenersLock.Lock()
|
n.listenersLock.Lock()
|
||||||
defer n.listenersLock.Unlock()
|
defer n.listenersLock.Unlock()
|
||||||
if n.listener == nil {
|
if n.listener == nil {
|
||||||
|
|||||||
@@ -353,9 +353,13 @@ func (d *Status) onConnectionChanged() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Status) notifyPeerListChanged() {
|
func (d *Status) notifyPeerListChanged() {
|
||||||
d.notifier.peerListChanged(len(d.peers) + len(d.offlinePeers))
|
d.notifier.peerListChanged(d.numOfPeers())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Status) notifyAddressChanged() {
|
func (d *Status) notifyAddressChanged() {
|
||||||
d.notifier.localAddressChanged(d.localPeer.FQDN, d.localPeer.IP)
|
d.notifier.localAddressChanged(d.localPeer.FQDN, d.localPeer.IP)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Status) numOfPeers() int {
|
||||||
|
return len(d.peers) + len(d.offlinePeers)
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,14 +27,19 @@ func genKey(format string, input string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewFirewall if supported, returns an iptables manager, otherwise returns a nftables manager
|
// NewFirewall if supported, returns an iptables manager, otherwise returns a nftables manager
|
||||||
func NewFirewall(parentCTX context.Context) firewallManager {
|
func NewFirewall(parentCTX context.Context) (firewallManager, error) {
|
||||||
manager, err := newNFTablesManager(parentCTX)
|
manager, err := newNFTablesManager(parentCTX)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
log.Debugf("nftables firewall manager will be used")
|
log.Debugf("nftables firewall manager will be used")
|
||||||
return manager
|
return manager, nil
|
||||||
}
|
}
|
||||||
log.Debugf("fallback to iptables firewall manager: %s", err)
|
fMgr, err := newIptablesManager(parentCTX)
|
||||||
return newIptablesManager(parentCTX)
|
if err != nil {
|
||||||
|
log.Debugf("failed to initialize iptables for root mgr: %s", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
log.Debugf("iptables firewall manager will be used")
|
||||||
|
return fMgr, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getInPair(pair routerPair) routerPair {
|
func getInPair(pair routerPair) routerPair {
|
||||||
|
|||||||
@@ -3,24 +3,12 @@
|
|||||||
|
|
||||||
package routemanager
|
package routemanager
|
||||||
|
|
||||||
import "context"
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
type unimplementedFirewall struct{}
|
// NewFirewall returns a nil manager
|
||||||
|
func NewFirewall(context.Context) (firewallManager, error) {
|
||||||
func (unimplementedFirewall) RestoreOrCreateContainers() error {
|
return nil, fmt.Errorf("firewall not supported on this OS")
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (unimplementedFirewall) InsertRoutingRules(pair routerPair) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (unimplementedFirewall) RemoveRoutingRules(pair routerPair) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (unimplementedFirewall) CleanRoutingRules() {
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewFirewall returns an unimplemented Firewall manager
|
|
||||||
func NewFirewall(parentCtx context.Context) firewallManager {
|
|
||||||
return unimplementedFirewall{}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,26 +49,29 @@ type iptablesManager struct {
|
|||||||
mux sync.Mutex
|
mux sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func newIptablesManager(parentCtx context.Context) *iptablesManager {
|
func newIptablesManager(parentCtx context.Context) (*iptablesManager, error) {
|
||||||
ctx, cancel := context.WithCancel(parentCtx)
|
ipv4Client, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
||||||
ipv4Client, _ := iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
if err != nil {
|
||||||
if !isIptablesClientAvailable(ipv4Client) {
|
return nil, err
|
||||||
log.Infof("iptables is missing for ipv4")
|
} else if !isIptablesClientAvailable(ipv4Client) {
|
||||||
ipv4Client = nil
|
return nil, fmt.Errorf("iptables is missing for ipv4")
|
||||||
}
|
}
|
||||||
ipv6Client, _ := iptables.NewWithProtocol(iptables.ProtocolIPv6)
|
ipv6Client, err := iptables.NewWithProtocol(iptables.ProtocolIPv6)
|
||||||
if !isIptablesClientAvailable(ipv6Client) {
|
if err != nil {
|
||||||
|
log.Debugf("failed to initialize iptables for ipv6: %s", err)
|
||||||
|
} else if !isIptablesClientAvailable(ipv6Client) {
|
||||||
log.Infof("iptables is missing for ipv6")
|
log.Infof("iptables is missing for ipv6")
|
||||||
ipv6Client = nil
|
ipv6Client = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(parentCtx)
|
||||||
return &iptablesManager{
|
return &iptablesManager{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
stop: cancel,
|
stop: cancel,
|
||||||
ipv4Client: ipv4Client,
|
ipv4Client: ipv4Client,
|
||||||
ipv6Client: ipv6Client,
|
ipv6Client: ipv6Client,
|
||||||
rules: make(map[string]map[string][]string),
|
rules: make(map[string]map[string][]string),
|
||||||
}
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CleanRoutingRules cleans existing iptables resources that we created by the agent
|
// CleanRoutingRules cleans existing iptables resources that we created by the agent
|
||||||
@@ -391,6 +394,10 @@ func (i *iptablesManager) insertRoutingRule(keyFormat, table, chain, jump string
|
|||||||
ipVersion = ipv6
|
ipVersion = ipv6
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if iptablesClient == nil {
|
||||||
|
return fmt.Errorf("unable to insert iptables routing rules. Iptables client is not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
ruleKey := genKey(keyFormat, pair.ID)
|
ruleKey := genKey(keyFormat, pair.ID)
|
||||||
rule := genRuleSpec(jump, ruleKey, pair.source, pair.destination)
|
rule := genRuleSpec(jump, ruleKey, pair.source, pair.destination)
|
||||||
existingRule, found := i.rules[ipVersion][ruleKey]
|
existingRule, found := i.rules[ipVersion][ruleKey]
|
||||||
@@ -455,6 +462,10 @@ func (i *iptablesManager) removeRoutingRule(keyFormat, table, chain string, pair
|
|||||||
ipVersion = ipv6
|
ipVersion = ipv6
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if iptablesClient == nil {
|
||||||
|
return fmt.Errorf("unable to remove iptables routing rules. Iptables client is not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
ruleKey := genKey(keyFormat, pair.ID)
|
ruleKey := genKey(keyFormat, pair.ID)
|
||||||
existingRule, found := i.rules[ipVersion][ruleKey]
|
existingRule, found := i.rules[ipVersion][ruleKey]
|
||||||
if found {
|
if found {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ func TestIptablesManager_RestoreOrCreateContainers(t *testing.T) {
|
|||||||
t.SkipNow()
|
t.SkipNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
manager := newIptablesManager(context.TODO())
|
manager, _ := newIptablesManager(context.TODO())
|
||||||
|
|
||||||
defer manager.CleanRoutingRules()
|
defer manager.CleanRoutingRules()
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ type DefaultManager struct {
|
|||||||
stop context.CancelFunc
|
stop context.CancelFunc
|
||||||
mux sync.Mutex
|
mux sync.Mutex
|
||||||
clientNetworks map[string]*clientNetwork
|
clientNetworks map[string]*clientNetwork
|
||||||
serverRouter *serverRouter
|
serverRouter serverRouter
|
||||||
statusRecorder *peer.Status
|
statusRecorder *peer.Status
|
||||||
wgInterface *iface.WGIface
|
wgInterface *iface.WGIface
|
||||||
pubKey string
|
pubKey string
|
||||||
@@ -36,13 +36,17 @@ type DefaultManager struct {
|
|||||||
|
|
||||||
// NewManager returns a new route manager
|
// NewManager returns a new route manager
|
||||||
func NewManager(ctx context.Context, pubKey string, wgInterface *iface.WGIface, statusRecorder *peer.Status, initialRoutes []*route.Route) *DefaultManager {
|
func NewManager(ctx context.Context, pubKey string, wgInterface *iface.WGIface, statusRecorder *peer.Status, initialRoutes []*route.Route) *DefaultManager {
|
||||||
mCTX, cancel := context.WithCancel(ctx)
|
serverRouter, err := newServerRouter(ctx, wgInterface)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("server router is not supported: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mCTX, cancel := context.WithCancel(ctx)
|
||||||
dm := &DefaultManager{
|
dm := &DefaultManager{
|
||||||
ctx: mCTX,
|
ctx: mCTX,
|
||||||
stop: cancel,
|
stop: cancel,
|
||||||
clientNetworks: make(map[string]*clientNetwork),
|
clientNetworks: make(map[string]*clientNetwork),
|
||||||
serverRouter: newServerRouter(ctx, wgInterface),
|
serverRouter: serverRouter,
|
||||||
statusRecorder: statusRecorder,
|
statusRecorder: statusRecorder,
|
||||||
wgInterface: wgInterface,
|
wgInterface: wgInterface,
|
||||||
pubKey: pubKey,
|
pubKey: pubKey,
|
||||||
@@ -59,7 +63,9 @@ func NewManager(ctx context.Context, pubKey string, wgInterface *iface.WGIface,
|
|||||||
// Stop stops the manager watchers and clean firewall rules
|
// Stop stops the manager watchers and clean firewall rules
|
||||||
func (m *DefaultManager) Stop() {
|
func (m *DefaultManager) Stop() {
|
||||||
m.stop()
|
m.stop()
|
||||||
|
if m.serverRouter != nil {
|
||||||
m.serverRouter.cleanUp()
|
m.serverRouter.cleanUp()
|
||||||
|
}
|
||||||
m.ctx = nil
|
m.ctx = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,10 +83,13 @@ func (m *DefaultManager) UpdateRoutes(updateSerial uint64, newRoutes []*route.Ro
|
|||||||
|
|
||||||
m.updateClientNetworks(updateSerial, newClientRoutesIDMap)
|
m.updateClientNetworks(updateSerial, newClientRoutesIDMap)
|
||||||
m.notifier.onNewRoutes(newClientRoutesIDMap)
|
m.notifier.onNewRoutes(newClientRoutesIDMap)
|
||||||
|
|
||||||
|
if m.serverRouter != nil {
|
||||||
err := m.serverRouter.updateRoutes(newServerRoutesMap)
|
err := m.serverRouter.updateRoutes(newServerRoutesMap)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ func TestManagerUpdateRoutes(t *testing.T) {
|
|||||||
inputInitRoutes []*route.Route
|
inputInitRoutes []*route.Route
|
||||||
inputRoutes []*route.Route
|
inputRoutes []*route.Route
|
||||||
inputSerial uint64
|
inputSerial uint64
|
||||||
shouldCheckServerRoutes bool
|
|
||||||
serverRoutesExpected int
|
serverRoutesExpected int
|
||||||
clientNetworkWatchersExpected int
|
clientNetworkWatchersExpected int
|
||||||
}{
|
}{
|
||||||
@@ -87,7 +86,6 @@ func TestManagerUpdateRoutes(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
inputSerial: 1,
|
inputSerial: 1,
|
||||||
shouldCheckServerRoutes: runtime.GOOS == "linux",
|
|
||||||
serverRoutesExpected: 2,
|
serverRoutesExpected: 2,
|
||||||
clientNetworkWatchersExpected: 0,
|
clientNetworkWatchersExpected: 0,
|
||||||
},
|
},
|
||||||
@@ -116,7 +114,6 @@ func TestManagerUpdateRoutes(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
inputSerial: 1,
|
inputSerial: 1,
|
||||||
shouldCheckServerRoutes: runtime.GOOS == "linux",
|
|
||||||
serverRoutesExpected: 1,
|
serverRoutesExpected: 1,
|
||||||
clientNetworkWatchersExpected: 1,
|
clientNetworkWatchersExpected: 1,
|
||||||
},
|
},
|
||||||
@@ -174,25 +171,6 @@ func TestManagerUpdateRoutes(t *testing.T) {
|
|||||||
inputSerial: 1,
|
inputSerial: 1,
|
||||||
clientNetworkWatchersExpected: 0,
|
clientNetworkWatchersExpected: 0,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "No Server Routes Should Be Added To Non Linux",
|
|
||||||
inputRoutes: []*route.Route{
|
|
||||||
{
|
|
||||||
ID: "a",
|
|
||||||
NetID: "routeA",
|
|
||||||
Peer: localPeerKey,
|
|
||||||
Network: netip.MustParsePrefix("1.2.3.4/32"),
|
|
||||||
NetworkType: route.IPv4Network,
|
|
||||||
Metric: 9999,
|
|
||||||
Masquerade: false,
|
|
||||||
Enabled: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
inputSerial: 1,
|
|
||||||
shouldCheckServerRoutes: runtime.GOOS != "linux",
|
|
||||||
serverRoutesExpected: 0,
|
|
||||||
clientNetworkWatchersExpected: 0,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "Remove 1 Client Route",
|
name: "Remove 1 Client Route",
|
||||||
inputInitRoutes: []*route.Route{
|
inputInitRoutes: []*route.Route{
|
||||||
@@ -335,7 +313,6 @@ func TestManagerUpdateRoutes(t *testing.T) {
|
|||||||
},
|
},
|
||||||
inputRoutes: []*route.Route{},
|
inputRoutes: []*route.Route{},
|
||||||
inputSerial: 1,
|
inputSerial: 1,
|
||||||
shouldCheckServerRoutes: true,
|
|
||||||
serverRoutesExpected: 0,
|
serverRoutesExpected: 0,
|
||||||
clientNetworkWatchersExpected: 0,
|
clientNetworkWatchersExpected: 0,
|
||||||
},
|
},
|
||||||
@@ -384,7 +361,6 @@ func TestManagerUpdateRoutes(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
inputSerial: 1,
|
inputSerial: 1,
|
||||||
shouldCheckServerRoutes: runtime.GOOS == "linux",
|
|
||||||
serverRoutesExpected: 2,
|
serverRoutesExpected: 2,
|
||||||
clientNetworkWatchersExpected: 1,
|
clientNetworkWatchersExpected: 1,
|
||||||
},
|
},
|
||||||
@@ -419,8 +395,9 @@ func TestManagerUpdateRoutes(t *testing.T) {
|
|||||||
|
|
||||||
require.Len(t, routeManager.clientNetworks, testCase.clientNetworkWatchersExpected, "client networks size should match")
|
require.Len(t, routeManager.clientNetworks, testCase.clientNetworkWatchersExpected, "client networks size should match")
|
||||||
|
|
||||||
if testCase.shouldCheckServerRoutes {
|
if runtime.GOOS == "linux" {
|
||||||
require.Len(t, routeManager.serverRouter.routes, testCase.serverRoutesExpected, "server networks size should match")
|
sr := routeManager.serverRouter.(*defaultServerRouter)
|
||||||
|
require.Len(t, sr.routes, testCase.serverRoutesExpected, "server networks size should match")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
9
client/internal/routemanager/server.go
Normal file
9
client/internal/routemanager/server.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package routemanager
|
||||||
|
|
||||||
|
import "github.com/netbirdio/netbird/route"
|
||||||
|
|
||||||
|
type serverRouter interface {
|
||||||
|
updateRoutes(map[string]*route.Route) error
|
||||||
|
removeFromServerNetwork(*route.Route) error
|
||||||
|
cleanUp()
|
||||||
|
}
|
||||||
@@ -2,20 +2,11 @@ package routemanager
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/iface"
|
"github.com/netbirdio/netbird/iface"
|
||||||
"github.com/netbirdio/netbird/route"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type serverRouter struct {
|
func newServerRouter(context.Context, *iface.WGIface) (serverRouter, error) {
|
||||||
|
return nil, fmt.Errorf("server route not supported on this os")
|
||||||
}
|
}
|
||||||
|
|
||||||
func newServerRouter(ctx context.Context, wgInterface *iface.WGIface) *serverRouter {
|
|
||||||
return &serverRouter{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *serverRouter) updateRoutes(routesMap map[string]*route.Route) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *serverRouter) cleanUp() {}
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
"github.com/netbirdio/netbird/route"
|
"github.com/netbirdio/netbird/route"
|
||||||
)
|
)
|
||||||
|
|
||||||
type serverRouter struct {
|
type defaultServerRouter struct {
|
||||||
mux sync.Mutex
|
mux sync.Mutex
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
routes map[string]*route.Route
|
routes map[string]*route.Route
|
||||||
@@ -21,16 +21,21 @@ type serverRouter struct {
|
|||||||
wgInterface *iface.WGIface
|
wgInterface *iface.WGIface
|
||||||
}
|
}
|
||||||
|
|
||||||
func newServerRouter(ctx context.Context, wgInterface *iface.WGIface) *serverRouter {
|
func newServerRouter(ctx context.Context, wgInterface *iface.WGIface) (serverRouter, error) {
|
||||||
return &serverRouter{
|
firewall, err := NewFirewall(ctx)
|
||||||
ctx: ctx,
|
if err != nil {
|
||||||
routes: make(map[string]*route.Route),
|
return nil, err
|
||||||
firewall: NewFirewall(ctx),
|
|
||||||
wgInterface: wgInterface,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *serverRouter) updateRoutes(routesMap map[string]*route.Route) error {
|
return &defaultServerRouter{
|
||||||
|
ctx: ctx,
|
||||||
|
routes: make(map[string]*route.Route),
|
||||||
|
firewall: firewall,
|
||||||
|
wgInterface: wgInterface,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *defaultServerRouter) updateRoutes(routesMap map[string]*route.Route) error {
|
||||||
serverRoutesToRemove := make([]string, 0)
|
serverRoutesToRemove := make([]string, 0)
|
||||||
|
|
||||||
if len(routesMap) > 0 {
|
if len(routesMap) > 0 {
|
||||||
@@ -81,7 +86,7 @@ func (m *serverRouter) updateRoutes(routesMap map[string]*route.Route) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *serverRouter) removeFromServerNetwork(route *route.Route) error {
|
func (m *defaultServerRouter) removeFromServerNetwork(route *route.Route) error {
|
||||||
select {
|
select {
|
||||||
case <-m.ctx.Done():
|
case <-m.ctx.Done():
|
||||||
log.Infof("not removing from server network because context is done")
|
log.Infof("not removing from server network because context is done")
|
||||||
@@ -98,7 +103,7 @@ func (m *serverRouter) removeFromServerNetwork(route *route.Route) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *serverRouter) addToServerNetwork(route *route.Route) error {
|
func (m *defaultServerRouter) addToServerNetwork(route *route.Route) error {
|
||||||
select {
|
select {
|
||||||
case <-m.ctx.Done():
|
case <-m.ctx.Done():
|
||||||
log.Infof("not adding to server network because context is done")
|
log.Infof("not adding to server network because context is done")
|
||||||
@@ -115,6 +120,6 @@ func (m *serverRouter) addToServerNetwork(route *route.Route) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *serverRouter) cleanUp() {
|
func (m *defaultServerRouter) cleanUp() {
|
||||||
m.firewall.CleanRoutingRules()
|
m.firewall.CleanRoutingRules()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ func InterfaceFilter(disallowList []string) func(string) bool {
|
|||||||
|
|
||||||
for _, s := range disallowList {
|
for _, s := range disallowList {
|
||||||
if strings.HasPrefix(iFace, s) {
|
if strings.HasPrefix(iFace, s) {
|
||||||
log.Debugf("ignoring interface %s - it is not allowed", iFace)
|
log.Tracef("ignoring interface %s - it is not allowed", iFace)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,14 +54,14 @@ type bpfSpecs struct {
|
|||||||
//
|
//
|
||||||
// It can be passed ebpf.CollectionSpec.Assign.
|
// It can be passed ebpf.CollectionSpec.Assign.
|
||||||
type bpfProgramSpecs struct {
|
type bpfProgramSpecs struct {
|
||||||
XdpProgFunc *ebpf.ProgramSpec `ebpf:"xdp_prog_func"`
|
NbWgProxy *ebpf.ProgramSpec `ebpf:"nb_wg_proxy"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// bpfMapSpecs contains maps before they are loaded into the kernel.
|
// bpfMapSpecs contains maps before they are loaded into the kernel.
|
||||||
//
|
//
|
||||||
// It can be passed ebpf.CollectionSpec.Assign.
|
// It can be passed ebpf.CollectionSpec.Assign.
|
||||||
type bpfMapSpecs struct {
|
type bpfMapSpecs struct {
|
||||||
XdpPortMap *ebpf.MapSpec `ebpf:"xdp_port_map"`
|
NbWgProxySettingsMap *ebpf.MapSpec `ebpf:"nb_wg_proxy_settings_map"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// bpfObjects contains all objects after they have been loaded into the kernel.
|
// bpfObjects contains all objects after they have been loaded into the kernel.
|
||||||
@@ -83,12 +83,12 @@ func (o *bpfObjects) Close() error {
|
|||||||
//
|
//
|
||||||
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
|
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
|
||||||
type bpfMaps struct {
|
type bpfMaps struct {
|
||||||
XdpPortMap *ebpf.Map `ebpf:"xdp_port_map"`
|
NbWgProxySettingsMap *ebpf.Map `ebpf:"nb_wg_proxy_settings_map"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *bpfMaps) Close() error {
|
func (m *bpfMaps) Close() error {
|
||||||
return _BpfClose(
|
return _BpfClose(
|
||||||
m.XdpPortMap,
|
m.NbWgProxySettingsMap,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,12 +96,12 @@ func (m *bpfMaps) Close() error {
|
|||||||
//
|
//
|
||||||
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
|
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
|
||||||
type bpfPrograms struct {
|
type bpfPrograms struct {
|
||||||
XdpProgFunc *ebpf.Program `ebpf:"xdp_prog_func"`
|
NbWgProxy *ebpf.Program `ebpf:"nb_wg_proxy"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *bpfPrograms) Close() error {
|
func (p *bpfPrograms) Close() error {
|
||||||
return _BpfClose(
|
return _BpfClose(
|
||||||
p.XdpProgFunc,
|
p.NbWgProxy,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -54,14 +54,14 @@ type bpfSpecs struct {
|
|||||||
//
|
//
|
||||||
// It can be passed ebpf.CollectionSpec.Assign.
|
// It can be passed ebpf.CollectionSpec.Assign.
|
||||||
type bpfProgramSpecs struct {
|
type bpfProgramSpecs struct {
|
||||||
XdpProgFunc *ebpf.ProgramSpec `ebpf:"xdp_prog_func"`
|
NbWgProxy *ebpf.ProgramSpec `ebpf:"nb_wg_proxy"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// bpfMapSpecs contains maps before they are loaded into the kernel.
|
// bpfMapSpecs contains maps before they are loaded into the kernel.
|
||||||
//
|
//
|
||||||
// It can be passed ebpf.CollectionSpec.Assign.
|
// It can be passed ebpf.CollectionSpec.Assign.
|
||||||
type bpfMapSpecs struct {
|
type bpfMapSpecs struct {
|
||||||
XdpPortMap *ebpf.MapSpec `ebpf:"xdp_port_map"`
|
NbWgProxySettingsMap *ebpf.MapSpec `ebpf:"nb_wg_proxy_settings_map"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// bpfObjects contains all objects after they have been loaded into the kernel.
|
// bpfObjects contains all objects after they have been loaded into the kernel.
|
||||||
@@ -83,12 +83,12 @@ func (o *bpfObjects) Close() error {
|
|||||||
//
|
//
|
||||||
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
|
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
|
||||||
type bpfMaps struct {
|
type bpfMaps struct {
|
||||||
XdpPortMap *ebpf.Map `ebpf:"xdp_port_map"`
|
NbWgProxySettingsMap *ebpf.Map `ebpf:"nb_wg_proxy_settings_map"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *bpfMaps) Close() error {
|
func (m *bpfMaps) Close() error {
|
||||||
return _BpfClose(
|
return _BpfClose(
|
||||||
m.XdpPortMap,
|
m.NbWgProxySettingsMap,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,12 +96,12 @@ func (m *bpfMaps) Close() error {
|
|||||||
//
|
//
|
||||||
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
|
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
|
||||||
type bpfPrograms struct {
|
type bpfPrograms struct {
|
||||||
XdpProgFunc *ebpf.Program `ebpf:"xdp_prog_func"`
|
NbWgProxy *ebpf.Program `ebpf:"nb_wg_proxy"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *bpfPrograms) Close() error {
|
func (p *bpfPrograms) Close() error {
|
||||||
return _BpfClose(
|
return _BpfClose(
|
||||||
p.XdpProgFunc,
|
p.NbWgProxy,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -50,22 +50,22 @@ func (l *EBPF) Load(proxyPort, wgPort int) error {
|
|||||||
_ = objs.Close()
|
_ = objs.Close()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
err = objs.XdpPortMap.Put(mapKeyProxyPort, uint16(proxyPort))
|
err = objs.NbWgProxySettingsMap.Put(mapKeyProxyPort, uint16(proxyPort))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = objs.XdpPortMap.Put(mapKeyWgPort, uint16(wgPort))
|
err = objs.NbWgProxySettingsMap.Put(mapKeyWgPort, uint16(wgPort))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
_ = objs.XdpPortMap.Close()
|
_ = objs.NbWgProxySettingsMap.Close()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
l.link, err = link.AttachXDP(link.XDPOptions{
|
l.link, err = link.AttachXDP(link.XDPOptions{
|
||||||
Program: objs.XdpProgFunc,
|
Program: objs.NbWgProxy,
|
||||||
Interface: ifce.Index,
|
Interface: ifce.Index,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -75,7 +75,7 @@ func (l *EBPF) Load(proxyPort, wgPort int) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Free free ebpf program
|
// Free ebpf program
|
||||||
func (l *EBPF) Free() error {
|
func (l *EBPF) Free() error {
|
||||||
if l.link != nil {
|
if l.link != nil {
|
||||||
return l.link.Close()
|
return l.link.Close()
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
const __u32 map_key_proxy_port = 0;
|
const __u32 map_key_proxy_port = 0;
|
||||||
const __u32 map_key_wg_port = 1;
|
const __u32 map_key_wg_port = 1;
|
||||||
|
|
||||||
struct bpf_map_def SEC("maps") xdp_port_map = {
|
struct bpf_map_def SEC("maps") nb_wg_proxy_settings_map = {
|
||||||
.type = BPF_MAP_TYPE_ARRAY,
|
.type = BPF_MAP_TYPE_ARRAY,
|
||||||
.key_size = sizeof(__u32),
|
.key_size = sizeof(__u32),
|
||||||
.value_size = sizeof(__u16),
|
.value_size = sizeof(__u16),
|
||||||
@@ -27,14 +27,14 @@ __u16 wg_port = 0;
|
|||||||
|
|
||||||
bool read_port_settings() {
|
bool read_port_settings() {
|
||||||
__u16 *value;
|
__u16 *value;
|
||||||
value = bpf_map_lookup_elem(&xdp_port_map, &map_key_proxy_port);
|
value = bpf_map_lookup_elem(&nb_wg_proxy_settings_map, &map_key_proxy_port);
|
||||||
if(!value) {
|
if(!value) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
proxy_port = *value;
|
proxy_port = *value;
|
||||||
|
|
||||||
value = bpf_map_lookup_elem(&xdp_port_map, &map_key_wg_port);
|
value = bpf_map_lookup_elem(&nb_wg_proxy_settings_map, &map_key_wg_port);
|
||||||
if(!value) {
|
if(!value) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -44,7 +44,7 @@ bool read_port_settings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SEC("xdp")
|
SEC("xdp")
|
||||||
int xdp_prog_func(struct xdp_md *ctx) {
|
int nb_wg_proxy(struct xdp_md *ctx) {
|
||||||
if(proxy_port == 0 || wg_port == 0) {
|
if(proxy_port == 0 || wg_port == 0) {
|
||||||
if(!read_port_settings()){
|
if(!read_port_settings()){
|
||||||
return XDP_PASS;
|
return XDP_PASS;
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ func (w *Factory) GetProxy() Proxy {
|
|||||||
|
|
||||||
func (w *Factory) Free() error {
|
func (w *Factory) Free() error {
|
||||||
if w.ebpfProxy != nil {
|
if w.ebpfProxy != nil {
|
||||||
return w.ebpfProxy.CloseConn()
|
return w.ebpfProxy.Free()
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ func NewFactory(wgPort int) *Factory {
|
|||||||
ebpfProxy := NewWGEBPFProxy(wgPort)
|
ebpfProxy := NewWGEBPFProxy(wgPort)
|
||||||
err := ebpfProxy.Listen()
|
err := ebpfProxy.Listen()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("failed to initialize ebpf proxy: %s", err)
|
log.Warnf("failed to initialize ebpf proxy, fallback to user space proxy: %s", err)
|
||||||
return f
|
return f
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ func (p *WGEBPFProxy) Listen() error {
|
|||||||
p.conn, err = net.ListenUDP("udp", &addr)
|
p.conn, err = net.ListenUDP("udp", &addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cErr := p.Free()
|
cErr := p.Free()
|
||||||
if err != nil {
|
if cErr != nil {
|
||||||
log.Errorf("failed to close the wgproxy: %s", cErr)
|
log.Errorf("failed to close the wgproxy: %s", cErr)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
@@ -104,6 +104,7 @@ func (p *WGEBPFProxy) CloseConn() error {
|
|||||||
|
|
||||||
// Free resources
|
// Free resources
|
||||||
func (p *WGEBPFProxy) Free() error {
|
func (p *WGEBPFProxy) Free() error {
|
||||||
|
log.Debugf("free up ebpf wg proxy")
|
||||||
var err1, err2, err3 error
|
var err1, err2, err3 error
|
||||||
if p.conn != nil {
|
if p.conn != nil {
|
||||||
err1 = p.conn.Close()
|
err1 = p.conn.Close()
|
||||||
@@ -153,7 +154,9 @@ func (p *WGEBPFProxy) proxyToRemote() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p.turnConnMutex.Lock()
|
||||||
conn, ok := p.turnConnStore[uint16(addr.Port)]
|
conn, ok := p.turnConnStore[uint16(addr.Port)]
|
||||||
|
p.turnConnMutex.Unlock()
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Errorf("turn conn not found by port: %d", addr.Port)
|
log.Errorf("turn conn not found by port: %d", addr.Port)
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ type WGUserSpaceProxy struct {
|
|||||||
|
|
||||||
// NewWGUserSpaceProxy instantiate a user space WireGuard proxy
|
// NewWGUserSpaceProxy instantiate a user space WireGuard proxy
|
||||||
func NewWGUserSpaceProxy(wgPort int) *WGUserSpaceProxy {
|
func NewWGUserSpaceProxy(wgPort int) *WGUserSpaceProxy {
|
||||||
|
log.Debugf("instantiate new userspace proxy")
|
||||||
p := &WGUserSpaceProxy{
|
p := &WGUserSpaceProxy{
|
||||||
localWGListenPort: wgPort,
|
localWGListenPort: wgPort,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -130,16 +130,22 @@ func ParseNameServerURL(nsURL string) (NameServer, error) {
|
|||||||
|
|
||||||
// Copy copies a nameserver group object
|
// Copy copies a nameserver group object
|
||||||
func (g *NameServerGroup) Copy() *NameServerGroup {
|
func (g *NameServerGroup) Copy() *NameServerGroup {
|
||||||
return &NameServerGroup{
|
nsGroup := &NameServerGroup{
|
||||||
ID: g.ID,
|
ID: g.ID,
|
||||||
Name: g.Name,
|
Name: g.Name,
|
||||||
Description: g.Description,
|
Description: g.Description,
|
||||||
NameServers: g.NameServers,
|
NameServers: make([]NameServer, len(g.NameServers)),
|
||||||
Groups: g.Groups,
|
Groups: make([]string, len(g.Groups)),
|
||||||
Enabled: g.Enabled,
|
Enabled: g.Enabled,
|
||||||
Primary: g.Primary,
|
Primary: g.Primary,
|
||||||
Domains: g.Domains,
|
Domains: make([]string, len(g.Domains)),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
copy(nsGroup.NameServers, g.NameServers)
|
||||||
|
copy(nsGroup.Groups, g.Groups)
|
||||||
|
copy(nsGroup.Domains, g.Domains)
|
||||||
|
|
||||||
|
return nsGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsEqual compares one nameserver group with the other
|
// IsEqual compares one nameserver group with the other
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -31,7 +31,7 @@ require (
|
|||||||
fyne.io/fyne/v2 v2.1.4
|
fyne.io/fyne/v2 v2.1.4
|
||||||
github.com/c-robinson/iplib v1.0.3
|
github.com/c-robinson/iplib v1.0.3
|
||||||
github.com/cilium/ebpf v0.10.0
|
github.com/cilium/ebpf v0.10.0
|
||||||
github.com/coreos/go-iptables v0.6.0
|
github.com/coreos/go-iptables v0.7.0
|
||||||
github.com/creack/pty v1.1.18
|
github.com/creack/pty v1.1.18
|
||||||
github.com/eko/gocache/v3 v3.1.1
|
github.com/eko/gocache/v3 v3.1.1
|
||||||
github.com/getlantern/systray v1.2.1
|
github.com/getlantern/systray v1.2.1
|
||||||
|
|||||||
4
go.sum
4
go.sum
@@ -131,8 +131,8 @@ github.com/containerd/typeurl v1.0.2/go.mod h1:9trJWW2sRlGub4wZJRTW83VtbOLS6hwcD
|
|||||||
github.com/coocood/freecache v1.2.1 h1:/v1CqMq45NFH9mp/Pt142reundeBM0dVUD3osQBeu/U=
|
github.com/coocood/freecache v1.2.1 h1:/v1CqMq45NFH9mp/Pt142reundeBM0dVUD3osQBeu/U=
|
||||||
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||||
github.com/coreos/go-iptables v0.6.0 h1:is9qnZMPYjLd8LYqmm/qlE+wwEgJIkTYdhV3rfZo4jk=
|
github.com/coreos/go-iptables v0.7.0 h1:XWM3V+MPRr5/q51NuWSgU0fqMad64Zyxs8ZUoMsamr8=
|
||||||
github.com/coreos/go-iptables v0.6.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
|
github.com/coreos/go-iptables v0.7.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
|
||||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||||
github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=
|
github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=
|
||||||
|
|||||||
736
infrastructure_files/getting-started-with-zitadel.sh
Normal file
736
infrastructure_files/getting-started-with-zitadel.sh
Normal file
@@ -0,0 +1,736 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
handle_request_command_status() {
|
||||||
|
PARSED_RESPONSE=$1
|
||||||
|
FUNCTION_NAME=$2
|
||||||
|
RESPONSE=$3
|
||||||
|
if [[ $PARSED_RESPONSE -ne 0 ]]; then
|
||||||
|
echo "ERROR calling $FUNCTION_NAME:" $(echo "$RESPONSE" | jq -r '.message') > /dev/stderr
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_zitadel_request_response() {
|
||||||
|
PARSED_RESPONSE=$1
|
||||||
|
FUNCTION_NAME=$2
|
||||||
|
RESPONSE=$3
|
||||||
|
if [[ $PARSED_RESPONSE == "null" ]]; then
|
||||||
|
echo "ERROR calling $FUNCTION_NAME:" $(echo "$RESPONSE" | jq -r '.message') > /dev/stderr
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
}
|
||||||
|
|
||||||
|
check_docker_compose() {
|
||||||
|
if command -v docker-compose &> /dev/null
|
||||||
|
then
|
||||||
|
echo "docker-compose"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if docker compose --help &> /dev/null
|
||||||
|
then
|
||||||
|
echo "docker compose"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "docker-compose is not installed or not in PATH. Please follow the steps from the official guide: https://docs.docker.com/engine/install/" > /dev/stderr
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
check_jq() {
|
||||||
|
if ! command -v jq &> /dev/null
|
||||||
|
then
|
||||||
|
echo "jq is not installed or not in PATH, please install with your package manager. e.g. sudo apt install jq" > /dev/stderr
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_crdb() {
|
||||||
|
set +e
|
||||||
|
while true; do
|
||||||
|
if $DOCKER_COMPOSE_COMMAND exec -T crdb curl -sf -o /dev/null 'http://localhost:8080/health?ready=1'; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
echo -n " ."
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
echo " done"
|
||||||
|
set -e
|
||||||
|
}
|
||||||
|
|
||||||
|
init_crdb() {
|
||||||
|
echo -e "\nInitializing Zitadel's CockroachDB\n\n"
|
||||||
|
$DOCKER_COMPOSE_COMMAND up -d crdb
|
||||||
|
echo ""
|
||||||
|
# shellcheck disable=SC2028
|
||||||
|
echo -n "Waiting cockroachDB to become ready "
|
||||||
|
wait_crdb
|
||||||
|
$DOCKER_COMPOSE_COMMAND exec -T crdb /bin/bash -c "cp /cockroach/certs/* /zitadel-certs/ && cockroach cert create-client --overwrite --certs-dir /zitadel-certs/ --ca-key /zitadel-certs/ca.key zitadel_user && chown -R 1000:1000 /zitadel-certs/"
|
||||||
|
handle_request_command_status $? "init_crdb failed" ""
|
||||||
|
}
|
||||||
|
|
||||||
|
get_main_ip_address() {
|
||||||
|
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||||
|
interface=$(route -n get default | grep 'interface:' | awk '{print $2}')
|
||||||
|
ip_address=$(ifconfig "$interface" | grep 'inet ' | awk '{print $2}')
|
||||||
|
else
|
||||||
|
interface=$(ip route | grep default | awk '{print $5}' | head -n 1)
|
||||||
|
ip_address=$(ip addr show "$interface" | grep 'inet ' | awk '{print $2}' | cut -d'/' -f1)
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$ip_address"
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_pat() {
|
||||||
|
PAT_PATH=$1
|
||||||
|
set +e
|
||||||
|
while true; do
|
||||||
|
if [[ -f "$PAT_PATH" ]]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
echo -n " ."
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
echo " done"
|
||||||
|
set -e
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_api() {
|
||||||
|
INSTANCE_URL=$1
|
||||||
|
PAT=$2
|
||||||
|
set +e
|
||||||
|
while true; do
|
||||||
|
curl -s --fail -o /dev/null "$INSTANCE_URL/auth/v1/users/me" -H "Authorization: Bearer $PAT"
|
||||||
|
if [[ $? -eq 0 ]]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
echo -n " ."
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
echo " done"
|
||||||
|
set -e
|
||||||
|
}
|
||||||
|
|
||||||
|
create_new_project() {
|
||||||
|
INSTANCE_URL=$1
|
||||||
|
PAT=$2
|
||||||
|
PROJECT_NAME="NETBIRD"
|
||||||
|
|
||||||
|
RESPONSE=$(
|
||||||
|
curl -sS -X POST "$INSTANCE_URL/management/v1/projects" \
|
||||||
|
-H "Authorization: Bearer $PAT" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"name": "'"$PROJECT_NAME"'"}'
|
||||||
|
)
|
||||||
|
PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.id')
|
||||||
|
handle_zitadel_request_response "$PARSED_RESPONSE" "create_new_project" "$RESPONSE"
|
||||||
|
echo "$PARSED_RESPONSE"
|
||||||
|
}
|
||||||
|
|
||||||
|
create_new_application() {
|
||||||
|
INSTANCE_URL=$1
|
||||||
|
PAT=$2
|
||||||
|
APPLICATION_NAME=$3
|
||||||
|
BASE_REDIRECT_URL1=$4
|
||||||
|
BASE_REDIRECT_URL2=$5
|
||||||
|
LOGOUT_URL=$6
|
||||||
|
ZITADEL_DEV_MODE=$7
|
||||||
|
|
||||||
|
RESPONSE=$(
|
||||||
|
curl -sS -X POST "$INSTANCE_URL/management/v1/projects/$PROJECT_ID/apps/oidc" \
|
||||||
|
-H "Authorization: Bearer $PAT" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"name": "'"$APPLICATION_NAME"'",
|
||||||
|
"redirectUris": [
|
||||||
|
"'"$BASE_REDIRECT_URL1"'",
|
||||||
|
"'"$BASE_REDIRECT_URL2"'"
|
||||||
|
],
|
||||||
|
"postLogoutRedirectUris": [
|
||||||
|
"'"$LOGOUT_URL"'"
|
||||||
|
],
|
||||||
|
"RESPONSETypes": [
|
||||||
|
"OIDC_RESPONSE_TYPE_CODE"
|
||||||
|
],
|
||||||
|
"grantTypes": [
|
||||||
|
"OIDC_GRANT_TYPE_AUTHORIZATION_CODE",
|
||||||
|
"OIDC_GRANT_TYPE_REFRESH_TOKEN"
|
||||||
|
],
|
||||||
|
"appType": "OIDC_APP_TYPE_USER_AGENT",
|
||||||
|
"authMethodType": "OIDC_AUTH_METHOD_TYPE_NONE",
|
||||||
|
"version": "OIDC_VERSION_1_0",
|
||||||
|
"devMode": '"$ZITADEL_DEV_MODE"',
|
||||||
|
"accessTokenType": "OIDC_TOKEN_TYPE_JWT",
|
||||||
|
"accessTokenRoleAssertion": true,
|
||||||
|
"skipNativeAppSuccessPage": true
|
||||||
|
}'
|
||||||
|
)
|
||||||
|
|
||||||
|
PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.clientId')
|
||||||
|
handle_zitadel_request_response "$PARSED_RESPONSE" "create_new_application" "$RESPONSE"
|
||||||
|
echo "$PARSED_RESPONSE"
|
||||||
|
}
|
||||||
|
|
||||||
|
create_service_user() {
|
||||||
|
INSTANCE_URL=$1
|
||||||
|
PAT=$2
|
||||||
|
|
||||||
|
RESPONSE=$(
|
||||||
|
curl -sS -X POST "$INSTANCE_URL/management/v1/users/machine" \
|
||||||
|
-H "Authorization: Bearer $PAT" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"userName": "netbird-service-account",
|
||||||
|
"name": "Netbird Service Account",
|
||||||
|
"description": "Netbird Service Account for IDP management",
|
||||||
|
"accessTokenType": "ACCESS_TOKEN_TYPE_JWT"
|
||||||
|
}'
|
||||||
|
)
|
||||||
|
PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.userId')
|
||||||
|
handle_zitadel_request_response "$PARSED_RESPONSE" "create_service_user" "$RESPONSE"
|
||||||
|
echo "$PARSED_RESPONSE"
|
||||||
|
}
|
||||||
|
|
||||||
|
create_service_user_secret() {
|
||||||
|
INSTANCE_URL=$1
|
||||||
|
PAT=$2
|
||||||
|
USER_ID=$3
|
||||||
|
|
||||||
|
RESPONSE=$(
|
||||||
|
curl -sS -X PUT "$INSTANCE_URL/management/v1/users/$USER_ID/secret" \
|
||||||
|
-H "Authorization: Bearer $PAT" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{}'
|
||||||
|
)
|
||||||
|
SERVICE_USER_CLIENT_ID=$(echo "$RESPONSE" | jq -r '.clientId')
|
||||||
|
handle_zitadel_request_response "$SERVICE_USER_CLIENT_ID" "create_service_user_secret_id" "$RESPONSE"
|
||||||
|
SERVICE_USER_CLIENT_SECRET=$(echo "$RESPONSE" | jq -r '.clientSecret')
|
||||||
|
handle_zitadel_request_response "$SERVICE_USER_CLIENT_SECRET" "create_service_user_secret" "$RESPONSE"
|
||||||
|
}
|
||||||
|
|
||||||
|
add_organization_user_manager() {
|
||||||
|
INSTANCE_URL=$1
|
||||||
|
PAT=$2
|
||||||
|
USER_ID=$3
|
||||||
|
|
||||||
|
RESPONSE=$(
|
||||||
|
curl -sS -X POST "$INSTANCE_URL/management/v1/orgs/me/members" \
|
||||||
|
-H "Authorization: Bearer $PAT" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"userId": "'"$USER_ID"'",
|
||||||
|
"roles": [
|
||||||
|
"ORG_USER_MANAGER"
|
||||||
|
]
|
||||||
|
}'
|
||||||
|
)
|
||||||
|
PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.details.creationDate')
|
||||||
|
handle_zitadel_request_response "$PARSED_RESPONSE" "add_organization_user_manager" "$RESPONSE"
|
||||||
|
echo "$PARSED_RESPONSE"
|
||||||
|
}
|
||||||
|
|
||||||
|
create_admin_user() {
|
||||||
|
INSTANCE_URL=$1
|
||||||
|
PAT=$2
|
||||||
|
USERNAME=$3
|
||||||
|
PASSWORD=$4
|
||||||
|
RESPONSE=$(
|
||||||
|
curl -sS -X POST "$INSTANCE_URL/management/v1/users/human/_import" \
|
||||||
|
-H "Authorization: Bearer $PAT" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"userName": "'"$USERNAME"'",
|
||||||
|
"profile": {
|
||||||
|
"firstName": "Zitadel",
|
||||||
|
"lastName": "Admin"
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"email": "'"$USERNAME"'",
|
||||||
|
"isEmailVerified": true
|
||||||
|
},
|
||||||
|
"password": "'"$PASSWORD"'",
|
||||||
|
"passwordChangeRequired": true
|
||||||
|
}'
|
||||||
|
)
|
||||||
|
PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.userId')
|
||||||
|
handle_zitadel_request_response "$PARSED_RESPONSE" "create_admin_user" "$RESPONSE"
|
||||||
|
echo "$PARSED_RESPONSE"
|
||||||
|
}
|
||||||
|
|
||||||
|
add_instance_admin() {
|
||||||
|
INSTANCE_URL=$1
|
||||||
|
PAT=$2
|
||||||
|
USER_ID=$3
|
||||||
|
|
||||||
|
RESPONSE=$(
|
||||||
|
curl -sS -X POST "$INSTANCE_URL/admin/v1/members" \
|
||||||
|
-H "Authorization: Bearer $PAT" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"userId": "'"$USER_ID"'",
|
||||||
|
"roles": [
|
||||||
|
"IAM_OWNER"
|
||||||
|
]
|
||||||
|
}'
|
||||||
|
)
|
||||||
|
PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.details.creationDate')
|
||||||
|
handle_zitadel_request_response "$PARSED_RESPONSE" "add_instance_admin" "$RESPONSE"
|
||||||
|
echo "$PARSED_RESPONSE"
|
||||||
|
}
|
||||||
|
|
||||||
|
delete_auto_service_user() {
|
||||||
|
INSTANCE_URL=$1
|
||||||
|
PAT=$2
|
||||||
|
|
||||||
|
RESPONSE=$(
|
||||||
|
curl -sS -X GET "$INSTANCE_URL/auth/v1/users/me" \
|
||||||
|
-H "Authorization: Bearer $PAT" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
)
|
||||||
|
USER_ID=$(echo "$RESPONSE" | jq -r '.user.id')
|
||||||
|
handle_zitadel_request_response "$USER_ID" "delete_auto_service_user_get_user" "$RESPONSE"
|
||||||
|
|
||||||
|
RESPONSE=$(
|
||||||
|
curl -sS -X DELETE "$INSTANCE_URL/admin/v1/members/$USER_ID" \
|
||||||
|
-H "Authorization: Bearer $PAT" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
)
|
||||||
|
PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.details.changeDate')
|
||||||
|
handle_zitadel_request_response "$PARSED_RESPONSE" "delete_auto_service_user_remove_instance_permissions" "$RESPONSE"
|
||||||
|
|
||||||
|
RESPONSE=$(
|
||||||
|
curl -sS -X DELETE "$INSTANCE_URL/management/v1/orgs/me/members/$USER_ID" \
|
||||||
|
-H "Authorization: Bearer $PAT" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
)
|
||||||
|
PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.details.changeDate')
|
||||||
|
handle_zitadel_request_response "$PARSED_RESPONSE" "delete_auto_service_user_remove_org_permissions" "$RESPONSE"
|
||||||
|
echo "$PARSED_RESPONSE"
|
||||||
|
}
|
||||||
|
|
||||||
|
init_zitadel() {
|
||||||
|
echo -e "\nInitializing Zitadel with NetBird's applications\n"
|
||||||
|
INSTANCE_URL="$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN:$NETBIRD_PORT"
|
||||||
|
|
||||||
|
TOKEN_PATH=./machinekey/zitadel-admin-sa.token
|
||||||
|
|
||||||
|
echo -n "Waiting for Zitadel's PAT to be created "
|
||||||
|
wait_pat "$TOKEN_PATH"
|
||||||
|
echo "Reading Zitadel PAT"
|
||||||
|
PAT=$(cat $TOKEN_PATH)
|
||||||
|
if [ "$PAT" = "null" ]; then
|
||||||
|
echo "Failed requesting getting Zitadel PAT"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -n "Waiting for Zitadel to become ready "
|
||||||
|
wait_api "$INSTANCE_URL" "$PAT"
|
||||||
|
|
||||||
|
# create the zitadel project
|
||||||
|
echo "Creating new zitadel project"
|
||||||
|
PROJECT_ID=$(create_new_project "$INSTANCE_URL" "$PAT")
|
||||||
|
|
||||||
|
ZITADEL_DEV_MODE=false
|
||||||
|
BASE_REDIRECT_URL=$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN
|
||||||
|
if [[ $NETBIRD_HTTP_PROTOCOL == "http" ]]; then
|
||||||
|
ZITADEL_DEV_MODE=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# create zitadel spa applications
|
||||||
|
echo "Creating new Zitadel SPA Dashboard application"
|
||||||
|
DASHBOARD_APPLICATION_CLIENT_ID=$(create_new_application "$INSTANCE_URL" "$PAT" "Dashboard" "$BASE_REDIRECT_URL/nb-auth" "$BASE_REDIRECT_URL/nb-silent-auth" "$BASE_REDIRECT_URL/" "$ZITADEL_DEV_MODE")
|
||||||
|
|
||||||
|
echo "Creating new Zitadel SPA Cli application"
|
||||||
|
CLI_APPLICATION_CLIENT_ID=$(create_new_application "$INSTANCE_URL" "$PAT" "Cli" "http://localhost:53000/" "http://localhost:54000/" "http://localhost:53000/" "true")
|
||||||
|
|
||||||
|
MACHINE_USER_ID=$(create_service_user "$INSTANCE_URL" "$PAT")
|
||||||
|
|
||||||
|
SERVICE_USER_CLIENT_ID="null"
|
||||||
|
SERVICE_USER_CLIENT_SECRET="null"
|
||||||
|
|
||||||
|
create_service_user_secret "$INSTANCE_URL" "$PAT" "$MACHINE_USER_ID"
|
||||||
|
|
||||||
|
DATE=$(add_organization_user_manager "$INSTANCE_URL" "$PAT" "$MACHINE_USER_ID")
|
||||||
|
|
||||||
|
ZITADEL_ADMIN_USERNAME="admin@$NETBIRD_DOMAIN"
|
||||||
|
ZITADEL_ADMIN_PASSWORD="$(openssl rand -base64 32 | sed 's/=//g')@"
|
||||||
|
|
||||||
|
HUMAN_USER_ID=$(create_admin_user "$INSTANCE_URL" "$PAT" "$ZITADEL_ADMIN_USERNAME" "$ZITADEL_ADMIN_PASSWORD")
|
||||||
|
|
||||||
|
DATE="null"
|
||||||
|
|
||||||
|
DATE=$(add_instance_admin "$INSTANCE_URL" "$PAT" "$HUMAN_USER_ID")
|
||||||
|
|
||||||
|
DATE="null"
|
||||||
|
DATE=$(delete_auto_service_user "$INSTANCE_URL" "$PAT")
|
||||||
|
if [ "$DATE" = "null" ]; then
|
||||||
|
echo "Failed deleting auto service user"
|
||||||
|
echo "Please remove it manually"
|
||||||
|
fi
|
||||||
|
|
||||||
|
export NETBIRD_AUTH_CLIENT_ID=$DASHBOARD_APPLICATION_CLIENT_ID
|
||||||
|
export NETBIRD_AUTH_CLIENT_ID_CLI=$CLI_APPLICATION_CLIENT_ID
|
||||||
|
export NETBIRD_IDP_MGMT_CLIENT_ID=$SERVICE_USER_CLIENT_ID
|
||||||
|
export NETBIRD_IDP_MGMT_CLIENT_SECRET=$SERVICE_USER_CLIENT_SECRET
|
||||||
|
export ZITADEL_ADMIN_USERNAME
|
||||||
|
export ZITADEL_ADMIN_PASSWORD
|
||||||
|
}
|
||||||
|
|
||||||
|
check_nb_domain() {
|
||||||
|
DOMAIN=$1
|
||||||
|
if [ "$DOMAIN-x" == "-x" ]; then
|
||||||
|
echo "The NETBIRD_DOMAIN variable cannot be empty." > /dev/stderr
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$DOMAIN" == "netbird.example.com" ]; then
|
||||||
|
echo "The NETBIRD_DOMAIN cannot be netbird.example.com" > /dev/stderr
|
||||||
|
retrun 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
read_nb_domain() {
|
||||||
|
READ_NETBIRD_DOMAIN=""
|
||||||
|
echo -n "Enter the domain you want to use for NetBird (e.g. netbird.my-domain.com): " > /dev/stderr
|
||||||
|
read -r READ_NETBIRD_DOMAIN < /dev/tty
|
||||||
|
if ! check_nb_domain "$READ_NETBIRD_DOMAIN"; then
|
||||||
|
read_nb_domain
|
||||||
|
fi
|
||||||
|
echo "$READ_NETBIRD_DOMAIN"
|
||||||
|
}
|
||||||
|
|
||||||
|
initEnvironment() {
|
||||||
|
CADDY_SECURE_DOMAIN=""
|
||||||
|
ZITADEL_EXTERNALSECURE="false"
|
||||||
|
ZITADEL_TLS_MODE="disabled"
|
||||||
|
ZITADEL_MASTERKEY="$(openssl rand -base64 32 | head -c 32)"
|
||||||
|
NETBIRD_PORT=80
|
||||||
|
NETBIRD_HTTP_PROTOCOL="http"
|
||||||
|
TURN_USER="self"
|
||||||
|
TURN_PASSWORD=$(openssl rand -base64 32 | sed 's/=//g')
|
||||||
|
TURN_MIN_PORT=49152
|
||||||
|
TURN_MAX_PORT=65535
|
||||||
|
|
||||||
|
if ! check_nb_domain "$NETBIRD_DOMAIN"; then
|
||||||
|
NETBIRD_DOMAIN=$(read_nb_domain)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$NETBIRD_DOMAIN" == "use-ip" ]; then
|
||||||
|
NETBIRD_DOMAIN=$(get_main_ip_address)
|
||||||
|
else
|
||||||
|
ZITADEL_EXTERNALSECURE="true"
|
||||||
|
ZITADEL_TLS_MODE="external"
|
||||||
|
NETBIRD_PORT=443
|
||||||
|
CADDY_SECURE_DOMAIN=", $NETBIRD_DOMAIN:$NETBIRD_PORT"
|
||||||
|
NETBIRD_HTTP_PROTOCOL="https"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||||
|
ZIDATE_TOKEN_EXPIRATION_DATE=$(date -u -v+30M "+%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
else
|
||||||
|
ZIDATE_TOKEN_EXPIRATION_DATE=$(date -u -d "+30 minutes" "+%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
fi
|
||||||
|
|
||||||
|
check_jq
|
||||||
|
|
||||||
|
DOCKER_COMPOSE_COMMAND=$(check_docker_compose)
|
||||||
|
|
||||||
|
if [ -f zitadel.env ]; then
|
||||||
|
echo "Generated files already exist, if you want to reinitialize the environment, please remove them first."
|
||||||
|
echo "You can use the following commands:"
|
||||||
|
echo " $DOCKER_COMPOSE_COMMAND down --volumes # to remove all containers and volumes"
|
||||||
|
echo " rm -f docker-compose.yml Caddyfile zitadel.env dashboard.env machinekey/zitadel-admin-sa.token turnserver.conf management.json"
|
||||||
|
echo "Be aware that this will remove all data from the database, and you will have to reconfigure the dashboard."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo Rendering initial files...
|
||||||
|
renderDockerCompose > docker-compose.yml
|
||||||
|
renderCaddyfile > Caddyfile
|
||||||
|
renderZitadelEnv > zitadel.env
|
||||||
|
echo "" > dashboard.env
|
||||||
|
echo "" > turnserver.conf
|
||||||
|
echo "" > management.json
|
||||||
|
|
||||||
|
mkdir -p machinekey
|
||||||
|
chmod 777 machinekey
|
||||||
|
|
||||||
|
init_crdb
|
||||||
|
|
||||||
|
echo -e "\nStarting Zidatel IDP for user management\n\n"
|
||||||
|
$DOCKER_COMPOSE_COMMAND up -d caddy zitadel
|
||||||
|
init_zitadel
|
||||||
|
|
||||||
|
echo -e "\nRendering NetBird files...\n"
|
||||||
|
renderTurnServerConf > turnserver.conf
|
||||||
|
renderManagementJson > management.json
|
||||||
|
renderDashboardEnv > dashboard.env
|
||||||
|
|
||||||
|
echo -e "\nStarting NetBird services\n"
|
||||||
|
$DOCKER_COMPOSE_COMMAND up -d
|
||||||
|
echo -e "\nDone!\n"
|
||||||
|
echo "You can access the NetBird dashboard at $NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN:$NETBIRD_PORT"
|
||||||
|
echo "Login with the following credentials:"
|
||||||
|
echo "Username: $ZITADEL_ADMIN_USERNAME" | tee .env
|
||||||
|
echo "Password: $ZITADEL_ADMIN_PASSWORD" | tee -a .env
|
||||||
|
}
|
||||||
|
|
||||||
|
renderCaddyfile() {
|
||||||
|
cat <<EOF
|
||||||
|
{
|
||||||
|
debug
|
||||||
|
servers :80,:443 {
|
||||||
|
protocols h1 h2c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:80${CADDY_SECURE_DOMAIN} {
|
||||||
|
# Signal
|
||||||
|
reverse_proxy /signalexchange.SignalExchange/* h2c://signal:10000
|
||||||
|
# Management
|
||||||
|
reverse_proxy /api/* management:80
|
||||||
|
reverse_proxy /management.ManagementService/* h2c://management:80
|
||||||
|
# Zitadel
|
||||||
|
reverse_proxy /zitadel.admin.v1.AdminService/* h2c://zitadel:8080
|
||||||
|
reverse_proxy /admin/v1/* h2c://zitadel:8080
|
||||||
|
reverse_proxy /zitadel.auth.v1.AuthService/* h2c://zitadel:8080
|
||||||
|
reverse_proxy /auth/v1/* h2c://zitadel:8080
|
||||||
|
reverse_proxy /zitadel.management.v1.ManagementService/* h2c://zitadel:8080
|
||||||
|
reverse_proxy /management/v1/* h2c://zitadel:8080
|
||||||
|
reverse_proxy /zitadel.system.v1.SystemService/* h2c://zitadel:8080
|
||||||
|
reverse_proxy /system/v1/* h2c://zitadel:8080
|
||||||
|
reverse_proxy /assets/v1/* h2c://zitadel:8080
|
||||||
|
reverse_proxy /ui/* h2c://zitadel:8080
|
||||||
|
reverse_proxy /oidc/v1/* h2c://zitadel:8080
|
||||||
|
reverse_proxy /saml/v2/* h2c://zitadel:8080
|
||||||
|
reverse_proxy /oauth/v2/* h2c://zitadel:8080
|
||||||
|
reverse_proxy /.well-known/openid-configuration h2c://zitadel:8080
|
||||||
|
reverse_proxy /openapi/* h2c://zitadel:8080
|
||||||
|
reverse_proxy /debug/* h2c://zitadel:8080
|
||||||
|
# Dashboard
|
||||||
|
reverse_proxy /* dashboard:80
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
renderTurnServerConf() {
|
||||||
|
cat <<EOF
|
||||||
|
listening-port=3478
|
||||||
|
tls-listening-port=5349
|
||||||
|
min-port=$TURN_MIN_PORT
|
||||||
|
max-port=$TURN_MAX_PORT
|
||||||
|
fingerprint
|
||||||
|
lt-cred-mech
|
||||||
|
user=$TURN_USER:$TURN_PASSWORD
|
||||||
|
realm=wiretrustee.com
|
||||||
|
cert=/etc/coturn/certs/cert.pem
|
||||||
|
pkey=/etc/coturn/private/privkey.pem
|
||||||
|
log-file=stdout
|
||||||
|
no-software-attribute
|
||||||
|
pidfile="/var/tmp/turnserver.pid"
|
||||||
|
no-cli
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
renderManagementJson() {
|
||||||
|
cat <<EOF
|
||||||
|
{
|
||||||
|
"Stuns": [
|
||||||
|
{
|
||||||
|
"Proto": "udp",
|
||||||
|
"URI": "stun:$NETBIRD_DOMAIN:3478"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"TURNConfig": {
|
||||||
|
"Turns": [
|
||||||
|
{
|
||||||
|
"Proto": "udp",
|
||||||
|
"URI": "turn:$NETBIRD_DOMAIN:3478",
|
||||||
|
"Username": "$TURN_USER",
|
||||||
|
"Password": "$TURN_PASSWORD"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"TimeBasedCredentials": false
|
||||||
|
},
|
||||||
|
"Signal": {
|
||||||
|
"Proto": "$NETBIRD_HTTP_PROTOCOL",
|
||||||
|
"URI": "$NETBIRD_DOMAIN:$NETBIRD_PORT"
|
||||||
|
},
|
||||||
|
"HttpConfig": {
|
||||||
|
"AuthIssuer": "$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN",
|
||||||
|
"AuthAudience": "$NETBIRD_AUTH_CLIENT_ID",
|
||||||
|
"OIDCConfigEndpoint":"$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN/.well-known/openid-configuration"
|
||||||
|
},
|
||||||
|
"IdpManagerConfig": {
|
||||||
|
"ManagerType": "zitadel",
|
||||||
|
"ClientConfig": {
|
||||||
|
"Issuer": "$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN:$NETBIRD_PORT",
|
||||||
|
"TokenEndpoint": "$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN:$NETBIRD_PORT/oauth/v2/token",
|
||||||
|
"ClientID": "$NETBIRD_IDP_MGMT_CLIENT_ID",
|
||||||
|
"ClientSecret": "$NETBIRD_IDP_MGMT_CLIENT_SECRET",
|
||||||
|
"GrantType": "client_credentials"
|
||||||
|
},
|
||||||
|
"ExtraConfig": {
|
||||||
|
"ManagementEndpoint": "$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN:$NETBIRD_PORT/management/v1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"PKCEAuthorizationFlow": {
|
||||||
|
"ProviderConfig": {
|
||||||
|
"Audience": "$NETBIRD_AUTH_CLIENT_ID_CLI",
|
||||||
|
"ClientID": "$NETBIRD_AUTH_CLIENT_ID_CLI",
|
||||||
|
"Scope": "openid profile email offline_access",
|
||||||
|
"RedirectURLs": ["http://localhost:53000/","http://localhost:54000/"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
renderDashboardEnv() {
|
||||||
|
cat <<EOF
|
||||||
|
# Endpoints
|
||||||
|
NETBIRD_MGMT_API_ENDPOINT=$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN:$NETBIRD_PORT
|
||||||
|
NETBIRD_MGMT_GRPC_API_ENDPOINT=$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN:$NETBIRD_PORT
|
||||||
|
# OIDC
|
||||||
|
AUTH_AUDIENCE=$NETBIRD_AUTH_CLIENT_ID
|
||||||
|
AUTH_CLIENT_ID=$NETBIRD_AUTH_CLIENT_ID
|
||||||
|
AUTH_AUTHORITY=$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN:$NETBIRD_PORT
|
||||||
|
USE_AUTH0=false
|
||||||
|
AUTH_SUPPORTED_SCOPES="openid profile email offline_access"
|
||||||
|
AUTH_REDIRECT_URI=/nb-auth
|
||||||
|
AUTH_SILENT_REDIRECT_URI=/nb-silent-auth
|
||||||
|
# SSL
|
||||||
|
NGINX_SSL_PORT=443
|
||||||
|
# Letsencrypt
|
||||||
|
LETSENCRYPT_DOMAIN=none
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
renderZitadelEnv() {
|
||||||
|
cat <<EOF
|
||||||
|
ZITADEL_LOG_LEVEL=debug
|
||||||
|
ZITADEL_MASTERKEY=$ZITADEL_MASTERKEY
|
||||||
|
ZITADEL_DATABASE_COCKROACH_HOST=crdb
|
||||||
|
ZITADEL_DATABASE_COCKROACH_USER_USERNAME=zitadel_user
|
||||||
|
ZITADEL_DATABASE_COCKROACH_USER_SSL_MODE=verify-full
|
||||||
|
ZITADEL_DATABASE_COCKROACH_USER_SSL_ROOTCERT="/crdb-certs/ca.crt"
|
||||||
|
ZITADEL_DATABASE_COCKROACH_USER_SSL_CERT="/crdb-certs/client.zitadel_user.crt"
|
||||||
|
ZITADEL_DATABASE_COCKROACH_USER_SSL_KEY="/crdb-certs/client.zitadel_user.key"
|
||||||
|
ZITADEL_DATABASE_COCKROACH_ADMIN_SSL_MODE=verify-full
|
||||||
|
ZITADEL_DATABASE_COCKROACH_ADMIN_SSL_ROOTCERT="/crdb-certs/ca.crt"
|
||||||
|
ZITADEL_DATABASE_COCKROACH_ADMIN_SSL_CERT="/crdb-certs/client.root.crt"
|
||||||
|
ZITADEL_DATABASE_COCKROACH_ADMIN_SSL_KEY="/crdb-certs/client.root.key"
|
||||||
|
ZITADEL_EXTERNALSECURE=$ZITADEL_EXTERNALSECURE
|
||||||
|
ZITADEL_TLS_ENABLED="false"
|
||||||
|
ZITADEL_EXTERNALPORT=$NETBIRD_PORT
|
||||||
|
ZITADEL_EXTERNALDOMAIN=$NETBIRD_DOMAIN
|
||||||
|
ZITADEL_FIRSTINSTANCE_PATPATH=/machinekey/zitadel-admin-sa.token
|
||||||
|
ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_USERNAME=zitadel-admin-sa
|
||||||
|
ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_NAME=Admin
|
||||||
|
ZITADEL_FIRSTINSTANCE_ORG_MACHINE_PAT_SCOPES=openid
|
||||||
|
ZITADEL_FIRSTINSTANCE_ORG_MACHINE_PAT_EXPIRATIONDATE=$ZIDATE_TOKEN_EXPIRATION_DATE
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
renderDockerCompose() {
|
||||||
|
cat <<EOF
|
||||||
|
version: "3.4"
|
||||||
|
services:
|
||||||
|
# Caddy reverse proxy
|
||||||
|
caddy:
|
||||||
|
image: caddy
|
||||||
|
restart: unless-stopped
|
||||||
|
networks: [ netbird ]
|
||||||
|
ports:
|
||||||
|
- '443:443'
|
||||||
|
- '80:80'
|
||||||
|
- '8080:8080'
|
||||||
|
volumes:
|
||||||
|
- netbird_caddy_data:/data
|
||||||
|
- ./Caddyfile:/etc/caddy/Caddyfile
|
||||||
|
#UI dashboard
|
||||||
|
dashboard:
|
||||||
|
image: wiretrustee/dashboard:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
networks: [netbird]
|
||||||
|
env_file:
|
||||||
|
- ./dashboard.env
|
||||||
|
# Signal
|
||||||
|
signal:
|
||||||
|
image: netbirdio/signal:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
networks: [netbird]
|
||||||
|
# Management
|
||||||
|
management:
|
||||||
|
image: netbirdio/management:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
networks: [netbird]
|
||||||
|
volumes:
|
||||||
|
- netbird_management:/var/lib/netbird
|
||||||
|
- ./management.json:/etc/netbird/management.json
|
||||||
|
command: [
|
||||||
|
"--port", "80",
|
||||||
|
"--log-file", "console",
|
||||||
|
"--log-level", "info",
|
||||||
|
"--disable-anonymous-metrics=false",
|
||||||
|
"--single-account-mode-domain=netbird.selfhosted",
|
||||||
|
"--dns-domain=netbird.selfhosted",
|
||||||
|
"--idp-sign-key-refresh-enabled",
|
||||||
|
]
|
||||||
|
# Coturn, AKA relay server
|
||||||
|
coturn:
|
||||||
|
image: coturn/coturn
|
||||||
|
restart: unless-stopped
|
||||||
|
domainname: netbird.relay.selfhosted
|
||||||
|
volumes:
|
||||||
|
- ./turnserver.conf:/etc/turnserver.conf:ro
|
||||||
|
network_mode: host
|
||||||
|
command:
|
||||||
|
- -c /etc/turnserver.conf
|
||||||
|
# Zitadel - identity provider
|
||||||
|
zitadel:
|
||||||
|
restart: 'always'
|
||||||
|
networks: [netbird]
|
||||||
|
image: 'ghcr.io/zitadel/zitadel:v2.31.3'
|
||||||
|
command: 'start-from-init --masterkeyFromEnv --tlsMode $ZITADEL_TLS_MODE'
|
||||||
|
env_file:
|
||||||
|
- ./zitadel.env
|
||||||
|
depends_on:
|
||||||
|
crdb:
|
||||||
|
condition: 'service_healthy'
|
||||||
|
volumes:
|
||||||
|
- ./machinekey:/machinekey
|
||||||
|
- netbird_zitadel_certs:/crdb-certs:ro
|
||||||
|
# CockroachDB for zitadel
|
||||||
|
crdb:
|
||||||
|
restart: 'always'
|
||||||
|
networks: [netbird]
|
||||||
|
image: 'cockroachdb/cockroach:v22.2.2'
|
||||||
|
command: 'start-single-node --advertise-addr crdb'
|
||||||
|
volumes:
|
||||||
|
- netbird_crdb_data:/cockroach/cockroach-data
|
||||||
|
- netbird_crdb_certs:/cockroach/certs
|
||||||
|
- netbird_zitadel_certs:/zitadel-certs
|
||||||
|
healthcheck:
|
||||||
|
test: [ "CMD", "curl", "-f", "http://localhost:8080/health?ready=1" ]
|
||||||
|
interval: '10s'
|
||||||
|
timeout: '30s'
|
||||||
|
retries: 5
|
||||||
|
start_period: '20s'
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
netbird_management:
|
||||||
|
netbird_caddy_data:
|
||||||
|
netbird_crdb_data:
|
||||||
|
netbird_crdb_certs:
|
||||||
|
netbird_zitadel_certs:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
netbird:
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
initEnvironment
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
request_jwt_token() {
|
|
||||||
INSTANCE_URL=$1
|
|
||||||
BODY="grant_type=client_credentials&scope=urn:zitadel:iam:org:project:id:zitadel:aud&client_id=$ZITADEL_CLIENT_ID&client_secret=$ZITADEL_CLIENT_SECRET"
|
|
||||||
|
|
||||||
RESPONSE=$(
|
|
||||||
curl -X POST "$INSTANCE_URL/oauth/v2/token" \
|
|
||||||
-H "Content-Type: application/x-www-form-urlencoded" \
|
|
||||||
-d "$BODY"
|
|
||||||
)
|
|
||||||
echo "$RESPONSE" | jq -r '.access_token'
|
|
||||||
}
|
|
||||||
|
|
||||||
create_new_project() {
|
|
||||||
INSTANCE_URL=$1
|
|
||||||
ACCESS_TOKEN=$2
|
|
||||||
PROJECT_NAME="NETBIRD"
|
|
||||||
|
|
||||||
RESPONSE=$(
|
|
||||||
curl -X POST "$INSTANCE_URL/management/v1/projects" \
|
|
||||||
-H "Authorization: Bearer $ACCESS_TOKEN" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"name": "'"$PROJECT_NAME"'"}'
|
|
||||||
)
|
|
||||||
echo "$RESPONSE" | jq -r '.id'
|
|
||||||
}
|
|
||||||
|
|
||||||
create_new_application() {
|
|
||||||
INSTANCE_URL=$1
|
|
||||||
ACCESS_TOKEN=$2
|
|
||||||
APPLICATION_NAME="netbird"
|
|
||||||
|
|
||||||
RESPONSE=$(
|
|
||||||
curl -X POST "$INSTANCE_URL/management/v1/projects/$PROJECT_ID/apps/oidc" \
|
|
||||||
-H "Authorization: Bearer $ACCESS_TOKEN" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{
|
|
||||||
"name": "'"$APPLICATION_NAME"'",
|
|
||||||
"redirectUris": [
|
|
||||||
"'"$BASE_REDIRECT_URL"'/auth"
|
|
||||||
],
|
|
||||||
"RESPONSETypes": [
|
|
||||||
"OIDC_RESPONSE_TYPE_CODE"
|
|
||||||
],
|
|
||||||
"grantTypes": [
|
|
||||||
"OIDC_GRANT_TYPE_AUTHORIZATION_CODE",
|
|
||||||
"OIDC_GRANT_TYPE_REFRESH_TOKEN"
|
|
||||||
],
|
|
||||||
"appType": "OIDC_APP_TYPE_USER_AGENT",
|
|
||||||
"authMethodType": "OIDC_AUTH_METHOD_TYPE_NONE",
|
|
||||||
"postLogoutRedirectUris": [
|
|
||||||
"'"$BASE_REDIRECT_URL"'/silent-auth"
|
|
||||||
],
|
|
||||||
"version": "OIDC_VERSION_1_0",
|
|
||||||
"devMode": '"$ZITADEL_DEV_MODE"',
|
|
||||||
"accessTokenType": "OIDC_TOKEN_TYPE_JWT",
|
|
||||||
"accessTokenRoleAssertion": true,
|
|
||||||
"skipNativeAppSuccessPage": true
|
|
||||||
}'
|
|
||||||
)
|
|
||||||
echo "$RESPONSE" | jq -r '.clientId'
|
|
||||||
}
|
|
||||||
|
|
||||||
configure_zitadel_instance() {
|
|
||||||
# extract zitadel instance url
|
|
||||||
INSTANCE_URL=$(echo "$NETBIRD_AUTH_OIDC_CONFIGURATION_ENDPOINT" | sed 's/\/\.well-known\/openid-configuration//')
|
|
||||||
DOC_URL="https://netbird.io/docs/integrations/identity-providers/self-hosted/using-netbird-with-zitadel#step-4-create-a-service-user"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
printf "configuring zitadel instance: $INSTANCE_URL \n \
|
|
||||||
before proceeding, please create a new service account for authorization by following the instructions (step 4 and 5
|
|
||||||
) in the documentation at %s\n" "$DOC_URL"
|
|
||||||
echo "Please ensure that the new service account has 'Org Owner' permission in order for this to work."
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
read -n 1 -s -r -p "press any key to continue..."
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# prompt the user to enter service account clientID
|
|
||||||
echo ""
|
|
||||||
read -r -p "enter service account ClientId: " ZITADEL_CLIENT_ID
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Prompt the user to enter service account clientSecret
|
|
||||||
read -r -p "enter service account ClientSecret: " ZITADEL_CLIENT_SECRET
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# get an access token from zitadel
|
|
||||||
echo "retrieving access token from zitadel"
|
|
||||||
ACCESS_TOKEN=$(request_jwt_token "$INSTANCE_URL")
|
|
||||||
if [ "$ACCESS_TOKEN" = "null" ]; then
|
|
||||||
echo "failed requesting access token"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# create the zitadel project
|
|
||||||
echo "creating new zitadel project"
|
|
||||||
PROJECT_ID=$(create_new_project "$INSTANCE_URL" "$ACCESS_TOKEN")
|
|
||||||
if [ "$PROJECT_ID" = "null" ]; then
|
|
||||||
echo "failed creating new zitadel project"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
ZITADEL_DEV_MODE=false
|
|
||||||
if [[ $NETBIRD_DOMAIN == *"localhost"* ]]; then
|
|
||||||
BASE_REDIRECT_URL="http://$NETBIRD_DOMAIN"
|
|
||||||
ZITADEL_DEV_MODE=true
|
|
||||||
else
|
|
||||||
BASE_REDIRECT_URL="https://$NETBIRD_DOMAIN"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# create zitadel spa application
|
|
||||||
echo "creating new zitadel spa application"
|
|
||||||
APPLICATION_CLIENT_ID=$(create_new_application "$INSTANCE_URL" "$ACCESS_TOKEN")
|
|
||||||
if [ "$APPLICATION_CLIENT_ID" = "null" ]; then
|
|
||||||
echo "failed creating new zitadel spa application"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
@@ -371,24 +371,24 @@ func handlerFunc(gRPCHandler *grpc.Server, httpHandler http.Handler) http.Handle
|
|||||||
}
|
}
|
||||||
|
|
||||||
func loadMgmtConfig(mgmtConfigPath string) (*server.Config, error) {
|
func loadMgmtConfig(mgmtConfigPath string) (*server.Config, error) {
|
||||||
config := &server.Config{}
|
loadedConfig := &server.Config{}
|
||||||
_, err := util.ReadJson(mgmtConfigPath, config)
|
_, err := util.ReadJson(mgmtConfigPath, loadedConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if mgmtLetsencryptDomain != "" {
|
if mgmtLetsencryptDomain != "" {
|
||||||
config.HttpConfig.LetsEncryptDomain = mgmtLetsencryptDomain
|
loadedConfig.HttpConfig.LetsEncryptDomain = mgmtLetsencryptDomain
|
||||||
}
|
}
|
||||||
if mgmtDataDir != "" {
|
if mgmtDataDir != "" {
|
||||||
config.Datadir = mgmtDataDir
|
loadedConfig.Datadir = mgmtDataDir
|
||||||
}
|
}
|
||||||
|
|
||||||
if certKey != "" && certFile != "" {
|
if certKey != "" && certFile != "" {
|
||||||
config.HttpConfig.CertFile = certFile
|
loadedConfig.HttpConfig.CertFile = certFile
|
||||||
config.HttpConfig.CertKey = certKey
|
loadedConfig.HttpConfig.CertKey = certKey
|
||||||
}
|
}
|
||||||
|
|
||||||
oidcEndpoint := config.HttpConfig.OIDCConfigEndpoint
|
oidcEndpoint := loadedConfig.HttpConfig.OIDCConfigEndpoint
|
||||||
if oidcEndpoint != "" {
|
if oidcEndpoint != "" {
|
||||||
// if OIDCConfigEndpoint is specified, we can load DeviceAuthEndpoint and TokenEndpoint automatically
|
// if OIDCConfigEndpoint is specified, we can load DeviceAuthEndpoint and TokenEndpoint automatically
|
||||||
log.Infof("loading OIDC configuration from the provided IDP configuration endpoint %s", oidcEndpoint)
|
log.Infof("loading OIDC configuration from the provided IDP configuration endpoint %s", oidcEndpoint)
|
||||||
@@ -399,45 +399,45 @@ func loadMgmtConfig(mgmtConfigPath string) (*server.Config, error) {
|
|||||||
log.Infof("loaded OIDC configuration from the provided IDP configuration endpoint: %s", oidcEndpoint)
|
log.Infof("loaded OIDC configuration from the provided IDP configuration endpoint: %s", oidcEndpoint)
|
||||||
|
|
||||||
log.Infof("overriding HttpConfig.AuthIssuer with a new value %s, previously configured value: %s",
|
log.Infof("overriding HttpConfig.AuthIssuer with a new value %s, previously configured value: %s",
|
||||||
oidcConfig.Issuer, config.HttpConfig.AuthIssuer)
|
oidcConfig.Issuer, loadedConfig.HttpConfig.AuthIssuer)
|
||||||
config.HttpConfig.AuthIssuer = oidcConfig.Issuer
|
loadedConfig.HttpConfig.AuthIssuer = oidcConfig.Issuer
|
||||||
|
|
||||||
log.Infof("overriding HttpConfig.AuthKeysLocation (JWT certs) with a new value %s, previously configured value: %s",
|
log.Infof("overriding HttpConfig.AuthKeysLocation (JWT certs) with a new value %s, previously configured value: %s",
|
||||||
oidcConfig.JwksURI, config.HttpConfig.AuthKeysLocation)
|
oidcConfig.JwksURI, loadedConfig.HttpConfig.AuthKeysLocation)
|
||||||
config.HttpConfig.AuthKeysLocation = oidcConfig.JwksURI
|
loadedConfig.HttpConfig.AuthKeysLocation = oidcConfig.JwksURI
|
||||||
|
|
||||||
if !(config.DeviceAuthorizationFlow == nil || strings.ToLower(config.DeviceAuthorizationFlow.Provider) == string(server.NONE)) {
|
if !(loadedConfig.DeviceAuthorizationFlow == nil || strings.ToLower(loadedConfig.DeviceAuthorizationFlow.Provider) == string(server.NONE)) {
|
||||||
log.Infof("overriding DeviceAuthorizationFlow.TokenEndpoint with a new value: %s, previously configured value: %s",
|
log.Infof("overriding DeviceAuthorizationFlow.TokenEndpoint with a new value: %s, previously configured value: %s",
|
||||||
oidcConfig.TokenEndpoint, config.DeviceAuthorizationFlow.ProviderConfig.TokenEndpoint)
|
oidcConfig.TokenEndpoint, loadedConfig.DeviceAuthorizationFlow.ProviderConfig.TokenEndpoint)
|
||||||
config.DeviceAuthorizationFlow.ProviderConfig.TokenEndpoint = oidcConfig.TokenEndpoint
|
loadedConfig.DeviceAuthorizationFlow.ProviderConfig.TokenEndpoint = oidcConfig.TokenEndpoint
|
||||||
log.Infof("overriding DeviceAuthorizationFlow.DeviceAuthEndpoint with a new value: %s, previously configured value: %s",
|
log.Infof("overriding DeviceAuthorizationFlow.DeviceAuthEndpoint with a new value: %s, previously configured value: %s",
|
||||||
oidcConfig.DeviceAuthEndpoint, config.DeviceAuthorizationFlow.ProviderConfig.DeviceAuthEndpoint)
|
oidcConfig.DeviceAuthEndpoint, loadedConfig.DeviceAuthorizationFlow.ProviderConfig.DeviceAuthEndpoint)
|
||||||
config.DeviceAuthorizationFlow.ProviderConfig.DeviceAuthEndpoint = oidcConfig.DeviceAuthEndpoint
|
loadedConfig.DeviceAuthorizationFlow.ProviderConfig.DeviceAuthEndpoint = oidcConfig.DeviceAuthEndpoint
|
||||||
|
|
||||||
u, err := url.Parse(oidcEndpoint)
|
u, err := url.Parse(oidcEndpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
log.Infof("overriding DeviceAuthorizationFlow.ProviderConfig.Domain with a new value: %s, previously configured value: %s",
|
log.Infof("overriding DeviceAuthorizationFlow.ProviderConfig.Domain with a new value: %s, previously configured value: %s",
|
||||||
u.Host, config.DeviceAuthorizationFlow.ProviderConfig.Domain)
|
u.Host, loadedConfig.DeviceAuthorizationFlow.ProviderConfig.Domain)
|
||||||
config.DeviceAuthorizationFlow.ProviderConfig.Domain = u.Host
|
loadedConfig.DeviceAuthorizationFlow.ProviderConfig.Domain = u.Host
|
||||||
|
|
||||||
if config.DeviceAuthorizationFlow.ProviderConfig.Scope == "" {
|
if loadedConfig.DeviceAuthorizationFlow.ProviderConfig.Scope == "" {
|
||||||
config.DeviceAuthorizationFlow.ProviderConfig.Scope = server.DefaultDeviceAuthFlowScope
|
loadedConfig.DeviceAuthorizationFlow.ProviderConfig.Scope = server.DefaultDeviceAuthFlowScope
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.PKCEAuthorizationFlow != nil {
|
if loadedConfig.PKCEAuthorizationFlow != nil {
|
||||||
log.Infof("overriding PKCEAuthorizationFlow.TokenEndpoint with a new value: %s, previously configured value: %s",
|
log.Infof("overriding PKCEAuthorizationFlow.TokenEndpoint with a new value: %s, previously configured value: %s",
|
||||||
oidcConfig.TokenEndpoint, config.PKCEAuthorizationFlow.ProviderConfig.TokenEndpoint)
|
oidcConfig.TokenEndpoint, loadedConfig.PKCEAuthorizationFlow.ProviderConfig.TokenEndpoint)
|
||||||
config.DeviceAuthorizationFlow.ProviderConfig.TokenEndpoint = oidcConfig.TokenEndpoint
|
loadedConfig.PKCEAuthorizationFlow.ProviderConfig.TokenEndpoint = oidcConfig.TokenEndpoint
|
||||||
log.Infof("overriding PKCEAuthorizationFlow.AuthorizationEndpoint with a new value: %s, previously configured value: %s",
|
log.Infof("overriding PKCEAuthorizationFlow.AuthorizationEndpoint with a new value: %s, previously configured value: %s",
|
||||||
oidcConfig.AuthorizationEndpoint, config.PKCEAuthorizationFlow.ProviderConfig.AuthorizationEndpoint)
|
oidcConfig.AuthorizationEndpoint, loadedConfig.PKCEAuthorizationFlow.ProviderConfig.AuthorizationEndpoint)
|
||||||
config.PKCEAuthorizationFlow.ProviderConfig.AuthorizationEndpoint = oidcConfig.AuthorizationEndpoint
|
loadedConfig.PKCEAuthorizationFlow.ProviderConfig.AuthorizationEndpoint = oidcConfig.AuthorizationEndpoint
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return config, err
|
return loadedConfig, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// OIDCConfigResponse used for parsing OIDC config response
|
// OIDCConfigResponse used for parsing OIDC config response
|
||||||
|
|||||||
@@ -139,10 +139,14 @@ type DefaultAccountManager struct {
|
|||||||
type Settings struct {
|
type Settings struct {
|
||||||
// PeerLoginExpirationEnabled globally enables or disables peer login expiration
|
// PeerLoginExpirationEnabled globally enables or disables peer login expiration
|
||||||
PeerLoginExpirationEnabled bool
|
PeerLoginExpirationEnabled bool
|
||||||
|
|
||||||
// PeerLoginExpiration is a setting that indicates when peer login expires.
|
// PeerLoginExpiration is a setting that indicates when peer login expires.
|
||||||
// Applies to all peers that have Peer.LoginExpirationEnabled set to true.
|
// Applies to all peers that have Peer.LoginExpirationEnabled set to true.
|
||||||
PeerLoginExpiration time.Duration
|
PeerLoginExpiration time.Duration
|
||||||
|
|
||||||
|
// GroupsPropagationEnabled allows to propagate auto groups from the user to the peer
|
||||||
|
GroupsPropagationEnabled bool
|
||||||
|
|
||||||
// JWTGroupsEnabled allows extract groups from JWT claim, which name defined in the JWTGroupsClaimName
|
// JWTGroupsEnabled allows extract groups from JWT claim, which name defined in the JWTGroupsClaimName
|
||||||
// and add it to account groups.
|
// and add it to account groups.
|
||||||
JWTGroupsEnabled bool
|
JWTGroupsEnabled bool
|
||||||
@@ -158,6 +162,7 @@ func (s *Settings) Copy() *Settings {
|
|||||||
PeerLoginExpiration: s.PeerLoginExpiration,
|
PeerLoginExpiration: s.PeerLoginExpiration,
|
||||||
JWTGroupsEnabled: s.JWTGroupsEnabled,
|
JWTGroupsEnabled: s.JWTGroupsEnabled,
|
||||||
JWTGroupsClaimName: s.JWTGroupsClaimName,
|
JWTGroupsClaimName: s.JWTGroupsClaimName,
|
||||||
|
GroupsPropagationEnabled: s.GroupsPropagationEnabled,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,6 +197,7 @@ type UserInfo struct {
|
|||||||
Status string `json:"-"`
|
Status string `json:"-"`
|
||||||
IsServiceUser bool `json:"is_service_user"`
|
IsServiceUser bool `json:"is_service_user"`
|
||||||
IsBlocked bool `json:"is_blocked"`
|
IsBlocked bool `json:"is_blocked"`
|
||||||
|
LastLogin time.Time `json:"last_login"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// getRoutesToSync returns the enabled routes for the peer ID and the routes
|
// getRoutesToSync returns the enabled routes for the peer ID and the routes
|
||||||
@@ -624,26 +630,110 @@ func (a *Account) GetPeer(peerID string) *Peer {
|
|||||||
return a.Peers[peerID]
|
return a.Peers[peerID]
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddJWTGroups to existed groups if they does not exists
|
// SetJWTGroups to account and to user autoassigned groups
|
||||||
func (a *Account) AddJWTGroups(groups []string) (int, error) {
|
func (a *Account) SetJWTGroups(userID string, groupsNames []string) bool {
|
||||||
existedGroups := make(map[string]*Group)
|
user, ok := a.Users[userID]
|
||||||
for _, g := range a.Groups {
|
if !ok {
|
||||||
existedGroups[g.Name] = g
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
var count int
|
existedGroupsByName := make(map[string]*Group)
|
||||||
for _, name := range groups {
|
for _, group := range a.Groups {
|
||||||
if _, ok := existedGroups[name]; !ok {
|
existedGroupsByName[group.Name] = group
|
||||||
id := xid.New().String()
|
}
|
||||||
a.Groups[id] = &Group{
|
|
||||||
ID: id,
|
// remove JWT groups from the autogroups, to sync them again
|
||||||
|
removed := 0
|
||||||
|
jwtAutoGroups := make(map[string]struct{})
|
||||||
|
for i, id := range user.AutoGroups {
|
||||||
|
if group, ok := a.Groups[id]; ok && group.Issued == GroupIssuedJWT {
|
||||||
|
jwtAutoGroups[group.Name] = struct{}{}
|
||||||
|
user.AutoGroups = append(user.AutoGroups[:i-removed], user.AutoGroups[i-removed+1:]...)
|
||||||
|
removed++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create JWT groups if they doesn't exist
|
||||||
|
// and all of them to the autogroups
|
||||||
|
var modified bool
|
||||||
|
for _, name := range groupsNames {
|
||||||
|
group, ok := existedGroupsByName[name]
|
||||||
|
if !ok {
|
||||||
|
group = &Group{
|
||||||
|
ID: xid.New().String(),
|
||||||
Name: name,
|
Name: name,
|
||||||
Issued: GroupIssuedJWT,
|
Issued: GroupIssuedJWT,
|
||||||
}
|
}
|
||||||
count++
|
a.Groups[group.ID] = group
|
||||||
|
}
|
||||||
|
// only JWT groups will be synced
|
||||||
|
if group.Issued == GroupIssuedJWT {
|
||||||
|
user.AutoGroups = append(user.AutoGroups, group.ID)
|
||||||
|
if _, ok := jwtAutoGroups[name]; !ok {
|
||||||
|
modified = true
|
||||||
|
}
|
||||||
|
delete(jwtAutoGroups, name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return count, nil
|
|
||||||
|
// if not empty it means we removed some groups
|
||||||
|
if len(jwtAutoGroups) > 0 {
|
||||||
|
modified = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return modified
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserGroupsAddToPeers adds groups to all peers of user
|
||||||
|
func (a *Account) UserGroupsAddToPeers(userID string, groups ...string) {
|
||||||
|
userPeers := make(map[string]struct{})
|
||||||
|
for pid, peer := range a.Peers {
|
||||||
|
if peer.UserID == userID {
|
||||||
|
userPeers[pid] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, gid := range groups {
|
||||||
|
group, ok := a.Groups[gid]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
groupPeers := make(map[string]struct{})
|
||||||
|
for _, pid := range group.Peers {
|
||||||
|
groupPeers[pid] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
for pid := range userPeers {
|
||||||
|
groupPeers[pid] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
group.Peers = group.Peers[:0]
|
||||||
|
for pid := range groupPeers {
|
||||||
|
group.Peers = append(group.Peers, pid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserGroupsRemoveFromPeers removes groups from all peers of user
|
||||||
|
func (a *Account) UserGroupsRemoveFromPeers(userID string, groups ...string) {
|
||||||
|
for _, gid := range groups {
|
||||||
|
group, ok := a.Groups[gid]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
update := make([]string, 0, len(group.Peers))
|
||||||
|
for _, pid := range group.Peers {
|
||||||
|
peer, ok := a.Peers[pid]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if peer.UserID != userID {
|
||||||
|
update = append(update, pid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
group.Peers = update
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildManager creates a new DefaultAccountManager with a provided Store
|
// BuildManager creates a new DefaultAccountManager with a provided Store
|
||||||
@@ -797,6 +887,7 @@ func (am *DefaultAccountManager) peerLoginExpirationJob(accountID string) func()
|
|||||||
log.Errorf("failed saving peer status while expiring peer %s", peer.ID)
|
log.Errorf("failed saving peer status while expiring peer %s", peer.ID)
|
||||||
return account.GetNextPeerExpiration()
|
return account.GetNextPeerExpiration()
|
||||||
}
|
}
|
||||||
|
am.storeEvent(peer.UserID, peer.ID, account.Id, activity.PeerLoginExpired, peer.EventMeta(am.GetDNSDomain()))
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("discovered %d peers to expire for account %s", len(peerIDs), account.Id)
|
log.Debugf("discovered %d peers to expire for account %s", len(peerIDs), account.Id)
|
||||||
@@ -1282,21 +1373,58 @@ func (am *DefaultAccountManager) GetAccountFromToken(claims jwtclaims.Authorizat
|
|||||||
}
|
}
|
||||||
if claim, ok := claims.Raw[account.Settings.JWTGroupsClaimName]; ok {
|
if claim, ok := claims.Raw[account.Settings.JWTGroupsClaimName]; ok {
|
||||||
if slice, ok := claim.([]interface{}); ok {
|
if slice, ok := claim.([]interface{}); ok {
|
||||||
var groups []string
|
var groupsNames []string
|
||||||
for _, item := range slice {
|
for _, item := range slice {
|
||||||
if g, ok := item.(string); ok {
|
if g, ok := item.(string); ok {
|
||||||
groups = append(groups, g)
|
groupsNames = append(groupsNames, g)
|
||||||
} else {
|
} else {
|
||||||
log.Errorf("JWT claim %q is not a string: %v", account.Settings.JWTGroupsClaimName, item)
|
log.Errorf("JWT claim %q is not a string: %v", account.Settings.JWTGroupsClaimName, item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
n, err := account.AddJWTGroups(groups)
|
|
||||||
if err != nil {
|
oldGroups := make([]string, len(user.AutoGroups))
|
||||||
log.Errorf("failed to add JWT groups: %v", err)
|
copy(oldGroups, user.AutoGroups)
|
||||||
}
|
// if groups were added or modified, save the account
|
||||||
if n > 0 {
|
if account.SetJWTGroups(claims.UserId, groupsNames) {
|
||||||
|
if account.Settings.GroupsPropagationEnabled {
|
||||||
|
if user, err := account.FindUser(claims.UserId); err == nil {
|
||||||
|
addNewGroups := difference(user.AutoGroups, oldGroups)
|
||||||
|
removeOldGroups := difference(oldGroups, user.AutoGroups)
|
||||||
|
account.UserGroupsAddToPeers(claims.UserId, addNewGroups...)
|
||||||
|
account.UserGroupsRemoveFromPeers(claims.UserId, removeOldGroups...)
|
||||||
|
account.Network.IncSerial()
|
||||||
if err := am.Store.SaveAccount(account); err != nil {
|
if err := am.Store.SaveAccount(account); err != nil {
|
||||||
log.Errorf("failed to save account: %v", err)
|
log.Errorf("failed to save account: %v", err)
|
||||||
|
} else {
|
||||||
|
if err := am.updateAccountPeers(account); err != nil {
|
||||||
|
log.Errorf("failed updating account peers while updating user %s", account.Id)
|
||||||
|
}
|
||||||
|
for _, g := range addNewGroups {
|
||||||
|
if group := account.GetGroup(g); group != nil {
|
||||||
|
am.storeEvent(user.Id, user.Id, account.Id, activity.GroupAddedToUser,
|
||||||
|
map[string]any{
|
||||||
|
"group": group.Name,
|
||||||
|
"group_id": group.ID,
|
||||||
|
"is_service_user": user.IsServiceUser,
|
||||||
|
"user_name": user.ServiceUserName})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, g := range removeOldGroups {
|
||||||
|
if group := account.GetGroup(g); group != nil {
|
||||||
|
am.storeEvent(user.Id, user.Id, account.Id, activity.GroupRemovedFromUser,
|
||||||
|
map[string]any{
|
||||||
|
"group": group.Name,
|
||||||
|
"group_id": group.ID,
|
||||||
|
"is_service_user": user.IsServiceUser,
|
||||||
|
"user_name": user.ServiceUserName})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := am.Store.SaveAccount(account); err != nil {
|
||||||
|
log.Errorf("failed to save account: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -1343,7 +1471,7 @@ func (am *DefaultAccountManager) getAccountWithAuthorizationClaims(claims jwtcla
|
|||||||
if _, ok := accountFromID.Users[claims.UserId]; !ok {
|
if _, ok := accountFromID.Users[claims.UserId]; !ok {
|
||||||
return nil, fmt.Errorf("user %s is not part of the account id %s", claims.UserId, claims.AccountId)
|
return nil, fmt.Errorf("user %s is not part of the account id %s", claims.UserId, claims.AccountId)
|
||||||
}
|
}
|
||||||
if accountFromID.DomainCategory == PrivateCategory || claims.DomainCategory != PrivateCategory {
|
if accountFromID.DomainCategory == PrivateCategory || claims.DomainCategory != PrivateCategory || accountFromID.Domain != claims.Domain {
|
||||||
return accountFromID, nil
|
return accountFromID, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package server
|
|||||||
import (
|
import (
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
b64 "encoding/base64"
|
b64 "encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"reflect"
|
"reflect"
|
||||||
@@ -216,7 +217,6 @@ func TestAccount_GetPeerNetworkMap(t *testing.T) {
|
|||||||
assert.Len(t, networkMap.Peers, len(testCase.expectedPeers))
|
assert.Len(t, networkMap.Peers, len(testCase.expectedPeers))
|
||||||
assert.Len(t, networkMap.OfflinePeers, len(testCase.expectedOfflinePeers))
|
assert.Len(t, networkMap.OfflinePeers, len(testCase.expectedOfflinePeers))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewAccount(t *testing.T) {
|
func TestNewAccount(t *testing.T) {
|
||||||
@@ -1349,6 +1349,11 @@ func TestAccount_Copy(t *testing.T) {
|
|||||||
Peers: map[string]*Peer{
|
Peers: map[string]*Peer{
|
||||||
"peer1": {
|
"peer1": {
|
||||||
Key: "key1",
|
Key: "key1",
|
||||||
|
Status: &PeerStatus{
|
||||||
|
LastSeen: time.Now(),
|
||||||
|
Connected: true,
|
||||||
|
LoginExpired: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Users: map[string]*User{
|
Users: map[string]*User{
|
||||||
@@ -1372,27 +1377,35 @@ func TestAccount_Copy(t *testing.T) {
|
|||||||
Groups: map[string]*Group{
|
Groups: map[string]*Group{
|
||||||
"group1": {
|
"group1": {
|
||||||
ID: "group1",
|
ID: "group1",
|
||||||
|
Peers: []string{"peer1"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Rules: map[string]*Rule{
|
Rules: map[string]*Rule{
|
||||||
"rule1": {
|
"rule1": {
|
||||||
ID: "rule1",
|
ID: "rule1",
|
||||||
|
Destination: []string{},
|
||||||
|
Source: []string{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Policies: []*Policy{
|
Policies: []*Policy{
|
||||||
{
|
{
|
||||||
ID: "policy1",
|
ID: "policy1",
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
|
Rules: make([]*PolicyRule, 0),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Routes: map[string]*route.Route{
|
Routes: map[string]*route.Route{
|
||||||
"route1": {
|
"route1": {
|
||||||
ID: "route1",
|
ID: "route1",
|
||||||
|
Groups: []string{"group1"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
NameServerGroups: map[string]*nbdns.NameServerGroup{
|
NameServerGroups: map[string]*nbdns.NameServerGroup{
|
||||||
"nsGroup1": {
|
"nsGroup1": {
|
||||||
ID: "nsGroup1",
|
ID: "nsGroup1",
|
||||||
|
Domains: []string{},
|
||||||
|
Groups: []string{},
|
||||||
|
NameServers: []nbdns.NameServer{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
DNSSettings: &DNSSettings{DisabledManagementGroups: []string{}},
|
DNSSettings: &DNSSettings{DisabledManagementGroups: []string{}},
|
||||||
@@ -1403,10 +1416,20 @@ func TestAccount_Copy(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
accountCopy := account.Copy()
|
accountCopy := account.Copy()
|
||||||
assert.Equal(t, account, accountCopy, "account copy returned a different value than expected")
|
accBytes, err := json.Marshal(account)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
account.Peers["peer1"].Status.Connected = false // we change original object to confirm that copy wont change
|
||||||
|
accCopyBytes, err := json.Marshal(accountCopy)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
assert.Equal(t, string(accBytes), string(accCopyBytes), "account copy returned a different value than expected")
|
||||||
}
|
}
|
||||||
|
|
||||||
// hasNilField validates pointers, maps and slices if they are nil
|
// hasNilField validates pointers, maps and slices if they are nil
|
||||||
|
// TODO: make it check nested fields too
|
||||||
func hasNilField(x interface{}) error {
|
func hasNilField(x interface{}) error {
|
||||||
rv := reflect.ValueOf(x)
|
rv := reflect.ValueOf(x)
|
||||||
rv = rv.Elem()
|
rv = rv.Elem()
|
||||||
@@ -1931,6 +1954,120 @@ func TestAccount_GetNextPeerExpiration(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAccount_SetJWTGroups(t *testing.T) {
|
||||||
|
// create a new account
|
||||||
|
account := &Account{
|
||||||
|
Peers: map[string]*Peer{
|
||||||
|
"peer1": {ID: "peer1", Key: "key1", UserID: "user1"},
|
||||||
|
"peer2": {ID: "peer2", Key: "key2", UserID: "user1"},
|
||||||
|
"peer3": {ID: "peer3", Key: "key3", UserID: "user1"},
|
||||||
|
"peer4": {ID: "peer4", Key: "key4", UserID: "user2"},
|
||||||
|
"peer5": {ID: "peer5", Key: "key5", UserID: "user2"},
|
||||||
|
},
|
||||||
|
Groups: map[string]*Group{
|
||||||
|
"group1": {ID: "group1", Name: "group1", Issued: GroupIssuedAPI, Peers: []string{}},
|
||||||
|
},
|
||||||
|
Settings: &Settings{GroupsPropagationEnabled: true},
|
||||||
|
Users: map[string]*User{
|
||||||
|
"user1": {Id: "user1"},
|
||||||
|
"user2": {Id: "user2"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("api group already exists", func(t *testing.T) {
|
||||||
|
updated := account.SetJWTGroups("user1", []string{"group1"})
|
||||||
|
assert.False(t, updated, "account should not be updated")
|
||||||
|
assert.Empty(t, account.Users["user1"].AutoGroups, "auto groups must be empty")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("add jwt group", func(t *testing.T) {
|
||||||
|
updated := account.SetJWTGroups("user1", []string{"group1", "group2"})
|
||||||
|
assert.True(t, updated, "account should be updated")
|
||||||
|
assert.Len(t, account.Groups, 2, "new group should be added")
|
||||||
|
assert.Len(t, account.Users["user1"].AutoGroups, 1, "new group should be added")
|
||||||
|
assert.Contains(t, account.Groups, account.Users["user1"].AutoGroups[0], "groups must contain group2 from user groups")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("existed group not update", func(t *testing.T) {
|
||||||
|
updated := account.SetJWTGroups("user1", []string{"group2"})
|
||||||
|
assert.False(t, updated, "account should not be updated")
|
||||||
|
assert.Len(t, account.Groups, 2, "groups count should not be changed")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("add new group", func(t *testing.T) {
|
||||||
|
updated := account.SetJWTGroups("user2", []string{"group1", "group3"})
|
||||||
|
assert.True(t, updated, "account should be updated")
|
||||||
|
assert.Len(t, account.Groups, 3, "new group should be added")
|
||||||
|
assert.Len(t, account.Users["user2"].AutoGroups, 1, "new group should be added")
|
||||||
|
assert.Contains(t, account.Groups, account.Users["user2"].AutoGroups[0], "groups must contain group3 from user groups")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccount_UserGroupsAddToPeers(t *testing.T) {
|
||||||
|
account := &Account{
|
||||||
|
Peers: map[string]*Peer{
|
||||||
|
"peer1": {ID: "peer1", Key: "key1", UserID: "user1"},
|
||||||
|
"peer2": {ID: "peer2", Key: "key2", UserID: "user1"},
|
||||||
|
"peer3": {ID: "peer3", Key: "key3", UserID: "user1"},
|
||||||
|
"peer4": {ID: "peer4", Key: "key4", UserID: "user2"},
|
||||||
|
"peer5": {ID: "peer5", Key: "key5", UserID: "user2"},
|
||||||
|
},
|
||||||
|
Groups: map[string]*Group{
|
||||||
|
"group1": {ID: "group1", Name: "group1", Issued: GroupIssuedAPI, Peers: []string{}},
|
||||||
|
"group2": {ID: "group2", Name: "group2", Issued: GroupIssuedAPI, Peers: []string{}},
|
||||||
|
"group3": {ID: "group3", Name: "group3", Issued: GroupIssuedAPI, Peers: []string{}},
|
||||||
|
},
|
||||||
|
Users: map[string]*User{"user1": {Id: "user1"}, "user2": {Id: "user2"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("add groups", func(t *testing.T) {
|
||||||
|
account.UserGroupsAddToPeers("user1", "group1", "group2")
|
||||||
|
assert.ElementsMatch(t, account.Groups["group1"].Peers, []string{"peer1", "peer2", "peer3"}, "group1 contains users peers")
|
||||||
|
assert.ElementsMatch(t, account.Groups["group2"].Peers, []string{"peer1", "peer2", "peer3"}, "group2 contains users peers")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("add same groups", func(t *testing.T) {
|
||||||
|
account.UserGroupsAddToPeers("user1", "group1", "group2")
|
||||||
|
assert.Len(t, account.Groups["group1"].Peers, 3, "peers amount in group1 didn't change")
|
||||||
|
assert.Len(t, account.Groups["group2"].Peers, 3, "peers amount in group2 didn't change")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("add second user peers", func(t *testing.T) {
|
||||||
|
account.UserGroupsAddToPeers("user2", "group2")
|
||||||
|
assert.ElementsMatch(t, account.Groups["group2"].Peers,
|
||||||
|
[]string{"peer1", "peer2", "peer3", "peer4", "peer5"}, "group2 contains first and second user peers")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccount_UserGroupsRemoveFromPeers(t *testing.T) {
|
||||||
|
account := &Account{
|
||||||
|
Peers: map[string]*Peer{
|
||||||
|
"peer1": {ID: "peer1", Key: "key1", UserID: "user1"},
|
||||||
|
"peer2": {ID: "peer2", Key: "key2", UserID: "user1"},
|
||||||
|
"peer3": {ID: "peer3", Key: "key3", UserID: "user1"},
|
||||||
|
"peer4": {ID: "peer4", Key: "key4", UserID: "user2"},
|
||||||
|
"peer5": {ID: "peer5", Key: "key5", UserID: "user2"},
|
||||||
|
},
|
||||||
|
Groups: map[string]*Group{
|
||||||
|
"group1": {ID: "group1", Name: "group1", Issued: GroupIssuedAPI, Peers: []string{"peer1", "peer2", "peer3"}},
|
||||||
|
"group2": {ID: "group2", Name: "group2", Issued: GroupIssuedAPI, Peers: []string{"peer1", "peer2", "peer3", "peer4", "peer5"}},
|
||||||
|
"group3": {ID: "group3", Name: "group3", Issued: GroupIssuedAPI, Peers: []string{"peer4", "peer5"}},
|
||||||
|
},
|
||||||
|
Users: map[string]*User{"user1": {Id: "user1"}, "user2": {Id: "user2"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("remove groups", func(t *testing.T) {
|
||||||
|
account.UserGroupsRemoveFromPeers("user1", "group1", "group2")
|
||||||
|
assert.Empty(t, account.Groups["group1"].Peers, "remove all peers from group1")
|
||||||
|
assert.ElementsMatch(t, account.Groups["group2"].Peers, []string{"peer4", "peer5"}, "group2 contains only second users peers")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("remove group with no peers", func(t *testing.T) {
|
||||||
|
account.UserGroupsRemoveFromPeers("user1", "group3")
|
||||||
|
assert.Len(t, account.Groups["group3"].Peers, 2, "peers amount should not change")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func createManager(t *testing.T) (*DefaultAccountManager, error) {
|
func createManager(t *testing.T) (*DefaultAccountManager, error) {
|
||||||
store, err := createStore(t)
|
store, err := createStore(t)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
package activity
|
package activity
|
||||||
|
|
||||||
|
// Activity that triggered an Event
|
||||||
|
type Activity int
|
||||||
|
|
||||||
|
// Code is an activity string representation
|
||||||
|
type Code struct {
|
||||||
|
message string
|
||||||
|
code string
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// PeerAddedByUser indicates that a user added a new peer to the system
|
// PeerAddedByUser indicates that a user added a new peer to the system
|
||||||
PeerAddedByUser Activity = iota
|
PeerAddedByUser Activity = iota
|
||||||
@@ -97,314 +106,80 @@ const (
|
|||||||
UserUnblocked
|
UserUnblocked
|
||||||
// GroupDeleted indicates that a user deleted group
|
// GroupDeleted indicates that a user deleted group
|
||||||
GroupDeleted
|
GroupDeleted
|
||||||
|
// UserLoggedInPeer indicates that user logged in their peer with an interactive SSO login
|
||||||
|
UserLoggedInPeer
|
||||||
|
// PeerLoginExpired indicates that the user peer login has been expired and peer disconnected
|
||||||
|
PeerLoginExpired
|
||||||
|
// DashboardLogin indicates that the user logged in to the dashboard
|
||||||
|
DashboardLogin
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
var activityMap = map[Activity]Code{
|
||||||
// PeerAddedByUserMessage is a human-readable text message of the PeerAddedByUser activity
|
PeerAddedByUser: {"Peer added", "user.peer.add"},
|
||||||
PeerAddedByUserMessage string = "Peer added"
|
PeerAddedWithSetupKey: {"Peer added", "setupkey.peer.add"},
|
||||||
// PeerAddedWithSetupKeyMessage is a human-readable text message of the PeerAddedWithSetupKey activity
|
UserJoined: {"User joined", "user.join"},
|
||||||
PeerAddedWithSetupKeyMessage = PeerAddedByUserMessage
|
UserInvited: {"User invited", "user.invite"},
|
||||||
// UserJoinedMessage is a human-readable text message of the UserJoined activity
|
AccountCreated: {"Account created", "account.create"},
|
||||||
UserJoinedMessage string = "User joined"
|
PeerRemovedByUser: {"Peer deleted", "user.peer.delete"},
|
||||||
// UserInvitedMessage is a human-readable text message of the UserInvited activity
|
RuleAdded: {"Rule added", "rule.add"},
|
||||||
UserInvitedMessage string = "User invited"
|
RuleUpdated: {"Rule updated", "rule.update"},
|
||||||
// AccountCreatedMessage is a human-readable text message of the AccountCreated activity
|
RuleRemoved: {"Rule deleted", "rule.delete"},
|
||||||
AccountCreatedMessage string = "Account created"
|
PolicyAdded: {"Policy added", "policy.add"},
|
||||||
// PeerRemovedByUserMessage is a human-readable text message of the PeerRemovedByUser activity
|
PolicyUpdated: {"Policy updated", "policy.update"},
|
||||||
PeerRemovedByUserMessage string = "Peer deleted"
|
PolicyRemoved: {"Policy deleted", "policy.delete"},
|
||||||
// RuleAddedMessage is a human-readable text message of the RuleAdded activity
|
SetupKeyCreated: {"Setup key created", "setupkey.add"},
|
||||||
RuleAddedMessage string = "Rule added"
|
SetupKeyUpdated: {"Setup key updated", "setupkey.update"},
|
||||||
// RuleRemovedMessage is a human-readable text message of the RuleRemoved activity
|
SetupKeyRevoked: {"Setup key revoked", "setupkey.revoke"},
|
||||||
RuleRemovedMessage string = "Rule deleted"
|
SetupKeyOverused: {"Setup key overused", "setupkey.overuse"},
|
||||||
// RuleUpdatedMessage is a human-readable text message of the RuleRemoved activity
|
GroupCreated: {"Group created", "group.add"},
|
||||||
RuleUpdatedMessage string = "Rule updated"
|
GroupUpdated: {"Group updated", "group.update"},
|
||||||
// PolicyAddedMessage is a human-readable text message of the PolicyAdded activity
|
GroupAddedToPeer: {"Group added to peer", "peer.group.add"},
|
||||||
PolicyAddedMessage string = "Policy added"
|
GroupRemovedFromPeer: {"Group removed from peer", "peer.group.delete"},
|
||||||
// PolicyRemovedMessage is a human-readable text message of the PolicyRemoved activity
|
GroupAddedToUser: {"Group added to user", "user.group.add"},
|
||||||
PolicyRemovedMessage string = "Policy deleted"
|
GroupRemovedFromUser: {"Group removed from user", "user.group.delete"},
|
||||||
// PolicyUpdatedMessage is a human-readable text message of the PolicyRemoved activity
|
UserRoleUpdated: {"User role updated", "user.role.update"},
|
||||||
PolicyUpdatedMessage string = "Policy updated"
|
GroupAddedToSetupKey: {"Group added to setup key", "setupkey.group.add"},
|
||||||
// SetupKeyCreatedMessage is a human-readable text message of the SetupKeyCreated activity
|
GroupRemovedFromSetupKey: {"Group removed from user setup key", "setupkey.group.delete"},
|
||||||
SetupKeyCreatedMessage string = "Setup key created"
|
GroupAddedToDisabledManagementGroups: {"Group added to disabled management DNS setting", "dns.setting.disabled.management.group.add"},
|
||||||
// SetupKeyUpdatedMessage is a human-readable text message of the SetupKeyUpdated activity
|
GroupRemovedFromDisabledManagementGroups: {"Group removed from disabled management DNS setting", "dns.setting.disabled.management.group.delete"},
|
||||||
SetupKeyUpdatedMessage string = "Setup key updated"
|
RouteCreated: {"Route created", "route.add"},
|
||||||
// SetupKeyRevokedMessage is a human-readable text message of the SetupKeyRevoked activity
|
RouteRemoved: {"Route deleted", "route.delete"},
|
||||||
SetupKeyRevokedMessage string = "Setup key revoked"
|
RouteUpdated: {"Route updated", "route.update"},
|
||||||
// SetupKeyOverusedMessage is a human-readable text message of the SetupKeyOverused activity
|
PeerSSHEnabled: {"Peer SSH server enabled", "peer.ssh.enable"},
|
||||||
SetupKeyOverusedMessage string = "Setup key overused"
|
PeerSSHDisabled: {"Peer SSH server disabled", "peer.ssh.disable"},
|
||||||
// GroupCreatedMessage is a human-readable text message of the GroupCreated activity
|
PeerRenamed: {"Peer renamed", "peer.rename"},
|
||||||
GroupCreatedMessage string = "Group created"
|
PeerLoginExpirationEnabled: {"Peer login expiration enabled", "peer.login.expiration.enable"},
|
||||||
// GroupUpdatedMessage is a human-readable text message of the GroupUpdated activity
|
PeerLoginExpirationDisabled: {"Peer login expiration disabled", "peer.login.expiration.disable"},
|
||||||
GroupUpdatedMessage string = "Group updated"
|
NameserverGroupCreated: {"Nameserver group created", "nameserver.group.add"},
|
||||||
// GroupAddedToPeerMessage is a human-readable text message of the GroupAddedToPeer activity
|
NameserverGroupDeleted: {"Nameserver group deleted", "nameserver.group.delete"},
|
||||||
GroupAddedToPeerMessage string = "Group added to peer"
|
NameserverGroupUpdated: {"Nameserver group updated", "nameserver.group.update"},
|
||||||
// GroupRemovedFromPeerMessage is a human-readable text message of the GroupRemovedFromPeer activity
|
AccountPeerLoginExpirationDurationUpdated: {"Account peer login expiration duration updated", "account.setting.peer.login.expiration.update"},
|
||||||
GroupRemovedFromPeerMessage string = "Group removed from peer"
|
AccountPeerLoginExpirationEnabled: {"Account peer login expiration enabled", "account.setting.peer.login.expiration.enable"},
|
||||||
// GroupAddedToUserMessage is a human-readable text message of the GroupAddedToUser activity
|
AccountPeerLoginExpirationDisabled: {"Account peer login expiration disabled", "account.setting.peer.login.expiration.disable"},
|
||||||
GroupAddedToUserMessage string = "Group added to user"
|
PersonalAccessTokenCreated: {"Personal access token created", "personal.access.token.create"},
|
||||||
// GroupRemovedFromUserMessage is a human-readable text message of the GroupRemovedFromUser activity
|
PersonalAccessTokenDeleted: {"Personal access token deleted", "personal.access.token.delete"},
|
||||||
GroupRemovedFromUserMessage string = "Group removed from user"
|
ServiceUserCreated: {"Service user created", "service.user.create"},
|
||||||
// UserRoleUpdatedMessage is a human-readable text message of the UserRoleUpdatedMessage activity
|
ServiceUserDeleted: {"Service user deleted", "service.user.delete"},
|
||||||
UserRoleUpdatedMessage string = "User role updated"
|
UserBlocked: {"User blocked", "user.block"},
|
||||||
// GroupAddedToSetupKeyMessage is a human-readable text message of the GroupAddedToSetupKey activity
|
UserUnblocked: {"User unblocked", "user.unblock"},
|
||||||
GroupAddedToSetupKeyMessage string = "Group added to setup key"
|
GroupDeleted: {"Group deleted", "group.delete"},
|
||||||
// GroupRemovedFromSetupKeyMessage is a human-readable text message of the GroupRemovedFromSetupKey activity
|
UserLoggedInPeer: {"User logged in peer", "user.peer.login"},
|
||||||
GroupRemovedFromSetupKeyMessage string = "Group removed from user setup key"
|
PeerLoginExpired: {"Peer login expired", "peer.login.expire"},
|
||||||
// GroupAddedToDisabledManagementGroupsMessage is a human-readable text message of the GroupAddedToDisabledManagementGroups activity
|
DashboardLogin: {"Dashboard login", "dashboard.login"},
|
||||||
GroupAddedToDisabledManagementGroupsMessage string = "Group added to disabled management DNS setting"
|
|
||||||
// GroupRemovedFromDisabledManagementGroupsMessage is a human-readable text message of the GroupRemovedFromDisabledManagementGroups activity
|
|
||||||
GroupRemovedFromDisabledManagementGroupsMessage string = "Group removed from disabled management DNS setting"
|
|
||||||
// RouteCreatedMessage is a human-readable text message of the RouteCreated activity
|
|
||||||
RouteCreatedMessage string = "Route created"
|
|
||||||
// RouteRemovedMessage is a human-readable text message of the RouteRemoved activity
|
|
||||||
RouteRemovedMessage string = "Route deleted"
|
|
||||||
// RouteUpdatedMessage is a human-readable text message of the RouteUpdated activity
|
|
||||||
RouteUpdatedMessage string = "Route updated"
|
|
||||||
// PeerSSHEnabledMessage is a human-readable text message of the PeerSSHEnabled activity
|
|
||||||
PeerSSHEnabledMessage string = "Peer SSH server enabled"
|
|
||||||
// PeerSSHDisabledMessage is a human-readable text message of the PeerSSHDisabled activity
|
|
||||||
PeerSSHDisabledMessage string = "Peer SSH server disabled"
|
|
||||||
// PeerRenamedMessage is a human-readable text message of the PeerRenamed activity
|
|
||||||
PeerRenamedMessage string = "Peer renamed"
|
|
||||||
// PeerLoginExpirationDisabledMessage is a human-readable text message of the PeerLoginExpirationDisabled activity
|
|
||||||
PeerLoginExpirationDisabledMessage string = "Peer login expiration disabled"
|
|
||||||
// PeerLoginExpirationEnabledMessage is a human-readable text message of the PeerLoginExpirationEnabled activity
|
|
||||||
PeerLoginExpirationEnabledMessage string = "Peer login expiration enabled"
|
|
||||||
// NameserverGroupCreatedMessage is a human-readable text message of the NameserverGroupCreated activity
|
|
||||||
NameserverGroupCreatedMessage string = "Nameserver group created"
|
|
||||||
// NameserverGroupDeletedMessage is a human-readable text message of the NameserverGroupDeleted activity
|
|
||||||
NameserverGroupDeletedMessage string = "Nameserver group deleted"
|
|
||||||
// NameserverGroupUpdatedMessage is a human-readable text message of the NameserverGroupUpdated activity
|
|
||||||
NameserverGroupUpdatedMessage string = "Nameserver group updated"
|
|
||||||
// AccountPeerLoginExpirationEnabledMessage is a human-readable text message of the AccountPeerLoginExpirationEnabled activity
|
|
||||||
AccountPeerLoginExpirationEnabledMessage string = "Peer login expiration enabled for the account"
|
|
||||||
// AccountPeerLoginExpirationDisabledMessage is a human-readable text message of the AccountPeerLoginExpirationDisabled activity
|
|
||||||
AccountPeerLoginExpirationDisabledMessage string = "Peer login expiration disabled for the account"
|
|
||||||
// AccountPeerLoginExpirationDurationUpdatedMessage is a human-readable text message of the AccountPeerLoginExpirationDurationUpdated activity
|
|
||||||
AccountPeerLoginExpirationDurationUpdatedMessage string = "Peer login expiration duration updated"
|
|
||||||
// PersonalAccessTokenCreatedMessage is a human-readable text message of the PersonalAccessTokenCreated activity
|
|
||||||
PersonalAccessTokenCreatedMessage string = "Personal access token created"
|
|
||||||
// PersonalAccessTokenDeletedMessage is a human-readable text message of the PersonalAccessTokenDeleted activity
|
|
||||||
PersonalAccessTokenDeletedMessage string = "Personal access token deleted"
|
|
||||||
// ServiceUserCreatedMessage is a human-readable text message of the ServiceUserCreated activity
|
|
||||||
ServiceUserCreatedMessage string = "Service user created"
|
|
||||||
// ServiceUserDeletedMessage is a human-readable text message of the ServiceUserDeleted activity
|
|
||||||
ServiceUserDeletedMessage string = "Service user deleted"
|
|
||||||
// UserBlockedMessage is a human-readable text message of the UserBlocked activity
|
|
||||||
UserBlockedMessage string = "User blocked"
|
|
||||||
// UserUnblockedMessage is a human-readable text message of the UserUnblocked activity
|
|
||||||
UserUnblockedMessage string = "User unblocked"
|
|
||||||
// GroupDeletedMessage is a human-readable text message of the GroupDeleted activity
|
|
||||||
GroupDeletedMessage string = "Group deleted"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Activity that triggered an Event
|
|
||||||
type Activity int
|
|
||||||
|
|
||||||
// Message returns a string representation of an activity
|
|
||||||
func (a Activity) Message() string {
|
|
||||||
switch a {
|
|
||||||
case PeerAddedByUser:
|
|
||||||
return PeerAddedByUserMessage
|
|
||||||
case PeerRemovedByUser:
|
|
||||||
return PeerRemovedByUserMessage
|
|
||||||
case PeerAddedWithSetupKey:
|
|
||||||
return PeerAddedWithSetupKeyMessage
|
|
||||||
case UserJoined:
|
|
||||||
return UserJoinedMessage
|
|
||||||
case UserInvited:
|
|
||||||
return UserInvitedMessage
|
|
||||||
case AccountCreated:
|
|
||||||
return AccountCreatedMessage
|
|
||||||
case RuleAdded:
|
|
||||||
return RuleAddedMessage
|
|
||||||
case RuleRemoved:
|
|
||||||
return RuleRemovedMessage
|
|
||||||
case RuleUpdated:
|
|
||||||
return RuleUpdatedMessage
|
|
||||||
case PolicyAdded:
|
|
||||||
return PolicyAddedMessage
|
|
||||||
case PolicyRemoved:
|
|
||||||
return PolicyRemovedMessage
|
|
||||||
case PolicyUpdated:
|
|
||||||
return PolicyUpdatedMessage
|
|
||||||
case SetupKeyCreated:
|
|
||||||
return SetupKeyCreatedMessage
|
|
||||||
case SetupKeyUpdated:
|
|
||||||
return SetupKeyUpdatedMessage
|
|
||||||
case SetupKeyRevoked:
|
|
||||||
return SetupKeyRevokedMessage
|
|
||||||
case SetupKeyOverused:
|
|
||||||
return SetupKeyOverusedMessage
|
|
||||||
case GroupCreated:
|
|
||||||
return GroupCreatedMessage
|
|
||||||
case GroupUpdated:
|
|
||||||
return GroupUpdatedMessage
|
|
||||||
case GroupAddedToPeer:
|
|
||||||
return GroupAddedToPeerMessage
|
|
||||||
case GroupRemovedFromPeer:
|
|
||||||
return GroupRemovedFromPeerMessage
|
|
||||||
case GroupRemovedFromUser:
|
|
||||||
return GroupRemovedFromUserMessage
|
|
||||||
case GroupAddedToUser:
|
|
||||||
return GroupAddedToUserMessage
|
|
||||||
case UserRoleUpdated:
|
|
||||||
return UserRoleUpdatedMessage
|
|
||||||
case GroupAddedToSetupKey:
|
|
||||||
return GroupAddedToSetupKeyMessage
|
|
||||||
case GroupRemovedFromSetupKey:
|
|
||||||
return GroupRemovedFromSetupKeyMessage
|
|
||||||
case GroupAddedToDisabledManagementGroups:
|
|
||||||
return GroupAddedToDisabledManagementGroupsMessage
|
|
||||||
case GroupRemovedFromDisabledManagementGroups:
|
|
||||||
return GroupRemovedFromDisabledManagementGroupsMessage
|
|
||||||
case RouteCreated:
|
|
||||||
return RouteCreatedMessage
|
|
||||||
case RouteRemoved:
|
|
||||||
return RouteRemovedMessage
|
|
||||||
case RouteUpdated:
|
|
||||||
return RouteUpdatedMessage
|
|
||||||
case PeerSSHEnabled:
|
|
||||||
return PeerSSHEnabledMessage
|
|
||||||
case PeerSSHDisabled:
|
|
||||||
return PeerSSHDisabledMessage
|
|
||||||
case PeerLoginExpirationEnabled:
|
|
||||||
return PeerLoginExpirationEnabledMessage
|
|
||||||
case PeerLoginExpirationDisabled:
|
|
||||||
return PeerLoginExpirationDisabledMessage
|
|
||||||
case PeerRenamed:
|
|
||||||
return PeerRenamedMessage
|
|
||||||
case NameserverGroupCreated:
|
|
||||||
return NameserverGroupCreatedMessage
|
|
||||||
case NameserverGroupDeleted:
|
|
||||||
return NameserverGroupDeletedMessage
|
|
||||||
case NameserverGroupUpdated:
|
|
||||||
return NameserverGroupUpdatedMessage
|
|
||||||
case AccountPeerLoginExpirationEnabled:
|
|
||||||
return AccountPeerLoginExpirationEnabledMessage
|
|
||||||
case AccountPeerLoginExpirationDisabled:
|
|
||||||
return AccountPeerLoginExpirationDisabledMessage
|
|
||||||
case AccountPeerLoginExpirationDurationUpdated:
|
|
||||||
return AccountPeerLoginExpirationDurationUpdatedMessage
|
|
||||||
case PersonalAccessTokenCreated:
|
|
||||||
return PersonalAccessTokenCreatedMessage
|
|
||||||
case PersonalAccessTokenDeleted:
|
|
||||||
return PersonalAccessTokenDeletedMessage
|
|
||||||
case ServiceUserCreated:
|
|
||||||
return ServiceUserCreatedMessage
|
|
||||||
case ServiceUserDeleted:
|
|
||||||
return ServiceUserDeletedMessage
|
|
||||||
case UserBlocked:
|
|
||||||
return UserBlockedMessage
|
|
||||||
case UserUnblocked:
|
|
||||||
return UserUnblockedMessage
|
|
||||||
case GroupDeleted:
|
|
||||||
return GroupDeletedMessage
|
|
||||||
default:
|
|
||||||
return "UNKNOWN_ACTIVITY"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// StringCode returns a string code of the activity
|
// StringCode returns a string code of the activity
|
||||||
func (a Activity) StringCode() string {
|
func (a Activity) StringCode() string {
|
||||||
switch a {
|
if code, ok := activityMap[a]; ok {
|
||||||
case PeerAddedByUser:
|
return code.code
|
||||||
return "user.peer.add"
|
}
|
||||||
case PeerRemovedByUser:
|
|
||||||
return "user.peer.delete"
|
|
||||||
case PeerAddedWithSetupKey:
|
|
||||||
return "setupkey.peer.add"
|
|
||||||
case UserJoined:
|
|
||||||
return "user.join"
|
|
||||||
case UserInvited:
|
|
||||||
return "user.invite"
|
|
||||||
case UserBlocked:
|
|
||||||
return "user.block"
|
|
||||||
case UserUnblocked:
|
|
||||||
return "user.unblock"
|
|
||||||
case AccountCreated:
|
|
||||||
return "account.create"
|
|
||||||
case RuleAdded:
|
|
||||||
return "rule.add"
|
|
||||||
case RuleRemoved:
|
|
||||||
return "rule.delete"
|
|
||||||
case RuleUpdated:
|
|
||||||
return "rule.update"
|
|
||||||
case PolicyAdded:
|
|
||||||
return "policy.add"
|
|
||||||
case PolicyRemoved:
|
|
||||||
return "policy.delete"
|
|
||||||
case PolicyUpdated:
|
|
||||||
return "policy.update"
|
|
||||||
case SetupKeyCreated:
|
|
||||||
return "setupkey.add"
|
|
||||||
case SetupKeyRevoked:
|
|
||||||
return "setupkey.revoke"
|
|
||||||
case SetupKeyOverused:
|
|
||||||
return "setupkey.overuse"
|
|
||||||
case SetupKeyUpdated:
|
|
||||||
return "setupkey.update"
|
|
||||||
case GroupCreated:
|
|
||||||
return "group.add"
|
|
||||||
case GroupUpdated:
|
|
||||||
return "group.update"
|
|
||||||
case GroupDeleted:
|
|
||||||
return "group.delete"
|
|
||||||
case GroupRemovedFromPeer:
|
|
||||||
return "peer.group.delete"
|
|
||||||
case GroupAddedToPeer:
|
|
||||||
return "peer.group.add"
|
|
||||||
case GroupAddedToUser:
|
|
||||||
return "user.group.add"
|
|
||||||
case GroupRemovedFromUser:
|
|
||||||
return "user.group.delete"
|
|
||||||
case UserRoleUpdated:
|
|
||||||
return "user.role.update"
|
|
||||||
case GroupAddedToSetupKey:
|
|
||||||
return "setupkey.group.add"
|
|
||||||
case GroupRemovedFromSetupKey:
|
|
||||||
return "setupkey.group.delete"
|
|
||||||
case GroupAddedToDisabledManagementGroups:
|
|
||||||
return "dns.setting.disabled.management.group.add"
|
|
||||||
case GroupRemovedFromDisabledManagementGroups:
|
|
||||||
return "dns.setting.disabled.management.group.delete"
|
|
||||||
case RouteCreated:
|
|
||||||
return "route.add"
|
|
||||||
case RouteRemoved:
|
|
||||||
return "route.delete"
|
|
||||||
case RouteUpdated:
|
|
||||||
return "route.update"
|
|
||||||
case PeerRenamed:
|
|
||||||
return "peer.rename"
|
|
||||||
case PeerSSHEnabled:
|
|
||||||
return "peer.ssh.enable"
|
|
||||||
case PeerSSHDisabled:
|
|
||||||
return "peer.ssh.disable"
|
|
||||||
case PeerLoginExpirationDisabled:
|
|
||||||
return "peer.login.expiration.disable"
|
|
||||||
case PeerLoginExpirationEnabled:
|
|
||||||
return "peer.login.expiration.enable"
|
|
||||||
case NameserverGroupCreated:
|
|
||||||
return "nameserver.group.add"
|
|
||||||
case NameserverGroupDeleted:
|
|
||||||
return "nameserver.group.delete"
|
|
||||||
case NameserverGroupUpdated:
|
|
||||||
return "nameserver.group.update"
|
|
||||||
case AccountPeerLoginExpirationDurationUpdated:
|
|
||||||
return "account.setting.peer.login.expiration.update"
|
|
||||||
case AccountPeerLoginExpirationEnabled:
|
|
||||||
return "account.setting.peer.login.expiration.enable"
|
|
||||||
case AccountPeerLoginExpirationDisabled:
|
|
||||||
return "account.setting.peer.login.expiration.disable"
|
|
||||||
case PersonalAccessTokenCreated:
|
|
||||||
return "personal.access.token.create"
|
|
||||||
case PersonalAccessTokenDeleted:
|
|
||||||
return "personal.access.token.delete"
|
|
||||||
case ServiceUserCreated:
|
|
||||||
return "service.user.create"
|
|
||||||
case ServiceUserDeleted:
|
|
||||||
return "service.user.delete"
|
|
||||||
default:
|
|
||||||
return "UNKNOWN_ACTIVITY"
|
return "UNKNOWN_ACTIVITY"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Message returns a string representation of an activity
|
||||||
|
func (a Activity) Message() string {
|
||||||
|
if code, ok := activityMap[a]; ok {
|
||||||
|
return code.message
|
||||||
|
}
|
||||||
|
return "UNKNOWN_ACTIVITY"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -570,6 +570,26 @@ func (s *FileStore) SavePeerStatus(accountID, peerID string, peerStatus PeerStat
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SaveUserLastLogin stores the last login time for a user in memory. It doesn't attempt to persist data to speed up things.
|
||||||
|
func (s *FileStore) SaveUserLastLogin(accountID, userID string, lastLogin time.Time) error {
|
||||||
|
s.mux.Lock()
|
||||||
|
defer s.mux.Unlock()
|
||||||
|
|
||||||
|
account, err := s.getAccount(accountID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
peer := account.Users[userID]
|
||||||
|
if peer == nil {
|
||||||
|
return status.Errorf(status.NotFound, "user %s not found", userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
peer.LastLogin = lastLogin
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Close the FileStore persisting data to disk
|
// Close the FileStore persisting data to disk
|
||||||
func (s *FileStore) Close() error {
|
func (s *FileStore) Close() error {
|
||||||
s.mux.Lock()
|
s.mux.Lock()
|
||||||
|
|||||||
@@ -59,12 +59,14 @@ func (g *Group) EventMeta() map[string]any {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (g *Group) Copy() *Group {
|
func (g *Group) Copy() *Group {
|
||||||
return &Group{
|
group := &Group{
|
||||||
ID: g.ID,
|
ID: g.ID,
|
||||||
Name: g.Name,
|
Name: g.Name,
|
||||||
Issued: g.Issued,
|
Issued: g.Issued,
|
||||||
Peers: g.Peers[:],
|
Peers: make([]string, len(g.Peers)),
|
||||||
}
|
}
|
||||||
|
copy(group.Peers, g.Peers)
|
||||||
|
return group
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetGroup object of the peers
|
// GetGroup object of the peers
|
||||||
|
|||||||
@@ -80,12 +80,14 @@ func (h *AccountsHandler) UpdateAccount(w http.ResponseWriter, r *http.Request)
|
|||||||
if req.Settings.JwtGroupsEnabled != nil {
|
if req.Settings.JwtGroupsEnabled != nil {
|
||||||
settings.JWTGroupsEnabled = *req.Settings.JwtGroupsEnabled
|
settings.JWTGroupsEnabled = *req.Settings.JwtGroupsEnabled
|
||||||
}
|
}
|
||||||
|
if req.Settings.GroupsPropagationEnabled != nil {
|
||||||
|
settings.GroupsPropagationEnabled = *req.Settings.GroupsPropagationEnabled
|
||||||
|
}
|
||||||
if req.Settings.JwtGroupsClaimName != nil {
|
if req.Settings.JwtGroupsClaimName != nil {
|
||||||
settings.JWTGroupsClaimName = *req.Settings.JwtGroupsClaimName
|
settings.JWTGroupsClaimName = *req.Settings.JwtGroupsClaimName
|
||||||
}
|
}
|
||||||
|
|
||||||
updatedAccount, err := h.accountManager.UpdateAccountSettings(accountID, user.Id, settings)
|
updatedAccount, err := h.accountManager.UpdateAccountSettings(accountID, user.Id, settings)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.WriteError(err, w)
|
util.WriteError(err, w)
|
||||||
return
|
return
|
||||||
@@ -102,6 +104,7 @@ func toAccountResponse(account *server.Account) *api.Account {
|
|||||||
Settings: api.AccountSettings{
|
Settings: api.AccountSettings{
|
||||||
PeerLoginExpiration: int(account.Settings.PeerLoginExpiration.Seconds()),
|
PeerLoginExpiration: int(account.Settings.PeerLoginExpiration.Seconds()),
|
||||||
PeerLoginExpirationEnabled: account.Settings.PeerLoginExpirationEnabled,
|
PeerLoginExpirationEnabled: account.Settings.PeerLoginExpirationEnabled,
|
||||||
|
GroupsPropagationEnabled: &account.Settings.GroupsPropagationEnabled,
|
||||||
JwtGroupsEnabled: &account.Settings.JWTGroupsEnabled,
|
JwtGroupsEnabled: &account.Settings.JWTGroupsEnabled,
|
||||||
JwtGroupsClaimName: &account.Settings.JWTGroupsClaimName,
|
JwtGroupsClaimName: &account.Settings.JWTGroupsClaimName,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ func initAccountsTestData(account *server.Account, admin *server.User) *Accounts
|
|||||||
accCopy := account.Copy()
|
accCopy := account.Copy()
|
||||||
accCopy.UpdateSettings(newSettings)
|
accCopy.UpdateSettings(newSettings)
|
||||||
return accCopy, nil
|
return accCopy, nil
|
||||||
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
claimsExtractor: jwtclaims.NewClaimsExtractor(
|
claimsExtractor: jwtclaims.NewClaimsExtractor(
|
||||||
@@ -54,7 +53,6 @@ func initAccountsTestData(account *server.Account, admin *server.User) *Accounts
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAccounts_AccountsHandler(t *testing.T) {
|
func TestAccounts_AccountsHandler(t *testing.T) {
|
||||||
|
|
||||||
accountID := "test_account"
|
accountID := "test_account"
|
||||||
adminUser := server.NewAdminUser("test_user")
|
adminUser := server.NewAdminUser("test_user")
|
||||||
|
|
||||||
@@ -94,6 +92,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
|||||||
expectedSettings: api.AccountSettings{
|
expectedSettings: api.AccountSettings{
|
||||||
PeerLoginExpiration: int(time.Hour.Seconds()),
|
PeerLoginExpiration: int(time.Hour.Seconds()),
|
||||||
PeerLoginExpirationEnabled: false,
|
PeerLoginExpirationEnabled: false,
|
||||||
|
GroupsPropagationEnabled: br(false),
|
||||||
JwtGroupsClaimName: sr(""),
|
JwtGroupsClaimName: sr(""),
|
||||||
JwtGroupsEnabled: br(false),
|
JwtGroupsEnabled: br(false),
|
||||||
},
|
},
|
||||||
@@ -110,6 +109,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
|||||||
expectedSettings: api.AccountSettings{
|
expectedSettings: api.AccountSettings{
|
||||||
PeerLoginExpiration: 15552000,
|
PeerLoginExpiration: 15552000,
|
||||||
PeerLoginExpirationEnabled: true,
|
PeerLoginExpirationEnabled: true,
|
||||||
|
GroupsPropagationEnabled: br(false),
|
||||||
JwtGroupsClaimName: sr(""),
|
JwtGroupsClaimName: sr(""),
|
||||||
JwtGroupsEnabled: br(false),
|
JwtGroupsEnabled: br(false),
|
||||||
},
|
},
|
||||||
@@ -126,12 +126,30 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
|||||||
expectedSettings: api.AccountSettings{
|
expectedSettings: api.AccountSettings{
|
||||||
PeerLoginExpiration: 15552000,
|
PeerLoginExpiration: 15552000,
|
||||||
PeerLoginExpirationEnabled: false,
|
PeerLoginExpirationEnabled: false,
|
||||||
|
GroupsPropagationEnabled: br(false),
|
||||||
JwtGroupsClaimName: sr("roles"),
|
JwtGroupsClaimName: sr("roles"),
|
||||||
JwtGroupsEnabled: br(true),
|
JwtGroupsEnabled: br(true),
|
||||||
},
|
},
|
||||||
expectedArray: false,
|
expectedArray: false,
|
||||||
expectedID: accountID,
|
expectedID: accountID,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "PutAccount OK wiht JWT Propagation",
|
||||||
|
expectedBody: true,
|
||||||
|
requestType: http.MethodPut,
|
||||||
|
requestPath: "/api/accounts/" + accountID,
|
||||||
|
requestBody: bytes.NewBufferString("{\"settings\": {\"peer_login_expiration\": 554400,\"peer_login_expiration_enabled\": true,\"jwt_groups_enabled\":true,\"jwt_groups_claim_name\":\"groups\",\"groups_propagation_enabled\":true}}"),
|
||||||
|
expectedStatus: http.StatusOK,
|
||||||
|
expectedSettings: api.AccountSettings{
|
||||||
|
PeerLoginExpiration: 554400,
|
||||||
|
PeerLoginExpirationEnabled: true,
|
||||||
|
GroupsPropagationEnabled: br(true),
|
||||||
|
JwtGroupsClaimName: sr("groups"),
|
||||||
|
JwtGroupsEnabled: br(true),
|
||||||
|
},
|
||||||
|
expectedArray: false,
|
||||||
|
expectedID: accountID,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "Update account failure with high peer_login_expiration more than 180 days",
|
name: "Update account failure with high peer_login_expiration more than 180 days",
|
||||||
expectedBody: true,
|
expectedBody: true,
|
||||||
|
|||||||
@@ -54,6 +54,10 @@ components:
|
|||||||
description: Period of time after which peer login expires (seconds).
|
description: Period of time after which peer login expires (seconds).
|
||||||
type: integer
|
type: integer
|
||||||
example: 43200
|
example: 43200
|
||||||
|
groups_propagation_enabled:
|
||||||
|
description: Allows propagate the new user auto groups to peers that belongs to the user
|
||||||
|
type: boolean
|
||||||
|
example: true
|
||||||
jwt_groups_enabled:
|
jwt_groups_enabled:
|
||||||
description: Allows extract groups from JWT claim and add it to account groups.
|
description: Allows extract groups from JWT claim and add it to account groups.
|
||||||
type: boolean
|
type: boolean
|
||||||
@@ -96,6 +100,11 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
enum: [ "active","invited","blocked" ]
|
enum: [ "active","invited","blocked" ]
|
||||||
example: active
|
example: active
|
||||||
|
last_login:
|
||||||
|
description: Last time this user performed a login to the dashboard
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
example: 2023-05-05T09:00:35.477782Z
|
||||||
auto_groups:
|
auto_groups:
|
||||||
description: Groups to auto-assign to peers registered by this user
|
description: Groups to auto-assign to peers registered by this user
|
||||||
type: array
|
type: array
|
||||||
@@ -327,7 +336,7 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
example: valid
|
example: valid
|
||||||
auto_groups:
|
auto_groups:
|
||||||
description: Setup key groups to auto-assign to peers registered with this key
|
description: List of group IDs to auto-assign to peers registered with this key
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
@@ -369,13 +378,15 @@ components:
|
|||||||
expires_in:
|
expires_in:
|
||||||
description: Expiration time in seconds
|
description: Expiration time in seconds
|
||||||
type: integer
|
type: integer
|
||||||
example: 43200
|
minimum: 86400
|
||||||
|
maximum: 31536000
|
||||||
|
example: 86400
|
||||||
revoked:
|
revoked:
|
||||||
description: Setup key revocation status
|
description: Setup key revocation status
|
||||||
type: boolean
|
type: boolean
|
||||||
example: false
|
example: false
|
||||||
auto_groups:
|
auto_groups:
|
||||||
description: Setup key groups to auto-assign to peers registered with this key
|
description: List of group IDs to auto-assign to peers registered with this key
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
@@ -886,7 +897,7 @@ components:
|
|||||||
description: The string code of the activity that occurred during the event
|
description: The string code of the activity that occurred during the event
|
||||||
type: string
|
type: string
|
||||||
enum: [ "user.peer.delete", "user.join", "user.invite", "user.peer.add", "user.group.add", "user.group.delete",
|
enum: [ "user.peer.delete", "user.join", "user.invite", "user.peer.add", "user.group.add", "user.group.delete",
|
||||||
"user.role.update", "user.block", "user.unblock",
|
"user.role.update", "user.block", "user.unblock", "user.peer.login",
|
||||||
"setupkey.peer.add", "setupkey.add", "setupkey.update", "setupkey.revoke", "setupkey.overuse",
|
"setupkey.peer.add", "setupkey.add", "setupkey.update", "setupkey.revoke", "setupkey.overuse",
|
||||||
"setupkey.group.delete", "setupkey.group.add",
|
"setupkey.group.delete", "setupkey.group.add",
|
||||||
"rule.add", "rule.delete", "rule.update",
|
"rule.add", "rule.delete", "rule.update",
|
||||||
@@ -895,7 +906,7 @@ components:
|
|||||||
"account.create", "account.setting.peer.login.expiration.update", "account.setting.peer.login.expiration.disable", "account.setting.peer.login.expiration.enable",
|
"account.create", "account.setting.peer.login.expiration.update", "account.setting.peer.login.expiration.disable", "account.setting.peer.login.expiration.enable",
|
||||||
"route.add", "route.delete", "route.update",
|
"route.add", "route.delete", "route.update",
|
||||||
"nameserver.group.add", "nameserver.group.delete", "nameserver.group.update",
|
"nameserver.group.add", "nameserver.group.delete", "nameserver.group.update",
|
||||||
"peer.ssh.disable", "peer.ssh.enable", "peer.rename", "peer.login.expiration.disable", "peer.login.expiration.enable",
|
"peer.ssh.disable", "peer.ssh.enable", "peer.rename", "peer.login.expiration.disable", "peer.login.expiration.enable", "peer.login.expire",
|
||||||
"service.user.create", "personal.access.token.create", "service.user.delete", "personal.access.token.delete" ]
|
"service.user.create", "personal.access.token.create", "service.user.delete", "personal.access.token.delete" ]
|
||||||
example: route.add
|
example: route.add
|
||||||
initiator_id:
|
initiator_id:
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ const (
|
|||||||
EventActivityCodeNameserverGroupUpdate EventActivityCode = "nameserver.group.update"
|
EventActivityCodeNameserverGroupUpdate EventActivityCode = "nameserver.group.update"
|
||||||
EventActivityCodePeerLoginExpirationDisable EventActivityCode = "peer.login.expiration.disable"
|
EventActivityCodePeerLoginExpirationDisable EventActivityCode = "peer.login.expiration.disable"
|
||||||
EventActivityCodePeerLoginExpirationEnable EventActivityCode = "peer.login.expiration.enable"
|
EventActivityCodePeerLoginExpirationEnable EventActivityCode = "peer.login.expiration.enable"
|
||||||
|
EventActivityCodePeerLoginExpire EventActivityCode = "peer.login.expire"
|
||||||
EventActivityCodePeerRename EventActivityCode = "peer.rename"
|
EventActivityCodePeerRename EventActivityCode = "peer.rename"
|
||||||
EventActivityCodePeerSshDisable EventActivityCode = "peer.ssh.disable"
|
EventActivityCodePeerSshDisable EventActivityCode = "peer.ssh.disable"
|
||||||
EventActivityCodePeerSshEnable EventActivityCode = "peer.ssh.enable"
|
EventActivityCodePeerSshEnable EventActivityCode = "peer.ssh.enable"
|
||||||
@@ -57,6 +58,7 @@ const (
|
|||||||
EventActivityCodeUserJoin EventActivityCode = "user.join"
|
EventActivityCodeUserJoin EventActivityCode = "user.join"
|
||||||
EventActivityCodeUserPeerAdd EventActivityCode = "user.peer.add"
|
EventActivityCodeUserPeerAdd EventActivityCode = "user.peer.add"
|
||||||
EventActivityCodeUserPeerDelete EventActivityCode = "user.peer.delete"
|
EventActivityCodeUserPeerDelete EventActivityCode = "user.peer.delete"
|
||||||
|
EventActivityCodeUserPeerLogin EventActivityCode = "user.peer.login"
|
||||||
EventActivityCodeUserRoleUpdate EventActivityCode = "user.role.update"
|
EventActivityCodeUserRoleUpdate EventActivityCode = "user.role.update"
|
||||||
EventActivityCodeUserUnblock EventActivityCode = "user.unblock"
|
EventActivityCodeUserUnblock EventActivityCode = "user.unblock"
|
||||||
)
|
)
|
||||||
@@ -129,6 +131,9 @@ type AccountRequest struct {
|
|||||||
|
|
||||||
// AccountSettings defines model for AccountSettings.
|
// AccountSettings defines model for AccountSettings.
|
||||||
type AccountSettings struct {
|
type AccountSettings struct {
|
||||||
|
// GroupsPropagationEnabled Allows propagate the new user auto groups to peers that belongs to the user
|
||||||
|
GroupsPropagationEnabled *bool `json:"groups_propagation_enabled,omitempty"`
|
||||||
|
|
||||||
// JwtGroupsClaimName Name of the claim from which we extract groups names to add it to account groups.
|
// JwtGroupsClaimName Name of the claim from which we extract groups names to add it to account groups.
|
||||||
JwtGroupsClaimName *string `json:"jwt_groups_claim_name,omitempty"`
|
JwtGroupsClaimName *string `json:"jwt_groups_claim_name,omitempty"`
|
||||||
|
|
||||||
@@ -681,7 +686,7 @@ type RuleRequest struct {
|
|||||||
|
|
||||||
// SetupKey defines model for SetupKey.
|
// SetupKey defines model for SetupKey.
|
||||||
type SetupKey struct {
|
type SetupKey struct {
|
||||||
// AutoGroups Setup key groups to auto-assign to peers registered with this key
|
// AutoGroups List of group IDs to auto-assign to peers registered with this key
|
||||||
AutoGroups []string `json:"auto_groups"`
|
AutoGroups []string `json:"auto_groups"`
|
||||||
|
|
||||||
// Expires Setup Key expiration date
|
// Expires Setup Key expiration date
|
||||||
@@ -723,7 +728,7 @@ type SetupKey struct {
|
|||||||
|
|
||||||
// SetupKeyRequest defines model for SetupKeyRequest.
|
// SetupKeyRequest defines model for SetupKeyRequest.
|
||||||
type SetupKeyRequest struct {
|
type SetupKeyRequest struct {
|
||||||
// AutoGroups Setup key groups to auto-assign to peers registered with this key
|
// AutoGroups List of group IDs to auto-assign to peers registered with this key
|
||||||
AutoGroups []string `json:"auto_groups"`
|
AutoGroups []string `json:"auto_groups"`
|
||||||
|
|
||||||
// ExpiresIn Expiration time in seconds
|
// ExpiresIn Expiration time in seconds
|
||||||
@@ -762,6 +767,9 @@ type User struct {
|
|||||||
// IsServiceUser Is true if this user is a service user
|
// IsServiceUser Is true if this user is a service user
|
||||||
IsServiceUser *bool `json:"is_service_user,omitempty"`
|
IsServiceUser *bool `json:"is_service_user,omitempty"`
|
||||||
|
|
||||||
|
// LastLogin Last time this user performed a login to the dashboard
|
||||||
|
LastLogin *time.Time `json:"last_login,omitempty"`
|
||||||
|
|
||||||
// Name User's name from idp provider
|
// Name User's name from idp provider
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ var baseExistingNSGroup = &nbdns.NameServerGroup{
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Groups: []string{"testing"},
|
Groups: []string{"testing"},
|
||||||
|
Domains: []string{"domain"},
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,13 @@ func (h *SetupKeysHandler) CreateSetupKey(w http.ResponseWriter, r *http.Request
|
|||||||
|
|
||||||
expiresIn := time.Duration(req.ExpiresIn) * time.Second
|
expiresIn := time.Duration(req.ExpiresIn) * time.Second
|
||||||
|
|
||||||
|
day := time.Hour * 24
|
||||||
|
year := day * 365
|
||||||
|
if expiresIn < day || expiresIn > year {
|
||||||
|
util.WriteError(status.Errorf(status.InvalidArgument, "expiresIn should be between 1 day and 365 days"), w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if req.AutoGroups == nil {
|
if req.AutoGroups == nil {
|
||||||
req.AutoGroups = []string{}
|
req.AutoGroups = []string{}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ func TestSetupKeysHandlers(t *testing.T) {
|
|||||||
requestType: http.MethodPost,
|
requestType: http.MethodPost,
|
||||||
requestPath: "/api/setup-keys",
|
requestPath: "/api/setup-keys",
|
||||||
requestBody: bytes.NewBuffer(
|
requestBody: bytes.NewBuffer(
|
||||||
[]byte(fmt.Sprintf("{\"name\":\"%s\",\"type\":\"%s\"}", newSetupKey.Name, newSetupKey.Type))),
|
[]byte(fmt.Sprintf("{\"name\":\"%s\",\"type\":\"%s\",\"expires_in\":86400}", newSetupKey.Name, newSetupKey.Type))),
|
||||||
expectedStatus: http.StatusOK,
|
expectedStatus: http.StatusOK,
|
||||||
expectedBody: true,
|
expectedBody: true,
|
||||||
expectedSetupKey: toResponseBody(newSetupKey),
|
expectedSetupKey: toResponseBody(newSetupKey),
|
||||||
|
|||||||
@@ -270,5 +270,6 @@ func toUserResponse(user *server.UserInfo, currenUserID string) *api.User {
|
|||||||
IsCurrent: &isCurrent,
|
IsCurrent: &isCurrent,
|
||||||
IsServiceUser: &user.IsServiceUser,
|
IsServiceUser: &user.IsServiceUser,
|
||||||
IsBlocked: user.IsBlocked,
|
IsBlocked: user.IsBlocked,
|
||||||
|
LastLogin: &user.LastLogin,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -461,7 +461,7 @@ func (am *Auth0Manager) UpdateUserAppMetadata(userID string, appMetadata AppMeta
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildCreateUserRequestPayload(email string, name string, accountID string) (string, error) {
|
func buildCreateUserRequestPayload(email, name, accountID, invitedByEmail string) (string, error) {
|
||||||
invite := true
|
invite := true
|
||||||
req := &createUserRequest{
|
req := &createUserRequest{
|
||||||
Email: email,
|
Email: email,
|
||||||
@@ -469,6 +469,7 @@ func buildCreateUserRequestPayload(email string, name string, accountID string)
|
|||||||
AppMeta: AppMetadata{
|
AppMeta: AppMetadata{
|
||||||
WTAccountID: accountID,
|
WTAccountID: accountID,
|
||||||
WTPendingInvite: &invite,
|
WTPendingInvite: &invite,
|
||||||
|
WTInvitedBy: invitedByEmail,
|
||||||
},
|
},
|
||||||
Connection: "Username-Password-Authentication",
|
Connection: "Username-Password-Authentication",
|
||||||
Password: GeneratePassword(8, 1, 1, 1),
|
Password: GeneratePassword(8, 1, 1, 1),
|
||||||
@@ -634,9 +635,9 @@ func (am *Auth0Manager) GetUserByEmail(email string) ([]*UserData, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateUser creates a new user in Auth0 Idp and sends an invite
|
// CreateUser creates a new user in Auth0 Idp and sends an invite
|
||||||
func (am *Auth0Manager) CreateUser(email string, name string, accountID string) (*UserData, error) {
|
func (am *Auth0Manager) CreateUser(email, name, accountID, invitedByEmail string) (*UserData, error) {
|
||||||
|
|
||||||
payloadString, err := buildCreateUserRequestPayload(email, name, accountID)
|
payloadString, err := buildCreateUserRequestPayload(email, name, accountID, invitedByEmail)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -343,7 +343,7 @@ func TestAuth0_UpdateUserAppMetadata(t *testing.T) {
|
|||||||
updateUserAppMetadataTestCase2 := updateUserAppMetadataTest{
|
updateUserAppMetadataTestCase2 := updateUserAppMetadataTest{
|
||||||
name: "Bad Status Code",
|
name: "Bad Status Code",
|
||||||
inputReqBody: fmt.Sprintf("{\"access_token\":\"%s\",\"scope\":\"read:users\",\"expires_in\":%d,\"token_type\":\"Bearer\"}", token, exp),
|
inputReqBody: fmt.Sprintf("{\"access_token\":\"%s\",\"scope\":\"read:users\",\"expires_in\":%d,\"token_type\":\"Bearer\"}", token, exp),
|
||||||
expectedReqBody: fmt.Sprintf("{\"app_metadata\":{\"wt_account_id\":\"%s\",\"wt_pending_invite\":null}}", appMetadata.WTAccountID),
|
expectedReqBody: fmt.Sprintf("{\"app_metadata\":{\"wt_account_id\":\"%s\",\"wt_pending_invite\":null,\"wt_invited_by_email\":\"\"}}", appMetadata.WTAccountID),
|
||||||
appMetadata: appMetadata,
|
appMetadata: appMetadata,
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
helper: JsonParser{},
|
helper: JsonParser{},
|
||||||
@@ -366,7 +366,7 @@ func TestAuth0_UpdateUserAppMetadata(t *testing.T) {
|
|||||||
updateUserAppMetadataTestCase4 := updateUserAppMetadataTest{
|
updateUserAppMetadataTestCase4 := updateUserAppMetadataTest{
|
||||||
name: "Good request",
|
name: "Good request",
|
||||||
inputReqBody: fmt.Sprintf("{\"access_token\":\"%s\",\"scope\":\"read:users\",\"expires_in\":%d,\"token_type\":\"Bearer\"}", token, exp),
|
inputReqBody: fmt.Sprintf("{\"access_token\":\"%s\",\"scope\":\"read:users\",\"expires_in\":%d,\"token_type\":\"Bearer\"}", token, exp),
|
||||||
expectedReqBody: fmt.Sprintf("{\"app_metadata\":{\"wt_account_id\":\"%s\",\"wt_pending_invite\":null}}", appMetadata.WTAccountID),
|
expectedReqBody: fmt.Sprintf("{\"app_metadata\":{\"wt_account_id\":\"%s\",\"wt_pending_invite\":null,\"wt_invited_by_email\":\"\"}}", appMetadata.WTAccountID),
|
||||||
appMetadata: appMetadata,
|
appMetadata: appMetadata,
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
helper: JsonParser{},
|
helper: JsonParser{},
|
||||||
@@ -378,7 +378,7 @@ func TestAuth0_UpdateUserAppMetadata(t *testing.T) {
|
|||||||
updateUserAppMetadataTestCase5 := updateUserAppMetadataTest{
|
updateUserAppMetadataTestCase5 := updateUserAppMetadataTest{
|
||||||
name: "Update Pending Invite",
|
name: "Update Pending Invite",
|
||||||
inputReqBody: fmt.Sprintf("{\"access_token\":\"%s\",\"scope\":\"read:users\",\"expires_in\":%d,\"token_type\":\"Bearer\"}", token, exp),
|
inputReqBody: fmt.Sprintf("{\"access_token\":\"%s\",\"scope\":\"read:users\",\"expires_in\":%d,\"token_type\":\"Bearer\"}", token, exp),
|
||||||
expectedReqBody: fmt.Sprintf("{\"app_metadata\":{\"wt_account_id\":\"%s\",\"wt_pending_invite\":true}}", appMetadata.WTAccountID),
|
expectedReqBody: fmt.Sprintf("{\"app_metadata\":{\"wt_account_id\":\"%s\",\"wt_pending_invite\":true,\"wt_invited_by_email\":\"\"}}", appMetadata.WTAccountID),
|
||||||
appMetadata: AppMetadata{
|
appMetadata: AppMetadata{
|
||||||
WTAccountID: "ok",
|
WTAccountID: "ok",
|
||||||
WTPendingInvite: &invite,
|
WTPendingInvite: &invite,
|
||||||
|
|||||||
@@ -362,7 +362,7 @@ func (am *AuthentikManager) GetAllAccounts() (map[string][]*UserData, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateUser creates a new user in authentik Idp and sends an invitation.
|
// CreateUser creates a new user in authentik Idp and sends an invitation.
|
||||||
func (am *AuthentikManager) CreateUser(email string, name string, accountID string) (*UserData, error) {
|
func (am *AuthentikManager) CreateUser(email, name, accountID, invitedByEmail string) (*UserData, error) {
|
||||||
ctx, err := am.authenticationContext()
|
ctx, err := am.authenticationContext()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ func (ac *AzureCredentials) Authenticate() (JWTToken, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateUser creates a new user in azure AD Idp.
|
// CreateUser creates a new user in azure AD Idp.
|
||||||
func (am *AzureManager) CreateUser(email string, name string, accountID string) (*UserData, error) {
|
func (am *AzureManager) CreateUser(email, name, accountID, invitedByEmail string) (*UserData, error) {
|
||||||
payload, err := buildAzureCreateUserRequestPayload(email, name, accountID, am.ClientID)
|
payload, err := buildAzureCreateUserRequestPayload(email, name, accountID, am.ClientID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ func (gm *GoogleWorkspaceManager) GetAllAccounts() (map[string][]*UserData, erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateUser creates a new user in Google Workspace and sends an invitation.
|
// CreateUser creates a new user in Google Workspace and sends an invitation.
|
||||||
func (gm *GoogleWorkspaceManager) CreateUser(email string, name string, accountID string) (*UserData, error) {
|
func (gm *GoogleWorkspaceManager) CreateUser(email, name, accountID, invitedByEmail string) (*UserData, error) {
|
||||||
invite := true
|
invite := true
|
||||||
metadata := AppMetadata{
|
metadata := AppMetadata{
|
||||||
WTAccountID: accountID,
|
WTAccountID: accountID,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ type Manager interface {
|
|||||||
GetUserDataByID(userId string, appMetadata AppMetadata) (*UserData, error)
|
GetUserDataByID(userId string, appMetadata AppMetadata) (*UserData, error)
|
||||||
GetAccount(accountId string) ([]*UserData, error)
|
GetAccount(accountId string) ([]*UserData, error)
|
||||||
GetAllAccounts() (map[string][]*UserData, error)
|
GetAllAccounts() (map[string][]*UserData, error)
|
||||||
CreateUser(email string, name string, accountID string) (*UserData, error)
|
CreateUser(email, name, accountID, invitedByEmail string) (*UserData, error)
|
||||||
GetUserByEmail(email string) ([]*UserData, error)
|
GetUserByEmail(email string) ([]*UserData, error)
|
||||||
InviteUserByID(userID string) error
|
InviteUserByID(userID string) error
|
||||||
}
|
}
|
||||||
@@ -72,6 +72,7 @@ type AppMetadata struct {
|
|||||||
// maps to wt_account_id when json.marshal
|
// maps to wt_account_id when json.marshal
|
||||||
WTAccountID string `json:"wt_account_id,omitempty"`
|
WTAccountID string `json:"wt_account_id,omitempty"`
|
||||||
WTPendingInvite *bool `json:"wt_pending_invite"`
|
WTPendingInvite *bool `json:"wt_pending_invite"`
|
||||||
|
WTInvitedBy string `json:"wt_invited_by_email"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// JWTToken a JWT object that holds information of a token
|
// JWTToken a JWT object that holds information of a token
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ func (kc *KeycloakCredentials) Authenticate() (JWTToken, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateUser creates a new user in keycloak Idp and sends an invite.
|
// CreateUser creates a new user in keycloak Idp and sends an invite.
|
||||||
func (km *KeycloakManager) CreateUser(email string, name string, accountID string) (*UserData, error) {
|
func (km *KeycloakManager) CreateUser(email, name, accountID, invitedByEmail string) (*UserData, error) {
|
||||||
jwtToken, err := km.credentials.Authenticate()
|
jwtToken, err := km.credentials.Authenticate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ func (oc *OktaCredentials) Authenticate() (JWTToken, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateUser creates a new user in okta Idp and sends an invitation.
|
// CreateUser creates a new user in okta Idp and sends an invitation.
|
||||||
func (om *OktaManager) CreateUser(email string, name string, accountID string) (*UserData, error) {
|
func (om *OktaManager) CreateUser(email, name, accountID, invitedByEmail string) (*UserData, error) {
|
||||||
var (
|
var (
|
||||||
sendEmail = true
|
sendEmail = true
|
||||||
activate = true
|
activate = true
|
||||||
|
|||||||
@@ -234,7 +234,7 @@ func (zc *ZitadelCredentials) Authenticate() (JWTToken, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateUser creates a new user in zitadel Idp and sends an invite.
|
// CreateUser creates a new user in zitadel Idp and sends an invite.
|
||||||
func (zm *ZitadelManager) CreateUser(email string, name string, accountID string) (*UserData, error) {
|
func (zm *ZitadelManager) CreateUser(email, name, accountID, invitedByEmail string) (*UserData, error) {
|
||||||
payload, err := buildZitadelCreateUserRequestPayload(email, name)
|
payload, err := buildZitadelCreateUserRequestPayload(email, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package jwtclaims
|
package jwtclaims
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt"
|
"github.com/golang-jwt/jwt"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -10,6 +12,7 @@ type AuthorizationClaims struct {
|
|||||||
AccountId string
|
AccountId string
|
||||||
Domain string
|
Domain string
|
||||||
DomainCategory string
|
DomainCategory string
|
||||||
|
LastLogin time.Time
|
||||||
|
|
||||||
Raw jwt.MapClaims
|
Raw jwt.MapClaims
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package jwtclaims
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt"
|
"github.com/golang-jwt/jwt"
|
||||||
)
|
)
|
||||||
@@ -17,6 +18,8 @@ const (
|
|||||||
DomainCategorySuffix = "wt_account_domain_category"
|
DomainCategorySuffix = "wt_account_domain_category"
|
||||||
// UserIDClaim claim for the user id
|
// UserIDClaim claim for the user id
|
||||||
UserIDClaim = "sub"
|
UserIDClaim = "sub"
|
||||||
|
// LastLoginSuffix claim for the last login
|
||||||
|
LastLoginSuffix = "nb_last_login"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ExtractClaims Extract function type
|
// ExtractClaims Extract function type
|
||||||
@@ -93,9 +96,24 @@ func (c *ClaimsExtractor) FromToken(token *jwt.Token) AuthorizationClaims {
|
|||||||
if ok {
|
if ok {
|
||||||
jwtClaims.DomainCategory = domainCategoryClaim.(string)
|
jwtClaims.DomainCategory = domainCategoryClaim.(string)
|
||||||
}
|
}
|
||||||
|
LastLoginClaimString, ok := claims[c.authAudience+LastLoginSuffix]
|
||||||
|
if ok {
|
||||||
|
jwtClaims.LastLogin = parseTime(LastLoginClaimString.(string))
|
||||||
|
}
|
||||||
return jwtClaims
|
return jwtClaims
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseTime(timeString string) time.Time {
|
||||||
|
if timeString == "" {
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
parsedTime, err := time.Parse(time.RFC3339, timeString)
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
return parsedTime
|
||||||
|
}
|
||||||
|
|
||||||
// fromRequestContext extracts claims from the request context previously filled by the JWT token (after auth)
|
// fromRequestContext extracts claims from the request context previously filled by the JWT token (after auth)
|
||||||
func (c *ClaimsExtractor) fromRequestContext(r *http.Request) AuthorizationClaims {
|
func (c *ClaimsExtractor) fromRequestContext(r *http.Request) AuthorizationClaims {
|
||||||
if r.Context().Value(TokenUserProperty) == nil {
|
if r.Context().Value(TokenUserProperty) == nil {
|
||||||
|
|||||||
@@ -4,12 +4,15 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt"
|
"github.com/golang-jwt/jwt"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newTestRequestWithJWT(t *testing.T, claims AuthorizationClaims, audiance string) *http.Request {
|
func newTestRequestWithJWT(t *testing.T, claims AuthorizationClaims, audiance string) *http.Request {
|
||||||
|
const layout = "2006-01-02T15:04:05.999Z"
|
||||||
|
|
||||||
claimMaps := jwt.MapClaims{}
|
claimMaps := jwt.MapClaims{}
|
||||||
if claims.UserId != "" {
|
if claims.UserId != "" {
|
||||||
claimMaps[UserIDClaim] = claims.UserId
|
claimMaps[UserIDClaim] = claims.UserId
|
||||||
@@ -23,6 +26,9 @@ func newTestRequestWithJWT(t *testing.T, claims AuthorizationClaims, audiance st
|
|||||||
if claims.DomainCategory != "" {
|
if claims.DomainCategory != "" {
|
||||||
claimMaps[audiance+DomainCategorySuffix] = claims.DomainCategory
|
claimMaps[audiance+DomainCategorySuffix] = claims.DomainCategory
|
||||||
}
|
}
|
||||||
|
if claims.LastLogin != (time.Time{}) {
|
||||||
|
claimMaps[audiance+LastLoginSuffix] = claims.LastLogin.Format(layout)
|
||||||
|
}
|
||||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claimMaps)
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claimMaps)
|
||||||
r, err := http.NewRequest(http.MethodGet, "http://localhost", nil)
|
r, err := http.NewRequest(http.MethodGet, "http://localhost", nil)
|
||||||
require.NoError(t, err, "creating testing request failed")
|
require.NoError(t, err, "creating testing request failed")
|
||||||
@@ -40,6 +46,9 @@ func TestExtractClaimsFromRequestContext(t *testing.T) {
|
|||||||
expectedMSG string
|
expectedMSG string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const layout = "2006-01-02T15:04:05.999Z"
|
||||||
|
lastLogin, _ := time.Parse(layout, "2023-08-17T09:30:40.465Z")
|
||||||
|
|
||||||
testCase1 := test{
|
testCase1 := test{
|
||||||
name: "All Claim Fields",
|
name: "All Claim Fields",
|
||||||
inputAudiance: "https://login/",
|
inputAudiance: "https://login/",
|
||||||
@@ -47,11 +56,13 @@ func TestExtractClaimsFromRequestContext(t *testing.T) {
|
|||||||
UserId: "test",
|
UserId: "test",
|
||||||
Domain: "test.com",
|
Domain: "test.com",
|
||||||
AccountId: "testAcc",
|
AccountId: "testAcc",
|
||||||
|
LastLogin: lastLogin,
|
||||||
DomainCategory: "public",
|
DomainCategory: "public",
|
||||||
Raw: jwt.MapClaims{
|
Raw: jwt.MapClaims{
|
||||||
"https://login/wt_account_domain": "test.com",
|
"https://login/wt_account_domain": "test.com",
|
||||||
"https://login/wt_account_domain_category": "public",
|
"https://login/wt_account_domain_category": "public",
|
||||||
"https://login/wt_account_id": "testAcc",
|
"https://login/wt_account_id": "testAcc",
|
||||||
|
"https://login/nb_last_login": lastLogin.Format(layout),
|
||||||
"sub": "test",
|
"sub": "test",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
|
"github.com/rs/xid"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
nbdns "github.com/netbirdio/netbird/dns"
|
nbdns "github.com/netbirdio/netbird/dns"
|
||||||
"github.com/netbirdio/netbird/management/server/activity"
|
"github.com/netbirdio/netbird/management/server/activity"
|
||||||
"github.com/netbirdio/netbird/management/server/status"
|
"github.com/netbirdio/netbird/management/server/status"
|
||||||
"github.com/rs/xid"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"strconv"
|
|
||||||
"unicode/utf8"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -26,6 +30,8 @@ const (
|
|||||||
UpdateNameServerGroupPrimary
|
UpdateNameServerGroupPrimary
|
||||||
// UpdateNameServerGroupDomains indicates a nameserver group' domains update operation
|
// UpdateNameServerGroupDomains indicates a nameserver group' domains update operation
|
||||||
UpdateNameServerGroupDomains
|
UpdateNameServerGroupDomains
|
||||||
|
|
||||||
|
domainPattern = `^(?i)[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,}$`
|
||||||
)
|
)
|
||||||
|
|
||||||
// NameServerGroupUpdateOperationType operation type
|
// NameServerGroupUpdateOperationType operation type
|
||||||
@@ -364,9 +370,8 @@ func validateDomainInput(primary bool, domains []string) error {
|
|||||||
" you should set either primary or domain")
|
" you should set either primary or domain")
|
||||||
}
|
}
|
||||||
for _, domain := range domains {
|
for _, domain := range domains {
|
||||||
_, valid := dns.IsDomainName(domain)
|
if err := validateDomain(domain); err != nil {
|
||||||
if !valid {
|
return status.Errorf(status.InvalidArgument, "nameserver group got an invalid domain: %s %q", domain, err)
|
||||||
return status.Errorf(status.InvalidArgument, "nameserver group got an invalid domain: %s", domain)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -417,3 +422,21 @@ func validateGroups(list []string, groups map[string]*Group) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateDomain(domain string) error {
|
||||||
|
domainMatcher := regexp.MustCompile(domainPattern)
|
||||||
|
if !domainMatcher.MatchString(domain) {
|
||||||
|
return errors.New("domain should consists of only letters, numbers, and hyphens with no leading, trailing hyphens, or spaces")
|
||||||
|
}
|
||||||
|
|
||||||
|
labels, valid := dns.IsDomainName(domain)
|
||||||
|
if !valid {
|
||||||
|
return errors.New("invalid domain name")
|
||||||
|
}
|
||||||
|
|
||||||
|
if labels < 2 {
|
||||||
|
return errors.New("domain should consists of a minimum of two labels")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -1160,3 +1160,69 @@ func initTestNSAccount(t *testing.T, am *DefaultAccountManager) (*Account, error
|
|||||||
|
|
||||||
return account, nil
|
return account, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestValidateDomain(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
domain string
|
||||||
|
errFunc require.ErrorAssertionFunc
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Valid domain name with multiple labels",
|
||||||
|
domain: "123.example.com",
|
||||||
|
errFunc: require.NoError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Valid domain name with hyphen",
|
||||||
|
domain: "test-example.com",
|
||||||
|
errFunc: require.NoError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid domain name with double hyphen",
|
||||||
|
domain: "test--example.com",
|
||||||
|
errFunc: require.Error,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid domain name with only one label",
|
||||||
|
domain: "com",
|
||||||
|
errFunc: require.Error,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid domain name with a label exceeding 63 characters",
|
||||||
|
domain: "dnsdnsdnsdnsdnsdnsdnsdnsdnsdnsdnsdnsdnsdnsdnsdnsdnsdnsdnsdnsdnsdns.com",
|
||||||
|
errFunc: require.Error,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid domain name starting with a hyphen",
|
||||||
|
domain: "-example.com",
|
||||||
|
errFunc: require.Error,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid domain name ending with a hyphen",
|
||||||
|
domain: "example.com-",
|
||||||
|
errFunc: require.Error,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid domain with unicode",
|
||||||
|
domain: "example?,.com",
|
||||||
|
errFunc: require.Error,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid domain with space before top-level domain",
|
||||||
|
domain: "space .example.com",
|
||||||
|
errFunc: require.Error,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid domain with trailing space",
|
||||||
|
domain: "example.com ",
|
||||||
|
errFunc: require.Error,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
testCase.errFunc(t, validateDomain(testCase.domain))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -108,6 +108,10 @@ func (p *Peer) AddedWithSSOLogin() bool {
|
|||||||
|
|
||||||
// Copy copies Peer object
|
// Copy copies Peer object
|
||||||
func (p *Peer) Copy() *Peer {
|
func (p *Peer) Copy() *Peer {
|
||||||
|
peerStatus := p.Status
|
||||||
|
if peerStatus != nil {
|
||||||
|
peerStatus = p.Status.Copy()
|
||||||
|
}
|
||||||
return &Peer{
|
return &Peer{
|
||||||
ID: p.ID,
|
ID: p.ID,
|
||||||
Key: p.Key,
|
Key: p.Key,
|
||||||
@@ -115,11 +119,11 @@ func (p *Peer) Copy() *Peer {
|
|||||||
IP: p.IP,
|
IP: p.IP,
|
||||||
Meta: p.Meta,
|
Meta: p.Meta,
|
||||||
Name: p.Name,
|
Name: p.Name,
|
||||||
Status: p.Status,
|
DNSLabel: p.DNSLabel,
|
||||||
|
Status: peerStatus,
|
||||||
UserID: p.UserID,
|
UserID: p.UserID,
|
||||||
SSHKey: p.SSHKey,
|
SSHKey: p.SSHKey,
|
||||||
SSHEnabled: p.SSHEnabled,
|
SSHEnabled: p.SSHEnabled,
|
||||||
DNSLabel: p.DNSLabel,
|
|
||||||
LoginExpirationEnabled: p.LoginExpirationEnabled,
|
LoginExpirationEnabled: p.LoginExpirationEnabled,
|
||||||
LastLogin: p.LastLogin,
|
LastLogin: p.LastLogin,
|
||||||
}
|
}
|
||||||
@@ -695,6 +699,8 @@ func (am *DefaultAccountManager) LoginPeer(login PeerLogin) (*Peer, *NetworkMap,
|
|||||||
updatePeerLastLogin(peer, account)
|
updatePeerLastLogin(peer, account)
|
||||||
updateRemotePeers = true
|
updateRemotePeers = true
|
||||||
shouldStoreAccount = true
|
shouldStoreAccount = true
|
||||||
|
|
||||||
|
am.storeEvent(login.UserID, peer.ID, account.Id, activity.UserLoggedInPeer, peer.EventMeta(am.GetDNSDomain()))
|
||||||
}
|
}
|
||||||
|
|
||||||
peer, updated := updatePeerMeta(peer, login.Meta, account)
|
peer, updated := updatePeerMeta(peer, login.Meta, account)
|
||||||
|
|||||||
@@ -36,6 +36,18 @@ type PersonalAccessToken struct {
|
|||||||
LastUsed time.Time
|
LastUsed time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *PersonalAccessToken) Copy() *PersonalAccessToken {
|
||||||
|
return &PersonalAccessToken{
|
||||||
|
ID: t.ID,
|
||||||
|
Name: t.Name,
|
||||||
|
HashedToken: t.HashedToken,
|
||||||
|
ExpirationDate: t.ExpirationDate,
|
||||||
|
CreatedBy: t.CreatedBy,
|
||||||
|
CreatedAt: t.CreatedAt,
|
||||||
|
LastUsed: t.LastUsed,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// PersonalAccessTokenGenerated holds the new PersonalAccessToken and the plain text version of it
|
// PersonalAccessTokenGenerated holds the new PersonalAccessToken and the plain text version of it
|
||||||
type PersonalAccessTokenGenerated struct {
|
type PersonalAccessTokenGenerated struct {
|
||||||
PlainToken string
|
PlainToken string
|
||||||
|
|||||||
@@ -95,18 +95,22 @@ type PolicyRule struct {
|
|||||||
|
|
||||||
// Copy returns a copy of a policy rule
|
// Copy returns a copy of a policy rule
|
||||||
func (pm *PolicyRule) Copy() *PolicyRule {
|
func (pm *PolicyRule) Copy() *PolicyRule {
|
||||||
return &PolicyRule{
|
rule := &PolicyRule{
|
||||||
ID: pm.ID,
|
ID: pm.ID,
|
||||||
Name: pm.Name,
|
Name: pm.Name,
|
||||||
Description: pm.Description,
|
Description: pm.Description,
|
||||||
Enabled: pm.Enabled,
|
Enabled: pm.Enabled,
|
||||||
Action: pm.Action,
|
Action: pm.Action,
|
||||||
Destinations: pm.Destinations[:],
|
Destinations: make([]string, len(pm.Destinations)),
|
||||||
Sources: pm.Sources[:],
|
Sources: make([]string, len(pm.Sources)),
|
||||||
Bidirectional: pm.Bidirectional,
|
Bidirectional: pm.Bidirectional,
|
||||||
Protocol: pm.Protocol,
|
Protocol: pm.Protocol,
|
||||||
Ports: pm.Ports[:],
|
Ports: make([]string, len(pm.Ports)),
|
||||||
}
|
}
|
||||||
|
copy(rule.Destinations, pm.Destinations)
|
||||||
|
copy(rule.Sources, pm.Sources)
|
||||||
|
copy(rule.Ports, pm.Ports)
|
||||||
|
return rule
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToRule converts the PolicyRule to a legacy representation of the Rule (for backwards compatibility)
|
// ToRule converts the PolicyRule to a legacy representation of the Rule (for backwards compatibility)
|
||||||
@@ -147,9 +151,10 @@ func (p *Policy) Copy() *Policy {
|
|||||||
Name: p.Name,
|
Name: p.Name,
|
||||||
Description: p.Description,
|
Description: p.Description,
|
||||||
Enabled: p.Enabled,
|
Enabled: p.Enabled,
|
||||||
|
Rules: make([]*PolicyRule, len(p.Rules)),
|
||||||
}
|
}
|
||||||
for _, r := range p.Rules {
|
for i, r := range p.Rules {
|
||||||
c.Rules = append(c.Rules, r.Copy())
|
c.Rules[i] = r.Copy()
|
||||||
}
|
}
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,15 +45,18 @@ type Rule struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *Rule) Copy() *Rule {
|
func (r *Rule) Copy() *Rule {
|
||||||
return &Rule{
|
rule := &Rule{
|
||||||
ID: r.ID,
|
ID: r.ID,
|
||||||
Name: r.Name,
|
Name: r.Name,
|
||||||
Description: r.Description,
|
Description: r.Description,
|
||||||
Disabled: r.Disabled,
|
Disabled: r.Disabled,
|
||||||
Source: r.Source[:],
|
Source: make([]string, len(r.Source)),
|
||||||
Destination: r.Destination[:],
|
Destination: make([]string, len(r.Destination)),
|
||||||
Flow: r.Flow,
|
Flow: r.Flow,
|
||||||
}
|
}
|
||||||
|
copy(rule.Source, r.Source)
|
||||||
|
copy(rule.Destination, r.Destination)
|
||||||
|
return rule
|
||||||
}
|
}
|
||||||
|
|
||||||
// EventMeta returns activity event meta related to this rule
|
// EventMeta returns activity event meta related to this rule
|
||||||
|
|||||||
@@ -90,8 +90,8 @@ type SetupKey struct {
|
|||||||
|
|
||||||
// Copy copies SetupKey to a new object
|
// Copy copies SetupKey to a new object
|
||||||
func (key *SetupKey) Copy() *SetupKey {
|
func (key *SetupKey) Copy() *SetupKey {
|
||||||
autoGroups := make([]string, 0)
|
autoGroups := make([]string, len(key.AutoGroups))
|
||||||
autoGroups = append(autoGroups, key.AutoGroups...)
|
copy(autoGroups, key.AutoGroups)
|
||||||
if key.UpdatedAt.IsZero() {
|
if key.UpdatedAt.IsZero() {
|
||||||
key.UpdatedAt = key.CreatedAt
|
key.UpdatedAt = key.CreatedAt
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
type Store interface {
|
type Store interface {
|
||||||
GetAllAccounts() []*Account
|
GetAllAccounts() []*Account
|
||||||
GetAccount(accountID string) (*Account, error)
|
GetAccount(accountID string) (*Account, error)
|
||||||
@@ -20,6 +22,7 @@ type Store interface {
|
|||||||
// AcquireGlobalLock should attempt to acquire a global lock and return a function that releases the lock
|
// AcquireGlobalLock should attempt to acquire a global lock and return a function that releases the lock
|
||||||
AcquireGlobalLock() func()
|
AcquireGlobalLock() func()
|
||||||
SavePeerStatus(accountID, peerID string, status PeerStatus) error
|
SavePeerStatus(accountID, peerID string, status PeerStatus) error
|
||||||
|
SaveUserLastLogin(accountID, userID string, lastLogin time.Time) error
|
||||||
// Close should close the store persisting all unsaved data.
|
// Close should close the store persisting all unsaved data.
|
||||||
Close() error
|
Close() error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/netbirdio/netbird/management/proto"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/management/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
const channelBufferSize = 100
|
const channelBufferSize = 100
|
||||||
@@ -33,7 +35,7 @@ func (p *PeersUpdateManager) SendUpdate(peerID string, update *UpdateMessage) er
|
|||||||
if channel, ok := p.peerChannels[peerID]; ok {
|
if channel, ok := p.peerChannels[peerID]; ok {
|
||||||
select {
|
select {
|
||||||
case channel <- update:
|
case channel <- update:
|
||||||
log.Infof("update was sent to channel for peer %s", peerID)
|
log.Debugf("update was sent to channel for peer %s", peerID)
|
||||||
default:
|
default:
|
||||||
log.Warnf("channel for peer %s is %d full", peerID, len(channel))
|
log.Warnf("channel for peer %s is %d full", peerID, len(channel))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package server
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
@@ -53,6 +54,8 @@ type User struct {
|
|||||||
PATs map[string]*PersonalAccessToken
|
PATs map[string]*PersonalAccessToken
|
||||||
// Blocked indicates whether the user is blocked. Blocked users can't use the system.
|
// Blocked indicates whether the user is blocked. Blocked users can't use the system.
|
||||||
Blocked bool
|
Blocked bool
|
||||||
|
// LastLogin is the last time the user logged in to IdP
|
||||||
|
LastLogin time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsBlocked returns true if the user is blocked, false otherwise
|
// IsBlocked returns true if the user is blocked, false otherwise
|
||||||
@@ -60,6 +63,10 @@ func (u *User) IsBlocked() bool {
|
|||||||
return u.Blocked
|
return u.Blocked
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *User) LastDashboardLoginChanged(LastLogin time.Time) bool {
|
||||||
|
return LastLogin.After(u.LastLogin) && !u.LastLogin.IsZero()
|
||||||
|
}
|
||||||
|
|
||||||
// IsAdmin returns true if the user is an admin, false otherwise
|
// IsAdmin returns true if the user is an admin, false otherwise
|
||||||
func (u *User) IsAdmin() bool {
|
func (u *User) IsAdmin() bool {
|
||||||
return u.Role == UserRoleAdmin
|
return u.Role == UserRoleAdmin
|
||||||
@@ -82,6 +89,7 @@ func (u *User) ToUserInfo(userData *idp.UserData) (*UserInfo, error) {
|
|||||||
Status: string(UserStatusActive),
|
Status: string(UserStatusActive),
|
||||||
IsServiceUser: u.IsServiceUser,
|
IsServiceUser: u.IsServiceUser,
|
||||||
IsBlocked: u.Blocked,
|
IsBlocked: u.Blocked,
|
||||||
|
LastLogin: u.LastLogin,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
if userData.ID != u.Id {
|
if userData.ID != u.Id {
|
||||||
@@ -102,6 +110,7 @@ func (u *User) ToUserInfo(userData *idp.UserData) (*UserInfo, error) {
|
|||||||
Status: string(userStatus),
|
Status: string(userStatus),
|
||||||
IsServiceUser: u.IsServiceUser,
|
IsServiceUser: u.IsServiceUser,
|
||||||
IsBlocked: u.Blocked,
|
IsBlocked: u.Blocked,
|
||||||
|
LastLogin: u.LastLogin,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,9 +120,7 @@ func (u *User) Copy() *User {
|
|||||||
copy(autoGroups, u.AutoGroups)
|
copy(autoGroups, u.AutoGroups)
|
||||||
pats := make(map[string]*PersonalAccessToken, len(u.PATs))
|
pats := make(map[string]*PersonalAccessToken, len(u.PATs))
|
||||||
for k, v := range u.PATs {
|
for k, v := range u.PATs {
|
||||||
patCopy := new(PersonalAccessToken)
|
pats[k] = v.Copy()
|
||||||
*patCopy = *v
|
|
||||||
pats[k] = patCopy
|
|
||||||
}
|
}
|
||||||
return &User{
|
return &User{
|
||||||
Id: u.Id,
|
Id: u.Id,
|
||||||
@@ -123,6 +130,7 @@ func (u *User) Copy() *User {
|
|||||||
ServiceUserName: u.ServiceUserName,
|
ServiceUserName: u.ServiceUserName,
|
||||||
PATs: pats,
|
PATs: pats,
|
||||||
Blocked: u.Blocked,
|
Blocked: u.Blocked,
|
||||||
|
LastLogin: u.LastLogin,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,6 +194,7 @@ func (am *DefaultAccountManager) createServiceUser(accountID string, initiatorUs
|
|||||||
AutoGroups: newUser.AutoGroups,
|
AutoGroups: newUser.AutoGroups,
|
||||||
Status: string(UserStatusActive),
|
Status: string(UserStatusActive),
|
||||||
IsServiceUser: true,
|
IsServiceUser: true,
|
||||||
|
LastLogin: time.Time{},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,6 +224,12 @@ func (am *DefaultAccountManager) inviteNewUser(accountID, userID string, invite
|
|||||||
return nil, status.Errorf(status.NotFound, "account %s doesn't exist", accountID)
|
return nil, status.Errorf(status.NotFound, "account %s doesn't exist", accountID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// initiator is the one who is inviting the new user
|
||||||
|
initiatorUser, err := am.lookupUserInCache(userID, account)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(status.NotFound, "user %s doesn't exist in IdP", userID)
|
||||||
|
}
|
||||||
|
|
||||||
// check if the user is already registered with this email => reject
|
// check if the user is already registered with this email => reject
|
||||||
user, err := am.lookupUserInCacheByEmail(invite.Email, accountID)
|
user, err := am.lookupUserInCacheByEmail(invite.Email, accountID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -234,7 +249,7 @@ func (am *DefaultAccountManager) inviteNewUser(accountID, userID string, invite
|
|||||||
return nil, status.Errorf(status.UserAlreadyExists, "can't invite a user with an existing NetBird account")
|
return nil, status.Errorf(status.UserAlreadyExists, "can't invite a user with an existing NetBird account")
|
||||||
}
|
}
|
||||||
|
|
||||||
idpUser, err := am.idpManager.CreateUser(invite.Email, invite.Name, accountID)
|
idpUser, err := am.idpManager.CreateUser(invite.Email, invite.Name, accountID, initiatorUser.Email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -260,7 +275,6 @@ func (am *DefaultAccountManager) inviteNewUser(accountID, userID string, invite
|
|||||||
am.storeEvent(userID, newUser.Id, accountID, activity.UserInvited, nil)
|
am.storeEvent(userID, newUser.Id, accountID, activity.UserInvited, nil)
|
||||||
|
|
||||||
return newUser.ToUserInfo(idpUser)
|
return newUser.ToUserInfo(idpUser)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUser looks up a user by provided authorization claims.
|
// GetUser looks up a user by provided authorization claims.
|
||||||
@@ -275,6 +289,21 @@ func (am *DefaultAccountManager) GetUser(claims jwtclaims.AuthorizationClaims) (
|
|||||||
if !ok {
|
if !ok {
|
||||||
return nil, status.Errorf(status.NotFound, "user not found")
|
return nil, status.Errorf(status.NotFound, "user not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// this code should be outside of the am.GetAccountFromToken(claims) because this method is called also by the gRPC
|
||||||
|
// server when user authenticates a device. And we need to separate the Dashboard login event from the Device login event.
|
||||||
|
unlock := am.Store.AcquireAccountLock(account.Id)
|
||||||
|
newLogin := user.LastDashboardLoginChanged(claims.LastLogin)
|
||||||
|
err = am.Store.SaveUserLastLogin(account.Id, claims.UserId, claims.LastLogin)
|
||||||
|
unlock()
|
||||||
|
if newLogin {
|
||||||
|
meta := map[string]any{"timestamp": claims.LastLogin}
|
||||||
|
am.storeEvent(claims.UserId, claims.UserId, account.Id, activity.DashboardLogin, meta)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed saving user last login: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -600,10 +629,26 @@ func (am *DefaultAccountManager) SaveUser(accountID, initiatorUserID string, upd
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if update.AutoGroups != nil && account.Settings.GroupsPropagationEnabled {
|
||||||
|
removedGroups := difference(oldUser.AutoGroups, update.AutoGroups)
|
||||||
|
// need force update all auto groups in any case they will not be dublicated
|
||||||
|
account.UserGroupsAddToPeers(oldUser.Id, update.AutoGroups...)
|
||||||
|
account.UserGroupsRemoveFromPeers(oldUser.Id, removedGroups...)
|
||||||
|
|
||||||
|
account.Network.IncSerial()
|
||||||
if err = am.Store.SaveAccount(account); err != nil {
|
if err = am.Store.SaveAccount(account); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := am.updateAccountPeers(account); err != nil {
|
||||||
|
log.Errorf("failed updating account peers while updating user %s", accountID)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err = am.Store.SaveAccount(account); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if oldUser.IsBlocked() != update.IsBlocked() {
|
if oldUser.IsBlocked() != update.IsBlocked() {
|
||||||
if update.IsBlocked() {
|
if update.IsBlocked() {
|
||||||
@@ -629,7 +674,6 @@ func (am *DefaultAccountManager) SaveUser(accountID, initiatorUserID string, upd
|
|||||||
} else {
|
} else {
|
||||||
log.Errorf("group %s not found while saving user activity event of account %s", g, account.Id)
|
log.Errorf("group %s not found while saving user activity event of account %s", g, account.Id)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, g := range addedGroups {
|
for _, g := range addedGroups {
|
||||||
@@ -640,7 +684,6 @@ func (am *DefaultAccountManager) SaveUser(accountID, initiatorUserID string, upd
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if !isNil(am.idpManager) && !newUser.IsServiceUser {
|
if !isNil(am.idpManager) && !newUser.IsServiceUser {
|
||||||
|
|||||||
@@ -267,6 +267,7 @@ func TestUser_Copy(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Blocked: false,
|
Blocked: false,
|
||||||
|
LastLogin: time.Now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
err := validateStruct(user)
|
err := validateStruct(user)
|
||||||
|
|||||||
@@ -30,7 +30,15 @@ download_release_binary() {
|
|||||||
BINARY_BASE_NAME="${VERSION#v}_${OS_TYPE}_${ARCH}_signed.zip"
|
BINARY_BASE_NAME="${VERSION#v}_${OS_TYPE}_${ARCH}_signed.zip"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ "$1" = "$UI_APP" ]; then
|
||||||
|
BINARY_NAME="$1-${OS_TYPE}_${BINARY_BASE_NAME}"
|
||||||
|
if [ "$OS_TYPE" = "darwin" ]; then
|
||||||
BINARY_NAME="$1_${BINARY_BASE_NAME}"
|
BINARY_NAME="$1_${BINARY_BASE_NAME}"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
BINARY_NAME="$1_${BINARY_BASE_NAME}"
|
||||||
|
fi
|
||||||
|
|
||||||
DOWNLOAD_URL="${BASE_URL}/${VERSION}/${BINARY_NAME}"
|
DOWNLOAD_URL="${BASE_URL}/${VERSION}/${BINARY_NAME}"
|
||||||
|
|
||||||
echo "Installing $1 from $DOWNLOAD_URL"
|
echo "Installing $1 from $DOWNLOAD_URL"
|
||||||
@@ -128,6 +136,14 @@ install_native_binaries() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
check_use_bin_variable() {
|
||||||
|
if [ "${USE_BIN_INSTALL}-x" = "true-x" ]; then
|
||||||
|
echo "The installation will be performed using binary files"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
install_netbird() {
|
install_netbird() {
|
||||||
# Check if netbird CLI is installed
|
# Check if netbird CLI is installed
|
||||||
if [ -x "$(command -v netbird)" ]; then
|
if [ -x "$(command -v netbird)" ]; then
|
||||||
@@ -170,8 +186,10 @@ install_netbird() {
|
|||||||
echo "Netbird UI installation will be omitted as Linux does not run desktop environment"
|
echo "Netbird UI installation will be omitted as Linux does not run desktop environment"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check the availability of a compactible package manager
|
# Check the availability of a compatible package manager
|
||||||
if [ -x "$(command -v apt)" ]; then
|
if check_use_bin_variable; then
|
||||||
|
PACKAGE_MANAGER="bin"
|
||||||
|
elif [ -x "$(command -v apt)" ]; then
|
||||||
PACKAGE_MANAGER="apt"
|
PACKAGE_MANAGER="apt"
|
||||||
echo "The installation will be performed using apt package manager"
|
echo "The installation will be performed using apt package manager"
|
||||||
elif [ -x "$(command -v dnf)" ]; then
|
elif [ -x "$(command -v dnf)" ]; then
|
||||||
@@ -191,7 +209,9 @@ install_netbird() {
|
|||||||
INSTALL_DIR="/usr/local/bin"
|
INSTALL_DIR="/usr/local/bin"
|
||||||
|
|
||||||
# Check the availability of a compatible package manager
|
# Check the availability of a compatible package manager
|
||||||
if [ -x "$(command -v brew)" ]; then
|
if check_use_bin_variable; then
|
||||||
|
PACKAGE_MANAGER="bin"
|
||||||
|
elif [ -x "$(command -v brew)" ]; then
|
||||||
PACKAGE_MANAGER="brew"
|
PACKAGE_MANAGER="brew"
|
||||||
echo "The installation will be performed using brew package manager"
|
echo "The installation will be performed using brew package manager"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
package route
|
package route
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/netbirdio/netbird/management/server/status"
|
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/management/server/status"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Windows has some limitation regarding metric size that differ from Unix-like systems.
|
// Windows has some limitation regarding metric size that differ from Unix-like systems.
|
||||||
@@ -83,7 +84,7 @@ func (r *Route) EventMeta() map[string]any {
|
|||||||
|
|
||||||
// Copy copies a route object
|
// Copy copies a route object
|
||||||
func (r *Route) Copy() *Route {
|
func (r *Route) Copy() *Route {
|
||||||
return &Route{
|
route := &Route{
|
||||||
ID: r.ID,
|
ID: r.ID,
|
||||||
Description: r.Description,
|
Description: r.Description,
|
||||||
NetID: r.NetID,
|
NetID: r.NetID,
|
||||||
@@ -93,8 +94,10 @@ func (r *Route) Copy() *Route {
|
|||||||
Metric: r.Metric,
|
Metric: r.Metric,
|
||||||
Masquerade: r.Masquerade,
|
Masquerade: r.Masquerade,
|
||||||
Enabled: r.Enabled,
|
Enabled: r.Enabled,
|
||||||
Groups: r.Groups,
|
Groups: make([]string, len(r.Groups)),
|
||||||
}
|
}
|
||||||
|
copy(route.Groups, r.Groups)
|
||||||
|
return route
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsEqual compares one route with the other
|
// IsEqual compares one route with the other
|
||||||
|
|||||||
Reference in New Issue
Block a user