Compare commits

..

1 Commits

Author SHA1 Message Date
Christoph Haas
08b28340cc feat: add live traffic stats (#530) 2026-01-25 00:32:59 +01:00
64 changed files with 2064 additions and 3112 deletions

View File

@@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ github.event_name == 'pull_request' }} if: ${{ github.event_name == 'pull_request' }}
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
fetch-depth: 0 fetch-depth: 0
@@ -35,7 +35,7 @@ jobs:
# ct lint requires Python 3.x to run following packages: # ct lint requires Python 3.x to run following packages:
# - yamale (https://github.com/23andMe/Yamale) # - yamale (https://github.com/23andMe/Yamale)
# - yamllint (https://github.com/adrienverge/yamllint) # - yamllint (https://github.com/adrienverge/yamllint)
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with: with:
python-version: '3.x' python-version: '3.x'
@@ -44,7 +44,7 @@ jobs:
- name: Run chart-testing (lint) - name: Run chart-testing (lint)
run: ct lint --config ct.yaml run: ct lint --config ct.yaml
- uses: nolar/setup-k3d-k3s@8bf8d22160e8b1d184dcb780e390d6952a7eec65 # v1.0.10 - uses: nolar/setup-k3d-k3s@293b8e5822a20bc0d5bcdd4826f1a665e72aba96 # v1.0.9
with: with:
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
@@ -60,9 +60,9 @@ jobs:
permissions: permissions:
packages: write packages: write
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}

View File

@@ -18,13 +18,13 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out the repo - name: Check out the repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Get Version - name: Get Version
shell: bash shell: bash
@@ -32,14 +32,14 @@ jobs:
- name: Login to Docker Hub - name: Login to Docker Hub
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with: with:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
@@ -47,7 +47,7 @@ jobs:
- name: Extract metadata (tags, labels) for Docker - name: Extract metadata (tags, labels) for Docker
id: meta id: meta
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
with: with:
images: | images: |
wgportal/wg-portal wgportal/wg-portal
@@ -68,7 +68,7 @@ jobs:
type=semver,pattern=v{{major}} type=semver,pattern=v{{major}}
- name: Build and push Docker image - name: Build and push Docker image
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with: with:
context: . context: .
push: ${{ github.event_name != 'pull_request' }} push: ${{ github.event_name != 'pull_request' }}
@@ -80,7 +80,7 @@ jobs:
BUILD_VERSION=${{ env.BUILD_VERSION }} BUILD_VERSION=${{ env.BUILD_VERSION }}
- name: Export binaries from images - name: Export binaries from images
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with: with:
context: . context: .
platforms: linux/amd64,linux/arm64,linux/arm/v7 platforms: linux/amd64,linux/arm64,linux/arm/v7
@@ -96,7 +96,7 @@ jobs:
done done
- name: Upload binaries - name: Upload binaries
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with: with:
name: binaries name: binaries
path: binaries/wg-portal_linux* path: binaries/wg-portal_linux*
@@ -110,12 +110,12 @@ jobs:
contents: write contents: write
steps: steps:
- name: Download binaries - name: Download binaries
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with: with:
name: binaries name: binaries
- name: Create GitHub Release - name: Create GitHub Release
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1 uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
with: with:
files: 'wg-portal_linux*' files: 'wg-portal_linux*'
generate_release_notes: true generate_release_notes: true

View File

@@ -15,11 +15,11 @@ jobs:
deploy: deploy:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
fetch-depth: 0 fetch-depth: 0
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with: with:
python-version: 3.x python-version: 3.x

View File

@@ -20,7 +20,7 @@ RUN npm run build
###### ######
# Build backend # Build backend
###### ######
FROM --platform=${BUILDPLATFORM} golang:1.26-alpine AS builder FROM --platform=${BUILDPLATFORM} golang:1.25-alpine AS builder
# Set the working directory # Set the working directory
WORKDIR /build WORKDIR /build
# Download dependencies # Download dependencies

View File

@@ -80,7 +80,7 @@ func main() {
internal.AssertNoError(err) internal.AssertNoError(err)
auditRecorder.StartBackgroundJobs(ctx) auditRecorder.StartBackgroundJobs(ctx)
userManager, err := users.NewUserManager(cfg, eventBus, database, database, database) userManager, err := users.NewUserManager(cfg, eventBus, database, database)
internal.AssertNoError(err) internal.AssertNoError(err)
userManager.StartBackgroundJobs(ctx) userManager.StartBackgroundJobs(ctx)

View File

@@ -16,7 +16,7 @@ annotations:
# This is the chart version. This version number should be incremented each time you make changes # This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version. # to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/) # Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.7.3 version: 0.7.2
# This is the version number of the application being deployed. This version number should be # This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to # incremented each time you make changes to the application. Versions are not expected to

View File

@@ -1,6 +1,6 @@
# wg-portal # wg-portal
![Version: 0.7.3](https://img.shields.io/badge/Version-0.7.3-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v2](https://img.shields.io/badge/AppVersion-v2-informational?style=flat-square) ![Version: 0.7.2](https://img.shields.io/badge/Version-0.7.2-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v2](https://img.shields.io/badge/AppVersion-v2-informational?style=flat-square)
WireGuard Configuration Portal with LDAP, OAuth, OIDC authentication WireGuard Configuration Portal with LDAP, OAuth, OIDC authentication
@@ -41,7 +41,6 @@ The [Values](#values) section lists the parameters that can be configured during
| config.web | tpl/object | `{}` | [Web configuration](https://wgportal.org/latest/documentation/configuration/overview/#web) options.<br> `listening_address` will be set automatically from `service.web.port`. `external_url` is required to enable ingress and certificate resources. | | config.web | tpl/object | `{}` | [Web configuration](https://wgportal.org/latest/documentation/configuration/overview/#web) options.<br> `listening_address` will be set automatically from `service.web.port`. `external_url` is required to enable ingress and certificate resources. |
| revisionHistoryLimit | string | `10` | The number of old ReplicaSets to retain to allow rollback. | | revisionHistoryLimit | string | `10` | The number of old ReplicaSets to retain to allow rollback. |
| workloadType | string | `"Deployment"` | Workload type - `Deployment` or `StatefulSet` | | workloadType | string | `"Deployment"` | Workload type - `Deployment` or `StatefulSet` |
| replicas | int | `1` | The replicas for the workload. |
| strategy | object | `{"type":"RollingUpdate"}` | Update strategy for the workload Valid values are: `RollingUpdate` or `Recreate` for Deployment, `RollingUpdate` or `OnDelete` for StatefulSet | | strategy | object | `{"type":"RollingUpdate"}` | Update strategy for the workload Valid values are: `RollingUpdate` or `Recreate` for Deployment, `RollingUpdate` or `OnDelete` for StatefulSet |
| image.repository | string | `"ghcr.io/h44z/wg-portal"` | Image repository | | image.repository | string | `"ghcr.io/h44z/wg-portal"` | Image repository |
| image.pullPolicy | string | `"IfNotPresent"` | Image pull policy | | image.pullPolicy | string | `"IfNotPresent"` | Image pull policy |
@@ -75,15 +74,12 @@ The [Values](#values) section lists the parameters that can be configured during
| service.web.type | string | `"ClusterIP"` | Web service type | | service.web.type | string | `"ClusterIP"` | Web service type |
| service.web.port | int | `8888` | Web service port Used for the web interface listener | | service.web.port | int | `8888` | Web service port Used for the web interface listener |
| service.web.appProtocol | string | `"http"` | Web service appProtocol. Will be auto set to `https` if certificate is enabled. | | service.web.appProtocol | string | `"http"` | Web service appProtocol. Will be auto set to `https` if certificate is enabled. |
| service.web.extraSelectorLabels | object | `{}` | Extra labels to append to the selector labels. |
| service.wireguard.annotations | object | `{}` | Annotations for the WireGuard service | | service.wireguard.annotations | object | `{}` | Annotations for the WireGuard service |
| service.wireguard.type | string | `"LoadBalancer"` | Wireguard service type | | service.wireguard.type | string | `"LoadBalancer"` | Wireguard service type |
| service.wireguard.ports | list | `[51820]` | Wireguard service ports. Exposes the WireGuard ports for created interfaces. Lowerest port is selected as start port for the first interface. Increment next port by 1 for each additional interface. | | service.wireguard.ports | list | `[51820]` | Wireguard service ports. Exposes the WireGuard ports for created interfaces. Lowerest port is selected as start port for the first interface. Increment next port by 1 for each additional interface. |
| service.wireguard.extraSelectorLabels | object | `{}` | Extra labels to append to the selector labels. |
| service.metrics.port | int | `8787` | | | service.metrics.port | int | `8787` | |
| ingress.enabled | bool | `false` | Specifies whether an ingress resource should be created | | ingress.enabled | bool | `false` | Specifies whether an ingress resource should be created |
| ingress.className | string | `""` | Ingress class name | | ingress.className | string | `""` | Ingress class name |
| ingress.pathType | string | `"ImplementationSpecific"` | Ingress pathType value. Valid values are `ImplementationSpecific`, `Exact` or `Prefix`. |
| ingress.annotations | object | `{}` | Ingress annotations | | ingress.annotations | object | `{}` | Ingress annotations |
| ingress.tls | bool | `false` | Ingress TLS configuration. Enable certificate resource or add ingress annotation to create required secret | | ingress.tls | bool | `false` | Ingress TLS configuration. Enable certificate resource or add ingress annotation to create required secret |
| certificate.enabled | bool | `false` | Specifies whether a certificate resource should be created. If enabled, certificate will be used for the web. | | certificate.enabled | bool | `false` | Specifies whether a certificate resource should be created. If enabled, certificate will be used for the web. |

View File

@@ -49,7 +49,7 @@ spec:
{{- with .scope.type }} {{- with .scope.type }}
type: {{ . }} type: {{ . }}
{{- end }} {{- end }}
selector: {{- include "wg-portal.util.merge" (list .context .scope.extraSelectorLabels "wg-portal.selectorLabels") | nindent 4 }} selector: {{- include "wg-portal.selectorLabels" .context | nindent 4 }}
{{- end -}} {{- end -}}
{{/* {{/*

View File

@@ -8,9 +8,6 @@ spec:
{{- with .Values.revisionHistoryLimit }} {{- with .Values.revisionHistoryLimit }}
revisionHistoryLimit: {{ . }} revisionHistoryLimit: {{ . }}
{{- end }} {{- end }}
{{- with .Values.replicas }}
replicas: {{ . }}
{{- end }}
{{- with .Values.strategy }} {{- with .Values.strategy }}
strategy: {{- toYaml . | nindent 4 }} strategy: {{- toYaml . | nindent 4 }}
{{- end }} {{- end }}

View File

@@ -15,7 +15,7 @@ spec:
http: http:
paths: paths:
- path: {{ default "/" (urlParse (tpl .Values.config.web.external_url .)).path }} - path: {{ default "/" (urlParse (tpl .Values.config.web.external_url .)).path }}
pathType: {{ default "ImplementationSpecific" .Values.ingress.pathType }} pathType: {{ default "ImplementationSpecific" .pathType }}
backend: backend:
service: service:
name: {{ include "wg-portal.fullname" . }} name: {{ include "wg-portal.fullname" . }}

View File

@@ -8,9 +8,6 @@ spec:
{{- with .Values.revisionHistoryLimit }} {{- with .Values.revisionHistoryLimit }}
revisionHistoryLimit: {{ . }} revisionHistoryLimit: {{ . }}
{{- end }} {{- end }}
{{- with .Values.replicas }}
replicas: {{ . }}
{{- end }}
{{- with .Values.strategy }} {{- with .Values.strategy }}
updateStrategy: {{- toYaml . | nindent 4 }} updateStrategy: {{- toYaml . | nindent 4 }}
{{- end }} {{- end }}

View File

@@ -35,9 +35,6 @@ config:
revisionHistoryLimit: "" revisionHistoryLimit: ""
# -- Workload type - `Deployment` or `StatefulSet` # -- Workload type - `Deployment` or `StatefulSet`
workloadType: Deployment workloadType: Deployment
# -- The replicas for the workload.
# @default -- `1`
replicas: 1
# -- Update strategy for the workload # -- Update strategy for the workload
# Valid values are: # Valid values are:
# `RollingUpdate` or `Recreate` for Deployment, # `RollingUpdate` or `Recreate` for Deployment,
@@ -127,8 +124,6 @@ service:
port: 8888 port: 8888
# -- Web service appProtocol. Will be auto set to `https` if certificate is enabled. # -- Web service appProtocol. Will be auto set to `https` if certificate is enabled.
appProtocol: http appProtocol: http
# -- Extra labels to append to the selector labels.
extraSelectorLabels: {}
wireguard: wireguard:
# -- Annotations for the WireGuard service # -- Annotations for the WireGuard service
annotations: {} annotations: {}
@@ -140,8 +135,6 @@ service:
# Increment next port by 1 for each additional interface. # Increment next port by 1 for each additional interface.
ports: ports:
- 51820 - 51820
# -- Extra labels to append to the selector labels.
extraSelectorLabels: {}
metrics: metrics:
port: 8787 port: 8787
@@ -150,10 +143,6 @@ ingress:
enabled: false enabled: false
# -- Ingress class name # -- Ingress class name
className: "" className: ""
# -- Ingress pathType value.
# Valid values are `ImplementationSpecific`, `Exact` or `Prefix`.
# @default -- `"ImplementationSpecific"`
pathType: "ImplementationSpecific"
# -- Ingress annotations # -- Ingress annotations
annotations: {} annotations: {}
# -- Ingress TLS configuration. # -- Ingress TLS configuration.

View File

@@ -86,9 +86,6 @@ auth:
memberof: memberOf memberof: memberOf
admin_group: CN=WireGuardAdmins,OU=Some-OU,DC=COMPANY,DC=LOCAL admin_group: CN=WireGuardAdmins,OU=Some-OU,DC=COMPANY,DC=LOCAL
registration_enabled: true registration_enabled: true
# Restrict interface access based on LDAP filters
interface_filter:
wg0: "(memberOf=CN=VPNUsers,OU=Groups,DC=COMPANY,DC=LOCAL)"
log_user_info: true log_user_info: true
``` ```

View File

@@ -28,7 +28,6 @@ core:
backend: backend:
default: local default: local
rekey_timeout_interval: 125s
local_resolvconf_prefix: tun. local_resolvconf_prefix: tun.
advanced: advanced:
@@ -204,13 +203,6 @@ The current MikroTik backend is in **BETA** and may not support all features.
- **Description:** The default backend to use for managing WireGuard interfaces. - **Description:** The default backend to use for managing WireGuard interfaces.
Valid options are: `local`, or other backend id's configured in the `mikrotik` section. Valid options are: `local`, or other backend id's configured in the `mikrotik` section.
### `rekey_timeout_interval`
- **Default:** `180s`
- **Environment Variable:** `WG_PORTAL_BACKEND_REKEY_TIMEOUT_INTERVAL`
- **Description:** The interval after which a WireGuard peer is considered disconnected if no handshake updates are received.
This corresponds to the WireGuard rekey timeout setting of 120 seconds plus a 60-second buffer to account for latency or retry handling.
Uses Go duration format (e.g., `10s`, `1m`). If omitted, a default of 180 seconds is used.
### `local_resolvconf_prefix` ### `local_resolvconf_prefix`
- **Default:** `tun.` - **Default:** `tun.`
- **Environment Variable:** `WG_PORTAL_BACKEND_LOCAL_RESOLVCONF_PREFIX` - **Environment Variable:** `WG_PORTAL_BACKEND_LOCAL_RESOLVCONF_PREFIX`
@@ -742,16 +734,6 @@ Below are the properties for each LDAP provider entry inside `auth.ldap`:
- **Important**: The `login_filter` must always be a valid LDAP filter. It should at most return one user. - **Important**: The `login_filter` must always be a valid LDAP filter. It should at most return one user.
If the filter returns multiple or no users, the login will fail. If the filter returns multiple or no users, the login will fail.
#### `interface_filter`
- **Default:** *(empty)*
- **Description:** A map of LDAP filters to restrict access to specific WireGuard interfaces. The map keys are the interface identifiers (e.g., `wg0`), and the values are LDAP filters. Only users matching the filter will be allowed to provision peers for the respective interface.
For example:
```yaml
interface_filter:
wg0: "(memberOf=CN=VPNUsers,OU=Groups,DC=COMPANY,DC=LOCAL)"
wg1: "(description=special-access)"
```
#### `admin_group` #### `admin_group`
- **Default:** *(empty)* - **Default:** *(empty)*
- **Description:** A specific LDAP group whose members are considered administrators in WireGuard Portal. - **Description:** A specific LDAP group whose members are considered administrators in WireGuard Portal.

View File

@@ -35,14 +35,6 @@ WireGuard Portal supports managing WireGuard interfaces through three distinct d
> :warning: If host networking is used, the WireGuard Portal UI will be accessible on all the host's IP addresses if the listening address is set to `:8888` in the configuration file. > :warning: If host networking is used, the WireGuard Portal UI will be accessible on all the host's IP addresses if the listening address is set to `:8888` in the configuration file.
To avoid this, you can bind the listening address to a specific IP address, for example, the loopback address (`127.0.0.1:8888`). It is also possible to deploy firewall rules to restrict access to the WireGuard Portal UI. To avoid this, you can bind the listening address to a specific IP address, for example, the loopback address (`127.0.0.1:8888`). It is also possible to deploy firewall rules to restrict access to the WireGuard Portal UI.
> :warning: If the host is running **systemd-networkd**, routes managed by WireGuard Portal may be removed whenever systemd-networkd restarts, as it will clean up routes it considers "foreign". To prevent this, add the following to your host's network configuration (e.g. `/etc/systemd/networkd.conf` or a drop-in file):
> ```ini
> [Network]
> ManageForeignRoutingPolicyRules=no
> ManageForeignRoutes=no
> ```
> After editing, reload the configuration with `sudo systemctl restart systemd-networkd`. For more information refer to the [systemd-networkd documentation](https://www.freedesktop.org/software/systemd/man/latest/networkd.conf.html#ManageForeignRoutes=).
- **Within the WireGuard Portal Docker container**: - **Within the WireGuard Portal Docker container**:
WireGuard interfaces can be managed directly from within the WireGuard Portal container itself. WireGuard interfaces can be managed directly from within the WireGuard Portal container itself.
This is the recommended approach when running WireGuard Portal via Docker, as it encapsulates all functionality in a single, portable container without requiring a separate WireGuard host or image. This is the recommended approach when running WireGuard Portal via Docker, as it encapsulates all functionality in a single, portable container without requiring a separate WireGuard host or image.

View File

@@ -4,7 +4,7 @@ To build the application from source files, use the Makefile provided in the rep
- [Git](https://git-scm.com/downloads) - [Git](https://git-scm.com/downloads)
- [Make](https://www.gnu.org/software/make/) - [Make](https://www.gnu.org/software/make/)
- [Go](https://go.dev/dl/): `>=1.25.0` - [Go](https://go.dev/dl/): `>=1.24.0`
- [Node.js with npm](https://nodejs.org/en/download): `node>=18, npm>=9` - [Node.js with npm](https://nodejs.org/en/download): `node>=18, npm>=9`
## Build ## Build

View File

@@ -147,26 +147,6 @@ You can map users to admin roles based on their group membership in the LDAP ser
The `admin_group` property defines the distinguished name of the group that is allowed to log in as admin. The `admin_group` property defines the distinguished name of the group that is allowed to log in as admin.
All groups that are listed in the `memberof` attribute of the user will be checked against this group. If one of the groups matches, the user is granted admin access. All groups that are listed in the `memberof` attribute of the user will be checked against this group. If one of the groups matches, the user is granted admin access.
### Interface-specific Provisioning Filters
You can restrict which users are allowed to provision peers for specific WireGuard interfaces by setting the `interface_filter` property.
This property is a map where each key corresponds to a WireGuard interface identifier, and the value is an LDAP filter.
A user will only be able to see and provision peers for an interface if they match the specified LDAP filter for that interface.
Example:
```yaml
auth:
ldap:
- provider_name: "ldap1"
# ... other settings
interface_filter:
wg0: "(memberOf=CN=VPNUsers,OU=Groups,DC=COMPANY,DC=LOCAL)"
wg1: "(department=IT)"
```
This feature works by materializing the list of authorized users for each interface during the periodic LDAP synchronization.
Even if a user bypasses the UI, the backend will enforce these restrictions at the service layer.
## User Synchronization ## User Synchronization

View File

@@ -43,12 +43,4 @@ If you set the `disable_missing` property to `true`, any user that is not found
All peers associated with that user will also be disabled. All peers associated with that user will also be disabled.
If you want a user and its peers to be automatically re-enabled once they are found in LDAP again, set the `auto_re_enable` property to `true`. If you want a user and its peers to be automatically re-enabled once they are found in LDAP again, set the `auto_re_enable` property to `true`.
This will only re-enable the user if they were disabled by the synchronization process. Manually disabled users will not be re-enabled. This will only re-enable the user if they were disabled by the synchronization process. Manually disabled users will not be re-enabled.
##### Interface-specific Access Materialization
If `interface_filter` is configured in the LDAP provider, the synchronization process will evaluate these filters for each enabled user.
The results are materialized in the `interfaces` table of the database in a hidden field.
This materialized list is used by the backend to quickly determine if a user has permission to provision peers for a specific interface, without having to query the LDAP server for every request.
The list is refreshed every time the LDAP synchronization runs.
For more details on how to configure these filters, see the [Authentication](./authentication.md#interface-specific-provisioning-filters) section.

View File

@@ -14,7 +14,7 @@
let WGPORTAL_SITE_TITLE="WireGuard Portal"; let WGPORTAL_SITE_TITLE="WireGuard Portal";
let WGPORTAL_SITE_COMPANY_NAME="WireGuard Portal"; let WGPORTAL_SITE_COMPANY_NAME="WireGuard Portal";
</script> </script>
<script src="/api/v0/config/frontend.js" vite-ignore></script> <script src="/api/v0/config/frontend.js"></script>
</head> </head>
<body class="d-flex flex-column min-vh-100"> <body class="d-flex flex-column min-vh-100">
<noscript> <noscript>

File diff suppressed because it is too large Load Diff

View File

@@ -9,28 +9,28 @@
}, },
"dependencies": { "dependencies": {
"@fontsource/nunito-sans": "^5.2.7", "@fontsource/nunito-sans": "^5.2.7",
"@fortawesome/fontawesome-free": "^7.2.0", "@fortawesome/fontawesome-free": "^7.1.0",
"@kyvg/vue3-notification": "^3.4.2", "@kyvg/vue3-notification": "^3.4.2",
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
"@simplewebauthn/browser": "^13.3.0", "@simplewebauthn/browser": "^13.2.2",
"@vojtechlanka/vue-tags-input": "^3.1.2", "@vojtechlanka/vue-tags-input": "^3.1.1",
"bootstrap": "^5.3.8", "bootstrap": "^5.3.8",
"bootswatch": "^5.3.8", "bootswatch": "^5.3.8",
"cidr-tools": "^11.3.2", "cidr-tools": "^11.0.3",
"flag-icons": "^7.5.0", "flag-icons": "^7.5.0",
"ip-address": "^10.1.0", "ip-address": "^10.1.0",
"is-cidr": "^6.0.3", "is-cidr": "^6.0.1",
"is-ip": "^5.0.1", "is-ip": "^5.0.1",
"pinia": "^3.0.4", "pinia": "^3.0.4",
"prismjs": "^1.30.0", "prismjs": "^1.30.0",
"vue": "^3.5.31", "vue": "^3.5.25",
"vue-i18n": "^11.3.0", "vue-i18n": "^11.2.2",
"vue-prism-component": "github:h44z/vue-prism-component", "vue-prism-component": "github:h44z/vue-prism-component",
"vue-router": "^5.0.4" "vue-router": "^4.6.3"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^6.0.5", "@vitejs/plugin-vue": "^6.0.2",
"sass-embedded": "^1.98.0", "sass-embedded": "^1.93.3",
"vite": "^8.0.3" "vite": "^7.2.7"
} }
} }

View File

@@ -315,7 +315,6 @@ async function applyPeerDefaults() {
async function del() { async function del() {
if (isDeleting.value) return if (isDeleting.value) return
if (!confirm(t('modals.interface-edit.confirm-delete', {id: selectedInterface.value.Identifier}))) return
isDeleting.value = true isDeleting.value = true
try { try {
await interfaces.DeleteInterface(selectedInterface.value.Identifier) await interfaces.DeleteInterface(selectedInterface.value.Identifier)

View File

@@ -26,13 +26,13 @@
display:block; display:block;
} }
.modal.show { .modal.show {
opacity: 1.0; opacity: 1;
} }
.modal-backdrop { .modal-backdrop {
background-color: rgba(0,0,0,0.6) !important; background-color: rgba(0,0,0,0.6) !important;
} }
.modal-backdrop.show { .modal-backdrop.show {
opacity: 1.0 !important; opacity: 1 !important;
} }
</style> </style>

View File

@@ -294,7 +294,6 @@ async function save() {
async function del() { async function del() {
if (isDeleting.value) return if (isDeleting.value) return
if (!confirm(t('modals.peer-edit.confirm-delete', {id: selectedPeer.value.Identifier}))) return
isDeleting.value = true isDeleting.value = true
try { try {
await peers.DeletePeer(selectedPeer.value.Identifier) await peers.DeletePeer(selectedPeer.value.Identifier)

View File

@@ -114,7 +114,6 @@ async function save() {
async function del() { async function del() {
if (isDeleting.value) return if (isDeleting.value) return
if (!confirm(t('modals.user-edit.confirm-delete', {id: selectedUser.value.Identifier}))) return
isDeleting.value = true isDeleting.value = true
try { try {
await users.DeleteUser(selectedUser.value.Identifier) await users.DeleteUser(selectedUser.value.Identifier)

View File

@@ -382,8 +382,7 @@
"persist-local-changes": { "persist-local-changes": {
"label": "Lokale Änderungen speichern" "label": "Lokale Änderungen speichern"
}, },
"sync-warning": "Um diesen synchronisierten Benutzer zu bearbeiten, aktivieren Sie die lokale Änderungsspeicherung. Andernfalls werden Ihre Änderungen bei der nächsten Synchronisierung überschrieben.", "sync-warning": "Um diesen synchronisierten Benutzer zu bearbeiten, aktivieren Sie die lokale Änderungsspeicherung. Andernfalls werden Ihre Änderungen bei der nächsten Synchronisierung überschrieben."
"confirm-delete": "Benutzer '{id}' wirklich löschen?"
}, },
"interface-view": { "interface-view": {
"headline": "Konfiguration für Schnittstelle:" "headline": "Konfiguration für Schnittstelle:"
@@ -504,8 +503,7 @@
"placeholder": "Persistentes Keepalive (0 = Standard)" "placeholder": "Persistentes Keepalive (0 = Standard)"
} }
}, },
"button-apply-defaults": "Peer-Standardeinstellungen anwenden", "button-apply-defaults": "Peer-Standardeinstellungen anwenden"
"confirm-delete": "Interface '{id}' wirklich löschen?"
}, },
"peer-view": { "peer-view": {
"headline-peer": "Peer:", "headline-peer": "Peer:",
@@ -627,8 +625,7 @@
}, },
"expires-at": { "expires-at": {
"label": "Ablaufdatum" "label": "Ablaufdatum"
}, }
"confirm-delete": "Peer '{id}' wirklich löschen?"
}, },
"peer-multi-create": { "peer-multi-create": {
"headline-peer": "Mehrere Peers erstellen", "headline-peer": "Mehrere Peers erstellen",

View File

@@ -271,16 +271,16 @@
"headline-preshared-key": "New Preshared Key", "headline-preshared-key": "New Preshared Key",
"button-generate": "Generate", "button-generate": "Generate",
"private-key": { "private-key": {
"label": "Private Key", "label": "Private Key",
"placeholder": "The private key" "placeholder": "The private key"
}, },
"public-key": { "public-key": {
"label": "Public Key", "label": "Public Key",
"placeholder": "The public key" "placeholder": "The public key"
}, },
"preshared-key": { "preshared-key": {
"label": "Preshared Key", "label": "Preshared Key",
"placeholder": "The pre-shared key" "placeholder": "The pre-shared key"
} }
}, },
"calculator": { "calculator": {
@@ -289,18 +289,18 @@
"headline-allowed-ip": "New Allowed IPs", "headline-allowed-ip": "New Allowed IPs",
"button-exclude-private": "Exclude Private IP Ranges", "button-exclude-private": "Exclude Private IP Ranges",
"allowed-ip": { "allowed-ip": {
"label": "Allowed IPs", "label": "Allowed IPs",
"placeholder": "0.0.0.0/0, ::/0", "placeholder": "0.0.0.0/0, ::/0",
"empty": "Value cannot be empty" "empty": "Value cannot be empty"
}, },
"dissallowed-ip": { "dissallowed-ip": {
"label": "Disallowed IPs", "label": "Disallowed IPs",
"placeholder": "10.0.0.0/8, 192.168.0.0/16", "placeholder": "10.0.0.0/8, 192.168.0.0/16",
"invalid": "Invalid address: {addr}" "invalid": "Invalid address: {addr}"
}, },
"new-allowed-ip": { "new-allowed-ip": {
"label": "Allowed IPs", "label": "Allowed IPs",
"placeholder": "" "placeholder": ""
} }
}, },
"modals": { "modals": {
@@ -382,8 +382,7 @@
"persist-local-changes": { "persist-local-changes": {
"label": "Persist local changes" "label": "Persist local changes"
}, },
"sync-warning": "To modify this synchronized user, enable local change persistence. Otherwise, your changes will be overwritten during the next synchronization.", "sync-warning": "To modify this synchronized user, enable local change persistence. Otherwise, your changes will be overwritten during the next synchronization."
"confirm-delete": "Are you sure you want to delete user '{id}'?"
}, },
"interface-view": { "interface-view": {
"headline": "Config for Interface:" "headline": "Config for Interface:"
@@ -504,8 +503,8 @@
"placeholder": "Persistent Keepalive (0 = default)" "placeholder": "Persistent Keepalive (0 = default)"
} }
}, },
"button-apply-defaults": "Apply Peer Defaults",
"confirm-delete": "Are you sure you want to delete interface '{id}'?" "button-apply-defaults": "Apply Peer Defaults"
}, },
"peer-view": { "peer-view": {
"headline-peer": "Peer:", "headline-peer": "Peer:",
@@ -627,8 +626,7 @@
}, },
"expires-at": { "expires-at": {
"label": "Expiry date" "label": "Expiry date"
}, }
"confirm-delete": "Are you sure you want to delete peer '{id}'?"
}, },
"peer-multi-create": { "peer-multi-create": {
"headline-peer": "Create multiple peers", "headline-peer": "Create multiple peers",

File diff suppressed because it is too large Load Diff

View File

@@ -126,7 +126,9 @@
"peer-expiring": "Le pair expire le", "peer-expiring": "Le pair expire le",
"peer-connected": "Connecté", "peer-connected": "Connecté",
"peer-not-connected": "Non connecté", "peer-not-connected": "Non connecté",
"peer-handshake": "Dernière négociation :" "peer-handshake": "Dernière négociation :",
"button-show-peer": "Afficher le pair",
"button-edit-peer": "Modifier le pair"
}, },
"users": { "users": {
"headline": "Administration des utilisateurs", "headline": "Administration des utilisateurs",
@@ -262,8 +264,7 @@
}, },
"admin": { "admin": {
"label": "Est Admin" "label": "Est Admin"
}, }
"confirm-delete": "Voulez-vous vraiment supprimer l'utilisateur \"{id}\" ?"
}, },
"interface-view": { "interface-view": {
"headline": "Configuration pour l'interface :" "headline": "Configuration pour l'interface :"
@@ -376,8 +377,7 @@
"placeholder": "Persistent Keepalive (0 = par défaut)" "placeholder": "Persistent Keepalive (0 = par défaut)"
} }
}, },
"button-apply-defaults": "Appliquer les valeurs par défaut des pairs", "button-apply-defaults": "Appliquer les valeurs par défaut des pairs"
"confirm-delete": "Voulez-vous vraiment supprimer l'interface \"{id}\" ?"
}, },
"peer-view": { "peer-view": {
"headline-peer": "Pair :", "headline-peer": "Pair :",
@@ -493,8 +493,7 @@
}, },
"expires-at": { "expires-at": {
"label": "Date d'expiration" "label": "Date d'expiration"
}, }
"confirm-delete": "Voulez-vous vraiment supprimer le pair \"{id}\" ?"
}, },
"peer-multi-create": { "peer-multi-create": {
"headline-peer": "Créer plusieurs pairs", "headline-peer": "Créer plusieurs pairs",

View File

@@ -282,7 +282,6 @@
"label": "관리자 여부" "label": "관리자 여부"
} }
}, },
"confirm-delete": "사용자 '{id}'를 삭제하시겠습니까?",
"interface-view": { "interface-view": {
"headline": "인터페이스 구성:" "headline": "인터페이스 구성:"
}, },
@@ -394,8 +393,7 @@
"placeholder": "영구 Keepalive (0 = 기본값)" "placeholder": "영구 Keepalive (0 = 기본값)"
} }
}, },
"button-apply-defaults": "피어 기본값 적용", "button-apply-defaults": "피어 기본값 적용"
"confirm-delete": "인터페이스 '{id}'를 삭제하시겠습니까?"
}, },
"peer-view": { "peer-view": {
"headline-peer": "피어:", "headline-peer": "피어:",
@@ -511,8 +509,7 @@
}, },
"expires-at": { "expires-at": {
"label": "만료 날짜" "label": "만료 날짜"
}, }
"confirm-delete": "피어 '{id}'를 삭제하시겠습니까?"
}, },
"peer-multi-create": { "peer-multi-create": {
"headline-peer": "여러 피어 생성", "headline-peer": "여러 피어 생성",

View File

@@ -300,8 +300,7 @@
}, },
"admin": { "admin": {
"label": "É Administrador" "label": "É Administrador"
}, }
"confirm-delete": "Tem certeza que deseja excluir o utilizador '{id}'?"
}, },
"interface-view": { "interface-view": {
"headline": "Configuração para a Interface:" "headline": "Configuração para a Interface:"
@@ -414,8 +413,7 @@
"placeholder": "Keepalive persistente (0 = padrão)" "placeholder": "Keepalive persistente (0 = padrão)"
} }
}, },
"button-apply-defaults": "Aplicar Padrões de Peer", "button-apply-defaults": "Aplicar Padrões de Peer"
"confirm-delete": "Tem certeza que deseja excluir a interface '{id}'?"
}, },
"peer-view": { "peer-view": {
"headline-peer": "Peer:", "headline-peer": "Peer:",
@@ -532,8 +530,7 @@
}, },
"expires-at": { "expires-at": {
"label": "Data de expiração" "label": "Data de expiração"
}, }
"confirm-delete": "Tem certeza que deseja excluir o par '{id}'?"
}, },
"peer-multi-create": { "peer-multi-create": {
"headline-peer": "Criar múltiplos peers", "headline-peer": "Criar múltiplos peers",

View File

@@ -259,16 +259,16 @@
"headline-preshared-key": "Новый общий ключ", "headline-preshared-key": "Новый общий ключ",
"button-generate": "Генерировать", "button-generate": "Генерировать",
"private-key": { "private-key": {
"label": "Приватный ключ", "label": "Приватный ключ",
"placeholder": "Приватный ключ" "placeholder": "Приватный ключ"
}, },
"public-key": { "public-key": {
"label": "Публичный ключ", "label": "Публичный ключ",
"placeholder": "Публичный ключ" "placeholder": "Публичный ключ"
}, },
"preshared-key": { "preshared-key": {
"label": "Общий ключ", "label": "Общий ключ",
"placeholder": "Общий ключ" "placeholder": "Общий ключ"
} }
}, },
"calculator": { "calculator": {
@@ -277,18 +277,18 @@
"headline-allowed-ip": "Новые разрешенные IP-адреса", "headline-allowed-ip": "Новые разрешенные IP-адреса",
"button-exclude-private": "Исключить частные диапазоны IP-адресов", "button-exclude-private": "Исключить частные диапазоны IP-адресов",
"allowed-ip": { "allowed-ip": {
"label": "Разрешенные IP-адреса", "label": "Разрешенные IP-адреса",
"placeholder": "0.0.0.0/0, ::/0", "placeholder": "0.0.0.0/0, ::/0",
"empty": "Поле ввода не должно быть пустым" "empty": "Поле ввода не должно быть пустым"
}, },
"dissallowed-ip": { "dissallowed-ip": {
"label": "Запрещенные IP-адреса", "label": "Запрещенные IP-адреса",
"placeholder": "10.0.0.0/8, 192.168.0.0/16", "placeholder": "10.0.0.0/8, 192.168.0.0/16",
"invalid": "Некорректный адрес: {addr}" "invalid": "Некорректный адрес: {addr}"
}, },
"new-allowed-ip": { "new-allowed-ip": {
"label": "Разрешенные IP-адреса", "label": "Разрешенные IP-адреса",
"placeholder": "" "placeholder": ""
} }
}, },
"modals": { "modals": {
@@ -366,8 +366,7 @@
}, },
"admin": { "admin": {
"label": "Является администратором" "label": "Является администратором"
}, }
"confirm-delete": "Вы уверены, что хотите удалить пользователя «{id}»?"
}, },
"interface-view": { "interface-view": {
"headline": "Конфигурация интерфейса:" "headline": "Конфигурация интерфейса:"
@@ -485,8 +484,7 @@
"placeholder": "Постоянное поддержание активности (0 = значение по умолчанию)" "placeholder": "Постоянное поддержание активности (0 = значение по умолчанию)"
} }
}, },
"button-apply-defaults": "Применить настройки пира по умолчанию", "button-apply-defaults": "Применить настройки пира по умолчанию"
"confirm-delete": "Вы уверены, что хотите удалить интерфейс «{id}»?"
}, },
"peer-view": { "peer-view": {
"headline-peer": "Пир:", "headline-peer": "Пир:",
@@ -607,8 +605,7 @@
}, },
"expires-at": { "expires-at": {
"label": "Дата истечения срока действия" "label": "Дата истечения срока действия"
}, }
"confirm-delete": "Вы уверены, что хотите удалить пир «{id}»?"
}, },
"peer-multi-create": { "peer-multi-create": {
"headline-peer": "Создать несколько узлов", "headline-peer": "Создать несколько узлов",

View File

@@ -151,6 +151,7 @@
"admin": "Користувач має адміністративні привілеї", "admin": "Користувач має адміністративні привілеї",
"no-admin": "Користувач не має адміністративних привілеїв" "no-admin": "Користувач не має адміністративних привілеїв"
}, },
"profile": { "profile": {
"headline": "Мої VPN-піри", "headline": "Мої VPN-піри",
"table-heading": { "table-heading": {
@@ -188,6 +189,7 @@
"api-link": "Документація API" "api-link": "Документація API"
} }
}, },
"modals": { "modals": {
"user-view": { "user-view": {
"headline": "Обліковий запис користувача:", "headline": "Обліковий запис користувача:",
@@ -262,8 +264,7 @@
}, },
"admin": { "admin": {
"label": "Адміністратор" "label": "Адміністратор"
}, }
"confirm-delete": "Ви впевнені, що хочете видалити користувача «{id}»?"
}, },
"interface-view": { "interface-view": {
"headline": "Конфігурація для інтерфейсу:" "headline": "Конфігурація для інтерфейсу:"
@@ -376,8 +377,7 @@
"placeholder": "Постійний Keepalive (0 = за замовчуванням)" "placeholder": "Постійний Keepalive (0 = за замовчуванням)"
} }
}, },
"button-apply-defaults": "Застосувати значення за замовчуванням для пірів", "button-apply-defaults": "Застосувати значення за замовчуванням для пірів"
"confirm-delete": "Ви впевнені, що хочете видалити інтерфейс «{id}»?"
}, },
"peer-view": { "peer-view": {
"headline-peer": "Пір:", "headline-peer": "Пір:",
@@ -493,8 +493,7 @@
}, },
"expires-at": { "expires-at": {
"label": "Дата закінчення терміну дії" "label": "Дата закінчення терміну дії"
}, }
"confirm-delete": "Ви впевнені, що хочете видалити пір «{id}»?"
}, },
"peer-multi-create": { "peer-multi-create": {
"headline-peer": "Створити декілька пір", "headline-peer": "Створити декілька пір",

View File

@@ -240,8 +240,7 @@
}, },
"admin": { "admin": {
"label": "Là Quản trị viên" "label": "Là Quản trị viên"
}, }
"confirm-delete": "Ban co chac muon xoa nguoi dung '{id}' khong?"
}, },
"interface-view": { "interface-view": {
"headline": "Cấu hình cho Giao diện:" "headline": "Cấu hình cho Giao diện:"
@@ -354,8 +353,8 @@
"placeholder": "Giữ kết nối liên tục (0 = mặc định)" "placeholder": "Giữ kết nối liên tục (0 = mặc định)"
} }
}, },
"button-apply-defaults": "Áp dụng Cài đặt Mặc định của Peer",
"confirm-delete": "Ban co chac muon xoa giao dien '{id}' khong?" "button-apply-defaults": "Áp dụng Cài đặt Mặc định của Peer"
}, },
"peer-view": { "peer-view": {
"headline-peer": "Peer:", "headline-peer": "Peer:",
@@ -471,8 +470,7 @@
}, },
"expires-at": { "expires-at": {
"label": "Ngày hết hạn" "label": "Ngày hết hạn"
}, }
"confirm-delete": "Ban co chac muon xoa peer '{id}' khong?"
}, },
"peer-multi-create": { "peer-multi-create": {
"headline-peer": "Tạo nhiều peer", "headline-peer": "Tạo nhiều peer",

View File

@@ -242,7 +242,6 @@
"label": "管理员" "label": "管理员"
} }
}, },
"confirm-delete": "确定要删除用户“{id}”吗?",
"interface-view": { "interface-view": {
"headline": "接口配置: " "headline": "接口配置: "
}, },
@@ -354,8 +353,7 @@
"placeholder": "持久保持连接 (0 = 默认)" "placeholder": "持久保持连接 (0 = 默认)"
} }
}, },
"button-apply-defaults": "应用节点默认值", "button-apply-defaults": "应用节点默认值"
"confirm-delete": "确定要删除接口“{id}”吗?"
}, },
"peer-view": { "peer-view": {
"headline-peer": "节点: ", "headline-peer": "节点: ",
@@ -471,8 +469,7 @@
}, },
"expires-at": { "expires-at": {
"label": "过期日期" "label": "过期日期"
}, }
"confirm-delete": "确定要删除对等点“{id}”吗?"
}, },
"peer-multi-create": { "peer-multi-create": {
"headline-peer": "创建多个节点", "headline-peer": "创建多个节点",

View File

@@ -1,6 +1,7 @@
import {createRouter, createWebHashHistory} from 'vue-router' import {createRouter, createWebHashHistory} from 'vue-router'
import HomeView from '../views/HomeView.vue' import HomeView from '../views/HomeView.vue'
import LoginView from '../views/LoginView.vue' import LoginView from '../views/LoginView.vue'
import InterfaceView from '../views/InterfaceView.vue'
import {authStore} from '@/stores/auth' import {authStore} from '@/stores/auth'
import {securityStore} from '@/stores/security' import {securityStore} from '@/stores/security'
@@ -19,6 +20,11 @@ const router = createRouter({
name: 'login', name: 'login',
component: LoginView component: LoginView
}, },
{
path: '/interface',
name: 'interface',
component: InterfaceView
},
{ {
path: '/interfaces', path: '/interfaces',
name: 'interfaces', name: 'interfaces',

View File

@@ -74,7 +74,6 @@ export const profileStore = defineStore('profile', {
}, },
hasStatistics: (state) => state.statsEnabled, hasStatistics: (state) => state.statsEnabled,
CountInterfaces: (state) => state.interfaces.length, CountInterfaces: (state) => state.interfaces.length,
HasInterface: (state) => (id) => state.interfaces.some((i) => i.Identifier === id),
}, },
actions: { actions: {
afterPageSizeChange() { afterPageSizeChange() {

View File

@@ -80,8 +80,6 @@ onMounted(async () => {
<div class="col-12 col-lg-5"> <div class="col-12 col-lg-5">
<h2 class="mt-2">{{ $t('profile.headline') }}</h2> <h2 class="mt-2">{{ $t('profile.headline') }}</h2>
</div> </div>
<div class="col-12 col-lg-3 text-lg-end" v-if="!settings.Setting('SelfProvisioning') || profile.CountInterfaces===0">
</div>
<div class="col-12 col-lg-4 text-lg-end"> <div class="col-12 col-lg-4 text-lg-end">
<div class="form-group d-inline"> <div class="form-group d-inline">
<div class="input-group mb-3"> <div class="input-group mb-3">
@@ -92,8 +90,8 @@ onMounted(async () => {
</div> </div>
</div> </div>
</div> </div>
<div class="col-12 col-lg-3 text-lg-end" v-if="settings.Setting('SelfProvisioning') && profile.CountInterfaces>0"> <div class="col-12 col-lg-3 text-lg-end">
<div class="form-group"> <div class="form-group" v-if="settings.Setting('SelfProvisioning')">
<div class="input-group mb-3"> <div class="input-group mb-3">
<button class="btn btn-primary" :title="$t('interfaces.button-add-peer')" @click.prevent="editPeerId = '#NEW#'"> <button class="btn btn-primary" :title="$t('interfaces.button-add-peer')" @click.prevent="editPeerId = '#NEW#'">
<i class="fa fa-plus me-1"></i><i class="fa fa-user"></i> <i class="fa fa-plus me-1"></i><i class="fa fa-user"></i>
@@ -162,7 +160,8 @@ onMounted(async () => {
</td> </td>
<td v-if="profile.hasStatistics"> <td v-if="profile.hasStatistics">
<div v-if="profile.Statistics(peer.Identifier).IsConnected"> <div v-if="profile.Statistics(peer.Identifier).IsConnected">
<span class="badge rounded-pill bg-success" :title="$t('profile.peer-connected')"><i class="fa-solid fa-link"></i></span> <small class="text-muted" :title="$t('interfaces.peer-handshake') + ' ' + profile.Statistics(peer.Identifier).LastHandshake"><i class="fa-solid fa-circle-info"></i></small> <span class="badge rounded-pill bg-success"><i class="fa-solid fa-link"></i></span>
<span :title="profile.Statistics(peer.Identifier).LastHandshake">{{ $t('profile.peer-connected') }}</span>
</div> </div>
<div v-else> <div v-else>
<span class="badge rounded-pill bg-light"><i class="fa-solid fa-link-slash"></i></span> <span class="badge rounded-pill bg-light"><i class="fa-solid fa-link-slash"></i></span>
@@ -175,7 +174,7 @@ onMounted(async () => {
<td class="text-center"> <td class="text-center">
<a href="#" :title="$t('profile.button-show-peer')" @click.prevent="viewedPeerId = peer.Identifier"><i <a href="#" :title="$t('profile.button-show-peer')" @click.prevent="viewedPeerId = peer.Identifier"><i
class="fas fa-eye me-2"></i></a> class="fas fa-eye me-2"></i></a>
<a href="#" :title="$t('profile.button-edit-peer')" @click.prevent="editPeerId = peer.Identifier" v-if="settings.Setting('SelfProvisioning') && profile.HasInterface(peer.InterfaceIdentifier)"><i <a href="#" :title="$t('profile.button-edit-peer')" @click.prevent="editPeerId = peer.Identifier"><i
class="fas fa-cog"></i></a> class="fas fa-cog"></i></a>
</td> </td>
</tr> </tr>

52
go.mod
View File

@@ -1,19 +1,19 @@
module github.com/h44z/wg-portal module github.com/h44z/wg-portal
go 1.25.0 go 1.24.0
require ( require (
github.com/a8m/envsubst v1.4.3 github.com/a8m/envsubst v1.4.3
github.com/alexedwards/scs/v2 v2.9.0 github.com/alexedwards/scs/v2 v2.9.0
github.com/coreos/go-oidc/v3 v3.17.0 github.com/coreos/go-oidc/v3 v3.17.0
github.com/glebarez/sqlite v1.11.0 github.com/glebarez/sqlite v1.11.0
github.com/go-ldap/ldap/v3 v3.4.13 github.com/go-ldap/ldap/v3 v3.4.12
github.com/go-pkgz/routegroup v1.6.0 github.com/go-pkgz/routegroup v1.6.0
github.com/go-playground/validator/v10 v10.30.1 github.com/go-playground/validator/v10 v10.30.1
github.com/go-webauthn/webauthn v0.16.1 github.com/go-webauthn/webauthn v0.15.0
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3 github.com/gorilla/websocket v1.5.3
github.com/prometheus-community/pro-bing v0.8.0 github.com/prometheus-community/pro-bing v0.7.0
github.com/prometheus/client_golang v1.23.2 github.com/prometheus/client_golang v1.23.2
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
github.com/swaggo/swag v1.16.6 github.com/swaggo/swag v1.16.6
@@ -22,9 +22,9 @@ require (
github.com/xhit/go-simple-mail/v2 v2.16.0 github.com/xhit/go-simple-mail/v2 v2.16.0
github.com/yeqown/go-qrcode/v2 v2.2.5 github.com/yeqown/go-qrcode/v2 v2.2.5
github.com/yeqown/go-qrcode/writer/compressed v1.0.1 github.com/yeqown/go-qrcode/writer/compressed v1.0.1
golang.org/x/crypto v0.49.0 golang.org/x/crypto v0.47.0
golang.org/x/oauth2 v0.36.0 golang.org/x/oauth2 v0.34.0
golang.org/x/sys v0.42.0 golang.org/x/sys v0.40.0
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
gorm.io/driver/mysql v1.6.0 gorm.io/driver/mysql v1.6.0
@@ -34,7 +34,7 @@ require (
) )
require ( require (
filippo.io/edwards25519 v1.2.0 // indirect filippo.io/edwards25519 v1.1.0 // indirect
github.com/Azure/go-ntlmssp v0.1.0 // indirect github.com/Azure/go-ntlmssp v0.1.0 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect github.com/KyleBanks/depth v1.2.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
@@ -42,13 +42,13 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.13 // indirect github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/glebarez/go-sqlite v1.22.0 // indirect github.com/glebarez/go-sqlite v1.22.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-openapi/jsonpointer v0.22.4 // indirect github.com/go-openapi/jsonpointer v0.22.4 // indirect
github.com/go-openapi/jsonreference v0.21.4 // indirect github.com/go-openapi/jsonreference v0.21.4 // indirect
github.com/go-openapi/spec v0.22.3 // indirect github.com/go-openapi/spec v0.22.2 // indirect
github.com/go-openapi/swag/conv v0.25.4 // indirect github.com/go-openapi/swag/conv v0.25.4 // indirect
github.com/go-openapi/swag/jsonname v0.25.4 // indirect github.com/go-openapi/swag/jsonname v0.25.4 // indirect
github.com/go-openapi/swag/jsonutils v0.25.4 // indirect github.com/go-openapi/swag/jsonutils v0.25.4 // indirect
@@ -60,16 +60,16 @@ require (
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-sql-driver/mysql v1.9.3 // indirect github.com/go-sql-driver/mysql v1.9.3 // indirect
github.com/go-test/deep v1.1.1 // indirect github.com/go-test/deep v1.1.1 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/go-webauthn/x v0.2.2 // indirect github.com/go-webauthn/x v0.1.26 // indirect
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect
github.com/google/go-tpm v0.9.8 // indirect github.com/google/go-tpm v0.9.7 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.8.0 // indirect github.com/jackc/pgx/v5 v5.7.6 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
@@ -78,12 +78,12 @@ require (
github.com/mdlayher/genetlink v1.3.2 // indirect github.com/mdlayher/genetlink v1.3.2 // indirect
github.com/mdlayher/netlink v1.8.0 // indirect github.com/mdlayher/netlink v1.8.0 // indirect
github.com/mdlayher/socket v0.5.1 // indirect github.com/mdlayher/socket v0.5.1 // indirect
github.com/microsoft/go-mssqldb v1.9.6 // indirect github.com/microsoft/go-mssqldb v1.9.5 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect github.com/prometheus/common v0.67.4 // indirect
github.com/prometheus/procfs v0.19.2 // indirect github.com/prometheus/procfs v0.19.2 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/shopspring/decimal v1.4.0 // indirect github.com/shopspring/decimal v1.4.0 // indirect
@@ -93,17 +93,17 @@ require (
github.com/yeqown/reedsolomon v1.0.0 // indirect github.com/yeqown/reedsolomon v1.0.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 // indirect
golang.org/x/mod v0.33.0 // indirect golang.org/x/mod v0.31.0 // indirect
golang.org/x/net v0.51.0 // indirect golang.org/x/net v0.48.0 // indirect
golang.org/x/sync v0.20.0 // indirect golang.org/x/sync v0.19.0 // indirect
golang.org/x/text v0.35.0 // indirect golang.org/x/text v0.33.0 // indirect
golang.org/x/tools v0.42.0 // indirect golang.org/x/tools v0.40.0 // indirect
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
google.golang.org/protobuf v1.36.11 // indirect google.golang.org/protobuf v1.36.10 // indirect
modernc.org/libc v1.68.0 // indirect modernc.org/libc v1.67.1 // indirect
modernc.org/mathutil v1.7.1 // indirect modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.46.1 // indirect modernc.org/sqlite v1.40.1 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect sigs.k8s.io/yaml v1.6.0 // indirect
) )

110
go.sum
View File

@@ -1,5 +1,5 @@
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo=
@@ -50,8 +50,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ= github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc= github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
@@ -60,14 +60,14 @@ github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-ldap/ldap/v3 v3.4.13 h1:+x1nG9h+MZN7h/lUi5Q3UZ0fJ1GyDQYbPvbuH38baDQ= github.com/go-ldap/ldap/v3 v3.4.12 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4=
github.com/go-ldap/ldap/v3 v3.4.13/go.mod h1:LxsGZV6vbaK0sIvYfsv47rfh4ca0JXokCoKjZxsszv0= github.com/go-ldap/ldap/v3 v3.4.12/go.mod h1:+SPAGcTtOfmGsCb3h1RFiq4xpp4N636G75OEace8lNo=
github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4=
github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80=
github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8= github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8=
github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4= github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4=
github.com/go-openapi/spec v0.22.3 h1:qRSmj6Smz2rEBxMnLRBMeBWxbbOvuOoElvSvObIgwQc= github.com/go-openapi/spec v0.22.2 h1:KEU4Fb+Lp1qg0V4MxrSCPv403ZjBl8Lx1a83gIPU8Qc=
github.com/go-openapi/spec v0.22.3/go.mod h1:iIImLODL2loCh3Vnox8TY2YWYJZjMAKYyLH2Mu8lOZs= github.com/go-openapi/spec v0.22.2/go.mod h1:iIImLODL2loCh3Vnox8TY2YWYJZjMAKYyLH2Mu8lOZs=
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4=
github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU=
@@ -103,17 +103,17 @@ github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/go-webauthn/webauthn v0.16.1 h1:x5/SSki5/aIfogaRukqvbg/RXa3Sgxy/9vU7UfFPHKU= github.com/go-webauthn/webauthn v0.15.0 h1:LR1vPv62E0/6+sTenX35QrCmpMCzLeVAcnXeH4MrbJY=
github.com/go-webauthn/webauthn v0.16.1/go.mod h1:RBS+rtQJMkE5VfMQ4diDA2VNrEL8OeUhp4Srz37FHbQ= github.com/go-webauthn/webauthn v0.15.0/go.mod h1:hcAOhVChPRG7oqG7Xj6XKN1mb+8eXTGP/B7zBLzkX5A=
github.com/go-webauthn/x v0.2.2 h1:zIiipvMbr48CXi5RG0XdBJR94kd8I5LfzHPb/q+YYmk= github.com/go-webauthn/x v0.1.26 h1:eNzreFKnwNLDFoywGh9FA8YOMebBWTUNlNSdolQRebs=
github.com/go-webauthn/x v0.2.2/go.mod h1:IpJ5qyWB9NRhLX3C7gIfjTU7RZLXEP6kzFkoVSE7Fz4= github.com/go-webauthn/x v0.1.26/go.mod h1:jmf/phPV6oIsF6hmdVre+ovHkxjDOmNH0t6fekWUxvg=
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
@@ -121,10 +121,8 @@ github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EO
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= github.com/google/go-tpm v0.9.7 h1:u89J4tUUeDTlH8xxC3CTW7OHZjbjKoHdQ9W7gCUhtxA=
github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= github.com/google/go-tpm v0.9.7/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
github.com/google/go-tpm-tools v0.3.13-0.20230620182252-4639ecce2aba h1:qJEJcuLzH5KDR0gKc0zcktin6KSAwL7+jWKBYceddTc=
github.com/google/go-tpm-tools v0.3.13-0.20230620182252-4639ecce2aba/go.mod h1:EFYHy8/1y2KfgTAsx7Luu7NGhoxtuVHnNo8jE7FikKc=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -143,8 +141,8 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo= github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw= github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
@@ -185,8 +183,8 @@ github.com/mdlayher/netlink v1.8.0/go.mod h1:UhgKXUlDQhzb09DrCl2GuRNEglHmhYoWAHi
github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos= github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ= github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
github.com/microsoft/go-mssqldb v1.8.2/go.mod h1:vp38dT33FGfVotRiTmDo3bFyaHq+p3LektQrjTULowo= github.com/microsoft/go-mssqldb v1.8.2/go.mod h1:vp38dT33FGfVotRiTmDo3bFyaHq+p3LektQrjTULowo=
github.com/microsoft/go-mssqldb v1.9.6 h1:1MNQg5UiSsokiPz3++K2KPx4moKrwIqly1wv+RyCKTw= github.com/microsoft/go-mssqldb v1.9.5 h1:orwya0X/5bsL1o+KasupTkk2eNTNFkTQG0BEe/HxCn0=
github.com/microsoft/go-mssqldb v1.9.6/go.mod h1:yYMPDufyoF2vVuVCUGtZARr06DKFIhMrluTcgWlXpr4= github.com/microsoft/go-mssqldb v1.9.5/go.mod h1:VCP2a0KEZZtGLRHd1PsLavLFYy/3xX2yJUPycv3Sr2Q=
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws= github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc= github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
@@ -201,14 +199,14 @@ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjL
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus-community/pro-bing v0.8.0 h1:CEY/g1/AgERRDjxw5P32ikcOgmrSuXs7xon7ovx6mNc= github.com/prometheus-community/pro-bing v0.7.0 h1:KFYFbxC2f2Fp6c+TyxbCOEarf7rbnzr9Gw8eIb0RfZA=
github.com/prometheus-community/pro-bing v0.8.0/go.mod h1:Idyxz8raDO6TgkUN6ByiEGvWJNyQd40kN9ZUeho3lN0= github.com/prometheus-community/pro-bing v0.7.0/go.mod h1:Moob9dvlY50Bfq6i88xIwfyw7xLFHH69LUgx9n5zqCE=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
@@ -274,18 +272,18 @@ golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOM
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0= golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 h1:MDfG8Cvcqlt9XXrmEiD4epKn7VJHZO84hejP9Jmp0MM=
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= golang.org/x/exp v0.0.0-20251209150349-8475f28825e9/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
@@ -303,10 +301,10 @@ golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -314,8 +312,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -336,8 +334,8 @@ golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -366,23 +364,23 @@ golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A= golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A=
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw= golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 h1:3GDAcqdIg1ozBNLgPy4SLT84nfcBjr6rhGtXYtrkWLU= golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 h1:3GDAcqdIg1ozBNLgPy4SLT84nfcBjr6rhGtXYtrkWLU=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10/go.mod h1:T97yPqesLiNrOYxkwmhMI0ZIlJDm+p0PMR8eRVeR5tQ= golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10/go.mod h1:T97yPqesLiNrOYxkwmhMI0ZIlJDm+p0PMR8eRVeR5tQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
@@ -404,18 +402,18 @@ gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.30.2 h1:4yPaaq9dXYXZ2V8s1UgrC3KIj580l2N4ClrLwnbv2so= modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=
modernc.org/ccgo/v4 v4.30.2/go.mod h1:yZMnhWEdW0qw3EtCndG1+ldRrVGS+bIwyWmAWzS0XEw= modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo= modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.68.0 h1:PJ5ikFOV5pwpW+VqCK1hKJuEWsonkIJhhIXyuF/91pQ= modernc.org/libc v1.67.1 h1:bFaqOaa5/zbWYJo8aW0tXPX21hXsngG2M7mckCnFSVk=
modernc.org/libc v1.68.0/go.mod h1:NnKCYeoYgsEqnY3PgvNgAeaJnso968ygU8Z0DxjoEc0= modernc.org/libc v1.67.1/go.mod h1:QvvnnJ5P7aitu0ReNpVIEyesuhmDLQ8kaEoyMjIFZJA=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
@@ -424,8 +422,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU= modernc.org/sqlite v1.40.1 h1:VfuXcxcUWWKRBuP8+BR9L7VnmusMgBNNnBYGEe9w/iY=
modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA= modernc.org/sqlite v1.40.1/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=

View File

@@ -232,19 +232,21 @@ func (r *SqlRepo) migrate() error {
slog.Debug("running migration: interface status", "result", r.db.AutoMigrate(&domain.InterfaceStatus{})) slog.Debug("running migration: interface status", "result", r.db.AutoMigrate(&domain.InterfaceStatus{}))
slog.Debug("running migration: audit data", "result", r.db.AutoMigrate(&domain.AuditEntry{})) slog.Debug("running migration: audit data", "result", r.db.AutoMigrate(&domain.AuditEntry{}))
var existingSysStat SysStat existingSysStat := SysStat{}
var err error
r.db.Order("schema_version desc").First(&existingSysStat) // get latest version r.db.Order("schema_version desc").First(&existingSysStat) // get latest version
// Migration: 0 --> 1 // Migration: 0 --> 1
if existingSysStat.SchemaVersion == 0 { if existingSysStat.SchemaVersion == 0 {
const schemaVersion = 1 const schemaVersion = 1
existingSysStat, err = r.addMigration(schemaVersion) // ensure that follow-up checks test against the latest version sysStat := SysStat{
if err != nil { MigratedAt: time.Now(),
return err SchemaVersion: schemaVersion,
}
if err := r.db.Create(&sysStat).Error; err != nil {
return fmt.Errorf("failed to write sysstat entry for schema version %d: %w", schemaVersion, err)
} }
slog.Debug("sys-stat entry written", "schema_version", schemaVersion) slog.Debug("sys-stat entry written", "schema_version", schemaVersion)
existingSysStat = sysStat // ensure that follow-up checks test against the latest version
} }
// Migration: 1 --> 2 // Migration: 1 --> 2
@@ -260,10 +262,14 @@ func (r *SqlRepo) migrate() error {
} }
slog.Debug("migrated interface create_default_peer flags", "schema_version", schemaVersion) slog.Debug("migrated interface create_default_peer flags", "schema_version", schemaVersion)
} }
existingSysStat, err = r.addMigration(schemaVersion) // ensure that follow-up checks test against the latest version sysStat := SysStat{
if err != nil { MigratedAt: time.Now(),
return err SchemaVersion: schemaVersion,
} }
if err := r.db.Create(&sysStat).Error; err != nil {
return fmt.Errorf("failed to write sysstat entry for schema version %d: %w", schemaVersion, err)
}
existingSysStat = sysStat // ensure that follow-up checks test against the latest version
} }
// Migration: 2 --> 3 // Migration: 2 --> 3
@@ -301,45 +307,19 @@ func (r *SqlRepo) migrate() error {
if err != nil { if err != nil {
return fmt.Errorf("failed to migrate to multi-auth: %w", err) return fmt.Errorf("failed to migrate to multi-auth: %w", err)
} }
existingSysStat, err = r.addMigration(schemaVersion) // ensure that follow-up checks test against the latest version sysStat := SysStat{
if err != nil { MigratedAt: time.Now(),
return err SchemaVersion: schemaVersion,
} }
} if err := r.db.Create(&sysStat).Error; err != nil {
return fmt.Errorf("failed to write sysstat entry for schema version %d: %w", schemaVersion, err)
// Migration: 3 --> 4
if existingSysStat.SchemaVersion == 3 {
const schemaVersion = 4
cutoff := time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)
// Fix zero created_at timestamps for users. Set the to the last known update timestamp.
err := r.db.Model(&domain.User{}).Where("created_at < ?", cutoff).
Update("created_at", gorm.Expr("updated_at")).Error
if err != nil {
slog.Warn("failed to fix zero created_at for users", "error", err)
}
slog.Debug("fixed zero created_at timestamps for users", "schema_version", schemaVersion)
existingSysStat, err = r.addMigration(schemaVersion) // ensure that follow-up checks test against the latest version
if err != nil {
return err
} }
existingSysStat = sysStat // ensure that follow-up checks test against the latest version
} }
return nil return nil
} }
func (r *SqlRepo) addMigration(schemaVersion uint64) (SysStat, error) {
sysStat := SysStat{
MigratedAt: time.Now(),
SchemaVersion: schemaVersion,
}
if err := r.db.Create(&sysStat).Error; err != nil {
return SysStat{}, fmt.Errorf("failed to write sysstat entry for schema version %d: %w", schemaVersion, err)
}
return sysStat, nil
}
// region interfaces // region interfaces
// GetInterface returns the interface with the given id. // GetInterface returns the interface with the given id.
@@ -502,7 +482,7 @@ func (r *SqlRepo) getOrCreateInterface(
Identifier: id, Identifier: id,
} }
err := tx.Preload("Addresses").Attrs(interfaceDefaults).FirstOrCreate(&in, id).Error err := tx.Attrs(interfaceDefaults).FirstOrCreate(&in, id).Error
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -711,7 +691,7 @@ func (r *SqlRepo) getOrCreatePeer(ui *domain.ContextUserInfo, tx *gorm.DB, id do
Identifier: id, Identifier: id,
} }
err := tx.Preload("Addresses").Attrs(interfaceDefaults).FirstOrCreate(&peer, id).Error err := tx.Attrs(interfaceDefaults).FirstOrCreate(&peer, id).Error
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -1,168 +0,0 @@
package adapters
import (
"context"
"testing"
"time"
"github.com/glebarez/sqlite"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
"github.com/h44z/wg-portal/internal/config"
"github.com/h44z/wg-portal/internal/domain"
)
func newTestDB(t *testing.T) *gorm.DB {
t.Helper()
db, err := gorm.Open(sqlite.Open("file::memory:"), &gorm.Config{})
require.NoError(t, err)
return db
}
func TestUpsertUser_SetsCreatedAtWhenZero(t *testing.T) {
db := newTestDB(t)
require.NoError(t, db.AutoMigrate(&domain.User{}, &domain.UserAuthentication{}, &domain.UserWebauthnCredential{}))
repo := &SqlRepo{db: db, cfg: &config.Config{}}
ui := domain.SystemAdminContextUserInfo()
user := &domain.User{
Identifier: "test-user",
Email: "test@example.com",
// CreatedAt is zero
}
err := repo.upsertUser(ui, db, user)
require.NoError(t, err)
assert.False(t, user.CreatedAt.IsZero(), "CreatedAt should be set when it was zero")
assert.Equal(t, ui.UserId(), user.UpdatedBy, "UpdatedBy should be set when it was empty")
assert.WithinDuration(t, user.UpdatedAt, user.CreatedAt, time.Second,
"CreatedAt should be close to UpdatedAt for new user")
}
func TestUpsertUser_PreservesExistingCreatedAt(t *testing.T) {
db := newTestDB(t)
require.NoError(t, db.AutoMigrate(&domain.User{}, &domain.UserAuthentication{}, &domain.UserWebauthnCredential{}))
repo := &SqlRepo{db: db, cfg: &config.Config{}}
ui := domain.SystemAdminContextUserInfo()
originalTime := time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC)
user := &domain.User{
Identifier: "test-user",
Email: "test@example.com",
BaseModel: domain.BaseModel{
CreatedAt: originalTime,
CreatedBy: "original-creator",
},
}
err := repo.upsertUser(ui, db, user)
require.NoError(t, err)
assert.Equal(t, originalTime, user.CreatedAt, "CreatedAt should not be overwritten")
assert.Equal(t, "original-creator", user.CreatedBy, "CreatedBy should not be overwritten")
}
func TestSaveUser_NewUserGetsCreatedAt(t *testing.T) {
db := newTestDB(t)
require.NoError(t, db.AutoMigrate(&domain.User{}, &domain.UserAuthentication{}, &domain.UserWebauthnCredential{}))
repo := &SqlRepo{db: db, cfg: &config.Config{}}
ctx := domain.SetUserInfo(context.Background(), domain.SystemAdminContextUserInfo())
before := time.Now().Add(-time.Second)
err := repo.SaveUser(ctx, "new-user", func(u *domain.User) (*domain.User, error) {
u.Email = "new@example.com"
return u, nil
})
require.NoError(t, err)
var saved domain.User
require.NoError(t, db.First(&saved, "identifier = ?", "new-user").Error)
assert.False(t, saved.CreatedAt.IsZero(), "CreatedAt should not be zero")
assert.True(t, saved.CreatedAt.After(before), "CreatedAt should be recent")
assert.NotEmpty(t, saved.CreatedBy, "CreatedBy should be set")
}
func TestMigration_FixesZeroCreatedAt(t *testing.T) {
db := newTestDB(t)
// Manually create tables and seed schema version 3
require.NoError(t, db.AutoMigrate(
&SysStat{},
&domain.User{},
&domain.UserAuthentication{},
&domain.Interface{},
&domain.Cidr{},
&domain.Peer{},
&domain.AuditEntry{},
&domain.UserWebauthnCredential{},
))
// Insert schema versions 1, 2, 3 so migration starts at 3
for v := uint64(1); v <= 3; v++ {
require.NoError(t, db.Create(&SysStat{SchemaVersion: v, MigratedAt: time.Now()}).Error)
}
updatedAt := time.Date(2025, 6, 15, 10, 0, 0, 0, time.UTC)
// Insert a user with zero created_at but valid updated_at
require.NoError(t, db.Exec(
"INSERT INTO users (identifier, email, created_at, updated_at) VALUES (?, ?, ?, ?)",
"zero-user", "zero@example.com", time.Time{}, updatedAt,
).Error)
// Run migration
repo := &SqlRepo{db: db, cfg: &config.Config{}}
require.NoError(t, repo.migrate())
// Verify created_at was backfilled from updated_at
var user domain.User
require.NoError(t, db.First(&user, "identifier = ?", "zero-user").Error)
assert.Equal(t, updatedAt, user.CreatedAt, "created_at should be backfilled from updated_at")
// Verify schema version advanced to 4
var latest SysStat
require.NoError(t, db.Order("schema_version DESC").First(&latest).Error)
assert.Equal(t, uint64(4), latest.SchemaVersion)
}
func TestMigration_DoesNotTouchValidCreatedAt(t *testing.T) {
db := newTestDB(t)
require.NoError(t, db.AutoMigrate(
&SysStat{},
&domain.User{},
&domain.UserAuthentication{},
&domain.Interface{},
&domain.Cidr{},
&domain.Peer{},
&domain.AuditEntry{},
&domain.UserWebauthnCredential{},
))
for v := uint64(1); v <= 3; v++ {
require.NoError(t, db.Create(&SysStat{SchemaVersion: v, MigratedAt: time.Now()}).Error)
}
createdAt := time.Date(2024, 3, 1, 8, 0, 0, 0, time.UTC)
updatedAt := time.Date(2025, 6, 15, 10, 0, 0, 0, time.UTC)
require.NoError(t, db.Exec(
"INSERT INTO users (identifier, email, created_at, updated_at) VALUES (?, ?, ?, ?)",
"valid-user", "valid@example.com", createdAt, updatedAt,
).Error)
repo := &SqlRepo{db: db, cfg: &config.Config{}}
require.NoError(t, repo.migrate())
var user domain.User
require.NoError(t, db.First(&user, "identifier = ?", "valid-user").Error)
assert.Equal(t, createdAt, user.CreatedAt, "valid created_at should not be modified")
}

View File

@@ -1,73 +0,0 @@
package adapters
import (
"context"
"reflect"
"testing"
"github.com/glebarez/sqlite"
"github.com/h44z/wg-portal/internal/config"
"github.com/h44z/wg-portal/internal/domain"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
"gorm.io/gorm/schema"
)
func init() {
schema.RegisterSerializer("encstr", dummySerializer{})
}
type dummySerializer struct{}
func (dummySerializer) Scan(ctx context.Context, field *schema.Field, dst reflect.Value, dbValue any) error {
return nil
}
func (dummySerializer) Value(ctx context.Context, field *schema.Field, dst reflect.Value, fieldValue any) (any, error) {
if fieldValue == nil {
return nil, nil
}
if v, ok := fieldValue.(string); ok {
return v, nil
}
if v, ok := fieldValue.(domain.PreSharedKey); ok {
return string(v), nil
}
return fieldValue, nil
}
func TestSqlRepo_SaveInterface_Simple(t *testing.T) {
// Initialize in-memory database
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
require.NoError(t, err)
// Migrate only what's needed for this test (avoids Peer and its encstr serializer)
require.NoError(t, db.AutoMigrate(&domain.Interface{}, &domain.Cidr{}))
repo := &SqlRepo{db: db, cfg: &config.Config{}}
ctx := domain.SetUserInfo(context.Background(), domain.SystemAdminContextUserInfo())
ifaceId := domain.InterfaceIdentifier("wg0")
// 1. Create an interface with one address
addr, _ := domain.CidrFromString("10.0.0.1/24")
initialIface := &domain.Interface{
Identifier: ifaceId,
Addresses: []domain.Cidr{addr},
}
require.NoError(t, db.Create(initialIface).Error)
// 2. Perform a "partial" update using SaveInterface (this is the buggy path)
err = repo.SaveInterface(ctx, ifaceId, func(in *domain.Interface) (*domain.Interface, error) {
in.DisplayName = "New Name"
return in, nil
})
require.NoError(t, err)
// 3. Verify that the address was NOT deleted
var finalIface domain.Interface
require.NoError(t, db.Preload("Addresses").First(&finalIface, "identifier = ?", ifaceId).Error)
require.Equal(t, "New Name", finalIface.DisplayName)
require.Len(t, finalIface.Addresses, 1, "Address list should still have 1 entry!")
require.Equal(t, "10.0.0.1/24", finalIface.Addresses[0].Cidr)
}

View File

@@ -30,7 +30,7 @@ type MetricsServer struct {
// Wireguard metrics labels // Wireguard metrics labels
var ( var (
ifaceLabels = []string{"interface"} ifaceLabels = []string{"interface"}
peerLabels = []string{"interface", "addresses", "id", "name", "user"} peerLabels = []string{"interface", "addresses", "id", "name"}
) )
// NewMetricsServer returns a new prometheus server // NewMetricsServer returns a new prometheus server
@@ -126,7 +126,6 @@ func (m *MetricsServer) UpdatePeerMetrics(peer *domain.Peer, status domain.PeerS
peer.Interface.AddressStr(), peer.Interface.AddressStr(),
string(status.PeerId), string(status.PeerId),
peer.DisplayName, peer.DisplayName,
string(peer.UserIdentifier),
} }
if status.LastHandshake != nil { if status.LastHandshake != nil {

View File

@@ -54,17 +54,6 @@ func (u UserService) GetAllUsers(ctx context.Context) ([]domain.User, error) {
} }
func (u UserService) UpdateUser(ctx context.Context, user *domain.User) (*domain.User, error) { func (u UserService) UpdateUser(ctx context.Context, user *domain.User) (*domain.User, error) {
sessionUser := domain.GetUserInfo(ctx)
currentUser, err := u.users.GetUser(ctx, user.Identifier)
if err != nil {
return nil, err
}
// if this endpoint is used by non-admins, make sure that the user can only modify a specific subset of attributes
if !sessionUser.IsAdmin {
user.CopyAdminAttributes(currentUser, u.cfg.Advanced.ApiAdminOnly)
}
return u.users.UpdateUser(ctx, user) return u.users.UpdateUser(ctx, user)
} }

View File

@@ -6,6 +6,7 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/go-pkgz/routegroup" "github.com/go-pkgz/routegroup"
@@ -448,17 +449,7 @@ func (e AuthEndpoint) handleLogoutPost() http.HandlerFunc {
// isValidReturnUrl checks if the given return URL matches the configured external URL of the application. // isValidReturnUrl checks if the given return URL matches the configured external URL of the application.
func (e AuthEndpoint) isValidReturnUrl(returnUrl string) bool { func (e AuthEndpoint) isValidReturnUrl(returnUrl string) bool {
expectedUrl, err := url.Parse(e.cfg.Web.ExternalUrl) if !strings.HasPrefix(returnUrl, e.cfg.Web.ExternalUrl) {
if err != nil {
return false
}
returnUrlParsed, err := url.Parse(returnUrl)
if err != nil {
return false
}
if returnUrlParsed.Scheme != expectedUrl.Scheme || returnUrlParsed.Host != expectedUrl.Host {
return false return false
} }

View File

@@ -351,6 +351,7 @@ func (a *Authenticator) passwordAuthentication(
domain.SystemAdminContextUserInfo()) // switch to admin user context to check if user exists domain.SystemAdminContextUserInfo()) // switch to admin user context to check if user exists
var ldapUserInfo *domain.AuthenticatorUserInfo var ldapUserInfo *domain.AuthenticatorUserInfo
var ldapProvider AuthenticatorLdap
var userInDatabase = false var userInDatabase = false
existingUser, err := a.users.GetUser(ctx, identifier) existingUser, err := a.users.GetUser(ctx, identifier)
@@ -416,14 +417,14 @@ func (a *Authenticator) passwordAuthentication(
"source", ldapAuth.GetName(), "identifier", identifier, "error", err) "source", ldapAuth.GetName(), "identifier", identifier, "error", err)
continue continue
} }
user, err := a.processUserInfo(ctx, ldapUserInfo, domain.UserSourceLdap, ldapAuth.GetName(), true) user, err := a.processUserInfo(ctx, ldapUserInfo, domain.UserSourceLdap, ldapProvider.GetName(), true)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to process user information: %w", err) return nil, fmt.Errorf("unable to process user information: %w", err)
} }
existingUser = user existingUser = user
slog.Debug("created new LDAP user in db", slog.Debug("created new LDAP user in db",
"identifier", user.Identifier, "provider", ldapAuth.GetName()) "identifier", user.Identifier, "provider", ldapProvider.GetName())
authOK = true authOK = true
break break

View File

@@ -190,21 +190,19 @@ func (m Manager) resolveEmail(ctx context.Context, peer *domain.Peer) (string, d
if err == nil { if err == nil {
slog.Debug("peer email: using user-identifier as email", slog.Debug("peer email: using user-identifier as email",
"peer", peer.Identifier, "email", peer.UserIdentifier) "peer", peer.Identifier, "email", peer.UserIdentifier)
return string(peer.UserIdentifier), domain.User{ return string(peer.UserIdentifier), domain.User{}
Email: string(peer.UserIdentifier), } else {
} slog.Debug("peer email: skipping peer email",
"peer", peer.Identifier,
"reason", "peer has no user linked and user-identifier is not a valid email address")
return "", domain.User{}
} }
} else {
slog.Debug("peer email: skipping peer email", slog.Debug("peer email: skipping peer email",
"peer", peer.Identifier, "peer", peer.Identifier,
"reason", "peer has no user linked and user-identifier is not a valid email address") "reason", "user has no user linked")
return "", domain.User{} return "", domain.User{}
} }
slog.Debug("peer email: skipping peer email",
"peer", peer.Identifier,
"reason", "user has no user linked")
return "", domain.User{}
} }
if user.Email == "" { if user.Email == "" {

View File

@@ -90,12 +90,6 @@ func (m Manager) synchronizeLdapUsers(ctx context.Context, provider *config.Ldap
} }
} }
// Update interface allowed users based on LDAP filters
err = m.updateInterfaceLdapFilters(ctx, conn, provider)
if err != nil {
return err
}
return nil return nil
} }
@@ -243,59 +237,3 @@ func (m Manager) disableMissingLdapUsers(
return nil return nil
} }
func (m Manager) updateInterfaceLdapFilters(
ctx context.Context,
conn *ldap.Conn,
provider *config.LdapProvider,
) error {
if len(provider.InterfaceFilter) == 0 {
return nil // nothing to do if no interfaces are configured for this provider
}
for ifaceName, groupFilter := range provider.InterfaceFilter {
ifaceId := domain.InterfaceIdentifier(ifaceName)
// Combined filter: user must match the provider's base SyncFilter AND the interface's LdapGroupFilter
combinedFilter := fmt.Sprintf("(&(%s)(%s))", provider.SyncFilter, groupFilter)
rawUsers, err := internal.LdapFindAllUsers(conn, provider.BaseDN, combinedFilter, &provider.FieldMap)
if err != nil {
slog.Error("failed to find users for interface filter",
"interface", ifaceId,
"provider", provider.ProviderName,
"error", err)
continue
}
matchedUserIds := make([]domain.UserIdentifier, 0, len(rawUsers))
for _, rawUser := range rawUsers {
userId := domain.UserIdentifier(internal.MapDefaultString(rawUser, provider.FieldMap.UserIdentifier, ""))
if userId != "" {
matchedUserIds = append(matchedUserIds, userId)
}
}
// Save the interface
err = m.interfaces.SaveInterface(ctx, ifaceId, func(i *domain.Interface) (*domain.Interface, error) {
if i.LdapAllowedUsers == nil {
i.LdapAllowedUsers = make(map[string][]domain.UserIdentifier)
}
i.LdapAllowedUsers[provider.ProviderName] = matchedUserIds
return i, nil
})
if err != nil {
slog.Error("failed to save interface ldap allowed users",
"interface", ifaceId,
"provider", provider.ProviderName,
"error", err)
} else {
slog.Debug("updated interface ldap allowed users",
"interface", ifaceId,
"provider", provider.ProviderName,
"matched_count", len(matchedUserIds))
}
}
return nil
}

View File

@@ -39,11 +39,6 @@ type PeerDatabaseRepo interface {
GetUserPeers(ctx context.Context, id domain.UserIdentifier) ([]domain.Peer, error) GetUserPeers(ctx context.Context, id domain.UserIdentifier) ([]domain.Peer, error)
} }
type InterfaceDatabaseRepo interface {
// SaveInterface saves the interface with the given identifier.
SaveInterface(ctx context.Context, id domain.InterfaceIdentifier, updateFunc func(i *domain.Interface) (*domain.Interface, error)) error
}
type EventBus interface { type EventBus interface {
// Publish sends a message to the message bus. // Publish sends a message to the message bus.
Publish(topic string, args ...any) Publish(topic string, args ...any)
@@ -55,27 +50,22 @@ type EventBus interface {
type Manager struct { type Manager struct {
cfg *config.Config cfg *config.Config
bus EventBus bus EventBus
users UserDatabaseRepo users UserDatabaseRepo
peers PeerDatabaseRepo peers PeerDatabaseRepo
interfaces InterfaceDatabaseRepo
} }
// NewUserManager creates a new user manager instance. // NewUserManager creates a new user manager instance.
func NewUserManager( func NewUserManager(cfg *config.Config, bus EventBus, users UserDatabaseRepo, peers PeerDatabaseRepo) (
cfg *config.Config, *Manager,
bus EventBus, error,
users UserDatabaseRepo, ) {
peers PeerDatabaseRepo,
interfaces InterfaceDatabaseRepo,
) (*Manager, error) {
m := &Manager{ m := &Manager{
cfg: cfg, cfg: cfg,
bus: bus, bus: bus,
users: users, users: users,
peers: peers, peers: peers,
interfaces: interfaces,
} }
return m, nil return m, nil
} }
@@ -282,9 +272,8 @@ func (m Manager) DeactivateApi(ctx context.Context, id domain.UserIdentifier) (*
func (m Manager) validateModifications(ctx context.Context, old, new *domain.User) error { func (m Manager) validateModifications(ctx context.Context, old, new *domain.User) error {
currentUser := domain.GetUserInfo(ctx) currentUser := domain.GetUserInfo(ctx)
adminErrors := m.validateAdminModifications(ctx, old, new) if currentUser.Id != new.Identifier && !currentUser.IsAdmin {
if adminErrors != nil { return fmt.Errorf("insufficient permissions")
return adminErrors
} }
if err := old.EditAllowed(new); err != nil && currentUser.Id != domain.SystemAdminContextUserInfo().Id { if err := old.EditAllowed(new); err != nil && currentUser.Id != domain.SystemAdminContextUserInfo().Id {
@@ -314,46 +303,6 @@ func (m Manager) validateModifications(ctx context.Context, old, new *domain.Use
return nil return nil
} }
func (m Manager) validateAdminModifications(ctx context.Context, old, new *domain.User) error {
currentUser := domain.GetUserInfo(ctx)
if currentUser.IsAdmin {
if currentUser.Id == old.Identifier && !new.IsAdmin {
return fmt.Errorf("cannot remove own admin rights: %w", domain.ErrInvalidData)
}
return nil // admins can do (almost) everything
}
// non-admins can only modify very their own profile data
if currentUser.Id != new.Identifier {
return fmt.Errorf("insufficient permissions: %w", domain.ErrInvalidData)
}
if new.IsAdmin {
return fmt.Errorf("cannot grant admin rights: %w", domain.ErrInvalidData)
}
if new.Notes != old.Notes {
return fmt.Errorf("cannot update notes: %w", domain.ErrInvalidData)
}
if old.Locked != new.Locked || old.LockedReason != new.LockedReason {
return fmt.Errorf("cannot change lock state: %w", domain.ErrInvalidData)
}
if old.Disabled != new.Disabled || old.DisabledReason != new.DisabledReason {
return fmt.Errorf("cannot change disabled state: %w", domain.ErrInvalidData)
}
if old.PersistLocalChanges != new.PersistLocalChanges {
return fmt.Errorf("cannot change disabled state: %w", domain.ErrInvalidData)
}
return nil
}
func (m Manager) validateCreation(ctx context.Context, new *domain.User) error { func (m Manager) validateCreation(ctx context.Context, new *domain.User) error {
currentUser := domain.GetUserInfo(ctx) currentUser := domain.GetUserInfo(ctx)
@@ -425,10 +374,6 @@ func (m Manager) validateDeletion(ctx context.Context, del *domain.User) error {
func (m Manager) validateApiChange(ctx context.Context, user *domain.User) error { func (m Manager) validateApiChange(ctx context.Context, user *domain.User) error {
currentUser := domain.GetUserInfo(ctx) currentUser := domain.GetUserInfo(ctx)
if !currentUser.IsAdmin && m.cfg.Advanced.ApiAdminOnly {
return fmt.Errorf("insufficient permissions to change API access: %w", domain.ErrNoPermission)
}
if currentUser.Id != user.Identifier { if currentUser.Id != user.Identifier {
return fmt.Errorf("cannot change API access of user: %w", domain.ErrNoPermission) return fmt.Errorf("cannot change API access of user: %w", domain.ErrNoPermission)
} }
@@ -533,7 +478,6 @@ func (m Manager) create(ctx context.Context, user *domain.User) (*domain.User, e
} }
err = m.users.SaveUser(ctx, user.Identifier, func(u *domain.User) (*domain.User, error) { err = m.users.SaveUser(ctx, user.Identifier, func(u *domain.User) (*domain.User, error) {
user.CopyCalculatedAttributes(u, false)
return user, nil return user, nil
}) })
if err != nil { if err != nil {

View File

@@ -204,13 +204,13 @@ func (c *StatisticsCollector) collectPeerData(ctx context.Context) {
// calculate if session was restarted // calculate if session was restarted
p.UpdatedAt = now p.UpdatedAt = now
p.LastSessionStart = c.getSessionStartTime(*p, peer.BytesUpload, peer.BytesDownload, p.LastSessionStart = getSessionStartTime(*p, peer.BytesUpload, peer.BytesDownload,
lastHandshake) lastHandshake)
p.BytesReceived = peer.BytesUpload // store bytes that where uploaded from the peer and received by the server p.BytesReceived = peer.BytesUpload // store bytes that where uploaded from the peer and received by the server
p.BytesTransmitted = peer.BytesDownload // store bytes that where received from the peer and sent by the server p.BytesTransmitted = peer.BytesDownload // store bytes that where received from the peer and sent by the server
p.Endpoint = peer.Endpoint p.Endpoint = peer.Endpoint
p.LastHandshake = lastHandshake p.LastHandshake = lastHandshake
p.CalcConnected(c.cfg.Backend.ReKeyTimeoutInterval) p.CalcConnected()
if wasConnected != p.IsConnected { if wasConnected != p.IsConnected {
slog.Debug("peer connection state changed", slog.Debug("peer connection state changed",
@@ -249,7 +249,7 @@ func (c *StatisticsCollector) collectPeerData(ctx context.Context) {
} }
} }
func (c *StatisticsCollector) getSessionStartTime( func getSessionStartTime(
oldStats domain.PeerStatus, oldStats domain.PeerStatus,
newReceived, newTransmitted uint64, newReceived, newTransmitted uint64,
latestHandshake *time.Time, latestHandshake *time.Time,
@@ -258,7 +258,7 @@ func (c *StatisticsCollector) getSessionStartTime(
return nil // currently not connected return nil // currently not connected
} }
oldestHandshakeTime := time.Now().Add(-1 * c.cfg.Backend.ReKeyTimeoutInterval) // if a handshake is older than the rekey interval + grace-period, the peer is no longer connected oldestHandshakeTime := time.Now().Add(-2 * time.Minute) // if a handshake is older than 2 minutes, the peer is no longer connected
switch { switch {
// old session was never initiated // old session was never initiated
case oldStats.BytesReceived == 0 && oldStats.BytesTransmitted == 0 && (newReceived > 0 || newTransmitted > 0): case oldStats.BytesReceived == 0 && oldStats.BytesTransmitted == 0 && (newReceived > 0 || newTransmitted > 0):
@@ -369,7 +369,7 @@ func (c *StatisticsCollector) pingWorker(ctx context.Context) {
p.LastPing = nil p.LastPing = nil
} }
p.UpdatedAt = time.Now() p.UpdatedAt = time.Now()
p.CalcConnected(c.cfg.Backend.ReKeyTimeoutInterval) p.CalcConnected()
if wasConnected != p.IsConnected { if wasConnected != p.IsConnected {
connectionStateChanged = true connectionStateChanged = true

View File

@@ -5,11 +5,10 @@ import (
"testing" "testing"
"time" "time"
"github.com/h44z/wg-portal/internal/config"
"github.com/h44z/wg-portal/internal/domain" "github.com/h44z/wg-portal/internal/domain"
) )
func TestStatisticsCollector_getSessionStartTime(t *testing.T) { func Test_getSessionStartTime(t *testing.T) {
now := time.Now() now := time.Now()
nowMinus1 := now.Add(-1 * time.Minute) nowMinus1 := now.Add(-1 * time.Minute)
nowMinus3 := now.Add(-3 * time.Minute) nowMinus3 := now.Add(-3 * time.Minute)
@@ -134,14 +133,7 @@ func TestStatisticsCollector_getSessionStartTime(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
c := &StatisticsCollector{ if got := getSessionStartTime(tt.args.oldStats, tt.args.newReceived, tt.args.newTransmitted,
cfg: &config.Config{
Backend: config.Backend{
ReKeyTimeoutInterval: 180 * time.Second,
},
},
}
if got := c.getSessionStartTime(tt.args.oldStats, tt.args.newReceived, tt.args.newTransmitted,
tt.args.lastHandshake); !reflect.DeepEqual(got, tt.want) { tt.args.lastHandshake); !reflect.DeepEqual(got, tt.want) {
t.Errorf("getSessionStartTime() = %v, want %v", got, tt.want) t.Errorf("getSessionStartTime() = %v, want %v", got, tt.want)
} }

View File

@@ -35,7 +35,6 @@ type InterfaceAndPeerDatabaseRepo interface {
DeletePeer(ctx context.Context, id domain.PeerIdentifier) error DeletePeer(ctx context.Context, id domain.PeerIdentifier) error
GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error) GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error)
GetUsedIpsPerSubnet(ctx context.Context, subnets []domain.Cidr) (map[domain.Cidr][]domain.Cidr, error) GetUsedIpsPerSubnet(ctx context.Context, subnets []domain.Cidr) (map[domain.Cidr][]domain.Cidr, error)
GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error)
} }
type WgQuickController interface { type WgQuickController interface {

View File

@@ -16,11 +16,6 @@ import (
"github.com/h44z/wg-portal/internal/domain" "github.com/h44z/wg-portal/internal/domain"
) )
// GetInterface returns the interface for the given interface identifier.
func (m Manager) GetInterface(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, error) {
return m.db.GetInterface(ctx, id)
}
// GetInterfaceAndPeers returns the interface and all peers for the given interface identifier. // GetInterfaceAndPeers returns the interface and all peers for the given interface identifier.
func (m Manager) GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceIdentifier) ( func (m Manager) GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceIdentifier) (
*domain.Interface, *domain.Interface,
@@ -68,17 +63,12 @@ func (m Manager) GetAllInterfacesAndPeers(ctx context.Context) ([]domain.Interfa
// GetUserInterfaces returns all interfaces that are available for users to create new peers. // GetUserInterfaces returns all interfaces that are available for users to create new peers.
// If self-provisioning is disabled, this function will return an empty list. // If self-provisioning is disabled, this function will return an empty list.
func (m Manager) GetUserInterfaces(ctx context.Context, userId domain.UserIdentifier) ([]domain.Interface, error) { // At the moment, there are no interfaces specific to single users, thus the user id is not used.
func (m Manager) GetUserInterfaces(ctx context.Context, _ domain.UserIdentifier) ([]domain.Interface, error) {
if !m.cfg.Core.SelfProvisioningAllowed { if !m.cfg.Core.SelfProvisioningAllowed {
return nil, nil // self-provisioning is disabled - no interfaces for users return nil, nil // self-provisioning is disabled - no interfaces for users
} }
user, err := m.db.GetUser(ctx, userId)
if err != nil {
slog.Error("failed to load user for interface group verification", "user", userId, "error", err)
return nil, nil // fail closed
}
interfaces, err := m.db.GetAllInterfaces(ctx) interfaces, err := m.db.GetAllInterfaces(ctx)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to load all interfaces: %w", err) return nil, fmt.Errorf("unable to load all interfaces: %w", err)
@@ -93,9 +83,6 @@ func (m Manager) GetUserInterfaces(ctx context.Context, userId domain.UserIdenti
if iface.Type != domain.InterfaceTypeServer { if iface.Type != domain.InterfaceTypeServer {
continue // skip client interfaces continue // skip client interfaces
} }
if !user.IsAdmin && !iface.IsUserAllowed(userId, m.cfg) {
continue // user not allowed due to LDAP group filter
}
userInterfaces = append(userInterfaces, iface.PublicInfo()) userInterfaces = append(userInterfaces, iface.PublicInfo())
} }
@@ -998,26 +985,7 @@ func (m Manager) importPeer(ctx context.Context, in *domain.Interface, p *domain
peer.InterfaceIdentifier = in.Identifier peer.InterfaceIdentifier = in.Identifier
peer.EndpointPublicKey = domain.NewConfigOption(in.PublicKey, true) peer.EndpointPublicKey = domain.NewConfigOption(in.PublicKey, true)
peer.AllowedIPsStr = domain.NewConfigOption(in.PeerDefAllowedIPsStr, true) peer.AllowedIPsStr = domain.NewConfigOption(in.PeerDefAllowedIPsStr, true)
peer.Interface.Addresses = p.AllowedIPs // use allowed IP's as the peer IP's TODO: Should this also match server interface address' prefix length?
// split allowed IP's into interface addresses and extra allowed IP's
var interfaceAddresses []domain.Cidr
var extraAllowedIPs []domain.Cidr
for _, allowedIP := range p.AllowedIPs {
isHost := (allowedIP.IsV4() && allowedIP.NetLength == 32) || (!allowedIP.IsV4() && allowedIP.NetLength == 128)
isNetworkAddr := allowedIP.Addr == allowedIP.NetworkAddr().Addr
// Network addresses (e.g. 10.0.0.0/24) will always be extra allowed IP's.
// For IP addresses, such as 10.0.0.1/24, it is challenging to tell whether it is an interface address or
// an extra allowed IP, therefore we treat such addresses as interface addresses.
if !isHost && isNetworkAddr {
extraAllowedIPs = append(extraAllowedIPs, allowedIP)
} else {
interfaceAddresses = append(interfaceAddresses, allowedIP)
}
}
peer.Interface.Addresses = interfaceAddresses
peer.ExtraAllowedIPsStr = domain.CidrsToString(extraAllowedIPs)
peer.Interface.DnsStr = domain.NewConfigOption(in.PeerDefDnsStr, true) peer.Interface.DnsStr = domain.NewConfigOption(in.PeerDefDnsStr, true)
peer.Interface.DnsSearchStr = domain.NewConfigOption(in.PeerDefDnsSearchStr, true) peer.Interface.DnsSearchStr = domain.NewConfigOption(in.PeerDefDnsSearchStr, true)
peer.Interface.Mtu = domain.NewConfigOption(in.PeerDefMtu, true) peer.Interface.Mtu = domain.NewConfigOption(in.PeerDefMtu, true)

View File

@@ -1,218 +0,0 @@
package wireguard
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/h44z/wg-portal/internal/config"
"github.com/h44z/wg-portal/internal/domain"
)
func TestImportPeer_AddressMapping(t *testing.T) {
tests := []struct {
name string
allowedIPs []string
expectedInterface []string
expectedExtraAllowed string
}{
{
name: "IPv4 host address",
allowedIPs: []string{"10.0.0.1/32"},
expectedInterface: []string{"10.0.0.1/32"},
expectedExtraAllowed: "",
},
{
name: "IPv6 host address",
allowedIPs: []string{"fd00::1/128"},
expectedInterface: []string{"fd00::1/128"},
expectedExtraAllowed: "",
},
{
name: "IPv4 network address",
allowedIPs: []string{"10.0.1.0/24"},
expectedInterface: []string{},
expectedExtraAllowed: "10.0.1.0/24",
},
{
name: "IPv4 normal address with mask",
allowedIPs: []string{"10.0.1.5/24"},
expectedInterface: []string{"10.0.1.5/24"},
expectedExtraAllowed: "",
},
{
name: "Mixed addresses",
allowedIPs: []string{
"10.0.0.1/32", "192.168.1.0/24", "172.16.0.5/24", "fd00::1/128", "fd00:1::/64",
},
expectedInterface: []string{"10.0.0.1/32", "172.16.0.5/24", "fd00::1/128"},
expectedExtraAllowed: "192.168.1.0/24,fd00:1::/64",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
db := &mockDB{}
m := Manager{
db: db,
}
iface := &domain.Interface{
Identifier: "wg0",
Type: domain.InterfaceTypeServer,
}
allowedIPs := make([]domain.Cidr, len(tt.allowedIPs))
for i, s := range tt.allowedIPs {
cidr, _ := domain.CidrFromString(s)
allowedIPs[i] = cidr
}
p := &domain.PhysicalPeer{
Identifier: "peer1",
KeyPair: domain.KeyPair{PublicKey: "peer1-public-key-is-long-enough"},
AllowedIPs: allowedIPs,
}
err := m.importPeer(context.Background(), iface, p)
assert.NoError(t, err)
savedPeer := db.savedPeers["peer1"]
assert.NotNil(t, savedPeer)
// Check interface addresses
actualInterface := make([]string, len(savedPeer.Interface.Addresses))
for i, addr := range savedPeer.Interface.Addresses {
actualInterface[i] = addr.String()
}
assert.ElementsMatch(t, tt.expectedInterface, actualInterface)
// Check extra allowed IPs
assert.Equal(t, tt.expectedExtraAllowed, savedPeer.ExtraAllowedIPsStr)
})
}
}
func (f *mockDB) GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error) {
return &domain.User{
Identifier: id,
IsAdmin: false,
}, nil
}
func TestInterface_IsUserAllowed(t *testing.T) {
cfg := &config.Config{
Auth: config.Auth{
Ldap: []config.LdapProvider{
{
ProviderName: "ldap1",
InterfaceFilter: map[string]string{
"wg0": "(memberOf=CN=VPNUsers,...)",
},
},
},
},
}
tests := []struct {
name string
iface domain.Interface
userId domain.UserIdentifier
expect bool
}{
{
name: "Unrestricted interface",
iface: domain.Interface{
Identifier: "wg1",
},
userId: "user1",
expect: true,
},
{
name: "Restricted interface - user allowed",
iface: domain.Interface{
Identifier: "wg0",
LdapAllowedUsers: map[string][]domain.UserIdentifier{
"ldap1": {"user1"},
},
},
userId: "user1",
expect: true,
},
{
name: "Restricted interface - user allowed (at least one match)",
iface: domain.Interface{
Identifier: "wg0",
LdapAllowedUsers: map[string][]domain.UserIdentifier{
"ldap1": {"user2"},
"ldap2": {"user1"},
},
},
userId: "user1",
expect: true,
},
{
name: "Restricted interface - user NOT allowed",
iface: domain.Interface{
Identifier: "wg0",
LdapAllowedUsers: map[string][]domain.UserIdentifier{
"ldap1": {"user2"},
},
},
userId: "user1",
expect: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.expect, tt.iface.IsUserAllowed(tt.userId, cfg))
})
}
}
func TestManager_GetUserInterfaces_Filtering(t *testing.T) {
cfg := &config.Config{}
cfg.Core.SelfProvisioningAllowed = true
cfg.Auth.Ldap = []config.LdapProvider{
{
ProviderName: "ldap1",
InterfaceFilter: map[string]string{
"wg_restricted": "(some-filter)",
},
},
}
db := &mockDB{
interfaces: []domain.Interface{
{Identifier: "wg_public", Type: domain.InterfaceTypeServer},
{
Identifier: "wg_restricted",
Type: domain.InterfaceTypeServer,
LdapAllowedUsers: map[string][]domain.UserIdentifier{
"ldap1": {"allowed_user"},
},
},
},
}
m := Manager{
cfg: cfg,
db: db,
}
t.Run("Allowed user sees both", func(t *testing.T) {
ifaces, err := m.GetUserInterfaces(context.Background(), "allowed_user")
assert.NoError(t, err)
assert.Equal(t, 2, len(ifaces))
})
t.Run("Unallowed user sees only public", func(t *testing.T) {
ifaces, err := m.GetUserInterfaces(context.Background(), "other_user")
assert.NoError(t, err)
assert.Equal(t, 1, len(ifaces))
if len(ifaces) > 0 {
assert.Equal(t, domain.InterfaceIdentifier("wg_public"), ifaces[0].Identifier)
}
})
}

View File

@@ -93,10 +93,6 @@ func (m Manager) PreparePeer(ctx context.Context, id domain.InterfaceIdentifier)
currentUser := domain.GetUserInfo(ctx) currentUser := domain.GetUserInfo(ctx)
if err := m.checkInterfaceAccess(ctx, id); err != nil {
return nil, err
}
iface, err := m.db.GetInterface(ctx, id) iface, err := m.db.GetInterface(ctx, id)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to find interface %s: %w", id, err) return nil, fmt.Errorf("unable to find interface %s: %w", id, err)
@@ -192,9 +188,6 @@ func (m Manager) CreatePeer(ctx context.Context, peer *domain.Peer) (*domain.Pee
if err := domain.ValidateUserAccessRights(ctx, peer.UserIdentifier); err != nil { if err := domain.ValidateUserAccessRights(ctx, peer.UserIdentifier); err != nil {
return nil, err return nil, err
} }
if err := m.checkInterfaceAccess(ctx, peer.InterfaceIdentifier); err != nil {
return nil, err
}
} }
sessionUser := domain.GetUserInfo(ctx) sessionUser := domain.GetUserInfo(ctx)
@@ -311,10 +304,6 @@ func (m Manager) UpdatePeer(ctx context.Context, peer *domain.Peer) (*domain.Pee
return nil, err return nil, err
} }
if err := m.checkInterfaceAccess(ctx, existingPeer.InterfaceIdentifier); err != nil {
return nil, err
}
if err := m.validatePeerModifications(ctx, existingPeer, peer); err != nil { if err := m.validatePeerModifications(ctx, existingPeer, peer); err != nil {
return nil, fmt.Errorf("update not allowed: %w", err) return nil, fmt.Errorf("update not allowed: %w", err)
} }
@@ -384,10 +373,6 @@ func (m Manager) DeletePeer(ctx context.Context, id domain.PeerIdentifier) error
return err return err
} }
if err := m.checkInterfaceAccess(ctx, peer.InterfaceIdentifier); err != nil {
return err
}
if err := m.validatePeerDeletion(ctx, peer); err != nil { if err := m.validatePeerDeletion(ctx, peer); err != nil {
return fmt.Errorf("delete not allowed: %w", err) return fmt.Errorf("delete not allowed: %w", err)
} }
@@ -621,22 +606,4 @@ func (m Manager) validatePeerDeletion(ctx context.Context, _ *domain.Peer) error
return nil return nil
} }
func (m Manager) checkInterfaceAccess(ctx context.Context, id domain.InterfaceIdentifier) error {
user := domain.GetUserInfo(ctx)
if user.IsAdmin {
return nil
}
iface, err := m.db.GetInterface(ctx, id)
if err != nil {
return fmt.Errorf("failed to get interface %s: %w", id, err)
}
if !iface.IsUserAllowed(user.Id, m.cfg) {
return fmt.Errorf("user %s is not allowed to access interface %s: %w", user.Id, id, domain.ErrNoPermission)
}
return nil
}
// endregion helper-functions // endregion helper-functions

View File

@@ -60,7 +60,6 @@ func (f *mockController) PingAddresses(_ context.Context, _ string) (*domain.Pin
type mockDB struct { type mockDB struct {
savedPeers map[domain.PeerIdentifier]*domain.Peer savedPeers map[domain.PeerIdentifier]*domain.Peer
iface *domain.Interface iface *domain.Interface
interfaces []domain.Interface
} }
func (f *mockDB) GetInterface(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, error) { func (f *mockDB) GetInterface(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, error) {
@@ -80,9 +79,6 @@ func (f *mockDB) GetPeersStats(ctx context.Context, ids ...domain.PeerIdentifier
return nil, nil return nil, nil
} }
func (f *mockDB) GetAllInterfaces(ctx context.Context) ([]domain.Interface, error) { func (f *mockDB) GetAllInterfaces(ctx context.Context) ([]domain.Interface, error) {
if f.interfaces != nil {
return f.interfaces, nil
}
if f.iface != nil { if f.iface != nil {
return []domain.Interface{*f.iface}, nil return []domain.Interface{*f.iface}, nil
} }

View File

@@ -214,10 +214,6 @@ type LdapProvider struct {
// If RegistrationEnabled is set to true, wg-portal will create new users that do not exist in the database. // If RegistrationEnabled is set to true, wg-portal will create new users that do not exist in the database.
RegistrationEnabled bool `yaml:"registration_enabled"` RegistrationEnabled bool `yaml:"registration_enabled"`
// InterfaceFilter allows restricting interfaces using an LDAP filter.
// Map key is the interface identifier (e.g., "wg0"), value is the filter string.
InterfaceFilter map[string]string `yaml:"interface_filter"`
// If LogUserInfo is set to true, the user info retrieved from the LDAP provider will be logged in trace level. // If LogUserInfo is set to true, the user info retrieved from the LDAP provider will be logged in trace level.
LogUserInfo bool `yaml:"log_user_info"` LogUserInfo bool `yaml:"log_user_info"`
} }

View File

@@ -10,8 +10,6 @@ const LocalBackendName = "local"
type Backend struct { type Backend struct {
Default string `yaml:"default"` // The default backend to use (defaults to the internal backend) Default string `yaml:"default"` // The default backend to use (defaults to the internal backend)
ReKeyTimeoutInterval time.Duration `yaml:"rekey_timeout_interval"` // Interval after which a connection is assumed dead
// Local Backend-specific configuration // Local Backend-specific configuration
IgnoredLocalInterfaces []string `yaml:"ignored_local_interfaces"` // A list of interface names that should be ignored by this backend (e.g., "wg0") IgnoredLocalInterfaces []string `yaml:"ignored_local_interfaces"` // A list of interface names that should be ignored by this backend (e.g., "wg0")
@@ -117,8 +115,8 @@ func (b *BackendMikrotik) GetApiTimeout() time.Duration {
type BackendPfsense struct { type BackendPfsense struct {
BackendBase `yaml:",inline"` // Embed the base fields BackendBase `yaml:",inline"` // Embed the base fields
ApiUrl string `yaml:"api_url"` // The base URL of the pfSense REST API (e.g., "https://pfsense.example.com/api/v2") ApiUrl string `yaml:"api_url"` // The base URL of the pfSense REST API (e.g., "https://pfsense.example.com/api/v2")
ApiKey string `yaml:"api_key"` // API key for authentication (generated in pfSense under 'System' -> 'REST API' -> 'Keys') ApiKey string `yaml:"api_key"` // API key for authentication (generated in pfSense under 'System' -> 'REST API' -> 'Keys')
ApiVerifyTls bool `yaml:"api_verify_tls"` // Whether to verify the TLS certificate of the pfSense API ApiVerifyTls bool `yaml:"api_verify_tls"` // Whether to verify the TLS certificate of the pfSense API
ApiTimeout time.Duration `yaml:"api_timeout"` // Timeout for API requests (default: 30 seconds) ApiTimeout time.Duration `yaml:"api_timeout"` // Timeout for API requests (default: 30 seconds)

View File

@@ -139,7 +139,6 @@ func defaultConfig() *Config {
cfg.Backend = Backend{ cfg.Backend = Backend{
Default: LocalBackendName, // local backend is the default (using wgcrtl) Default: LocalBackendName, // local backend is the default (using wgcrtl)
ReKeyTimeoutInterval: getEnvDuration("WG_PORTAL_BACKEND_REKEY_TIMEOUT_INTERVAL", 180*time.Second),
IgnoredLocalInterfaces: getEnvStrSlice("WG_PORTAL_BACKEND_IGNORED_LOCAL_INTERFACES", nil), IgnoredLocalInterfaces: getEnvStrSlice("WG_PORTAL_BACKEND_IGNORED_LOCAL_INTERFACES", nil),
// Most resolconf implementations use "tun." as a prefix for interface names. // Most resolconf implementations use "tun." as a prefix for interface names.
// But systemd's implementation uses no prefix, for example. // But systemd's implementation uses no prefix, for example.

View File

@@ -78,33 +78,6 @@ type Interface struct {
PeerDefPostUp string // default action that is executed after the device is up PeerDefPostUp string // default action that is executed after the device is up
PeerDefPreDown string // default action that is executed before the device is down PeerDefPreDown string // default action that is executed before the device is down
PeerDefPostDown string // default action that is executed after the device is down PeerDefPostDown string // default action that is executed after the device is down
// Self-provisioning access control
LdapAllowedUsers map[string][]UserIdentifier `gorm:"serializer:json"` // Materialised during LDAP sync, keyed by ProviderName
}
// IsUserAllowed returns true if the interface has no filter, or if the user is in the allowed list.
func (i *Interface) IsUserAllowed(userId UserIdentifier, cfg *config.Config) bool {
isRestricted := false
for _, provider := range cfg.Auth.Ldap {
if _, exists := provider.InterfaceFilter[string(i.Identifier)]; exists {
isRestricted = true
break
}
}
if !isRestricted {
return true // The interface is completely unrestricted by LDAP config
}
for _, allowedUsers := range i.LdapAllowedUsers {
for _, uid := range allowedUsers {
if uid == userId {
return true
}
}
}
return false
} }
// PublicInfo returns a copy of the interface with only the public information. // PublicInfo returns a copy of the interface with only the public information.

View File

@@ -21,8 +21,8 @@ type PeerStatus struct {
LastSessionStart *time.Time `gorm:"column:last_session_start" json:"LastSessionStart"` LastSessionStart *time.Time `gorm:"column:last_session_start" json:"LastSessionStart"`
} }
func (s *PeerStatus) CalcConnected(timeout time.Duration) { func (s *PeerStatus) CalcConnected() {
oldestHandshakeTime := time.Now().Add(-1 * timeout) // if a handshake is older than the rekey-interval + grace-period, the peer is no longer connected oldestHandshakeTime := time.Now().Add(-2 * time.Minute) // if a handshake is older than 2 minutes, the peer is no longer connected
handshakeValid := false handshakeValid := false
if s.LastHandshake != nil { if s.LastHandshake != nil {

View File

@@ -9,16 +9,11 @@ func TestPeerStatus_IsConnected(t *testing.T) {
now := time.Now() now := time.Now()
past := now.Add(-3 * time.Minute) past := now.Add(-3 * time.Minute)
recent := now.Add(-1 * time.Minute) recent := now.Add(-1 * time.Minute)
defaultTimeout := 125 * time.Second // rekey interval of 120s + 5 seconds grace period
past126 := now.Add(-1*defaultTimeout - 1*time.Second)
past125 := now.Add(-1 * defaultTimeout)
past124 := now.Add(-1*defaultTimeout + 1*time.Second)
tests := []struct { tests := []struct {
name string name string
status PeerStatus status PeerStatus
timeout time.Duration want bool
want bool
}{ }{
{ {
name: "Pingable and recent handshake", name: "Pingable and recent handshake",
@@ -26,8 +21,7 @@ func TestPeerStatus_IsConnected(t *testing.T) {
IsPingable: true, IsPingable: true,
LastHandshake: &recent, LastHandshake: &recent,
}, },
timeout: defaultTimeout, want: true,
want: true,
}, },
{ {
name: "Not pingable but recent handshake", name: "Not pingable but recent handshake",
@@ -35,8 +29,7 @@ func TestPeerStatus_IsConnected(t *testing.T) {
IsPingable: false, IsPingable: false,
LastHandshake: &recent, LastHandshake: &recent,
}, },
timeout: defaultTimeout, want: true,
want: true,
}, },
{ {
name: "Pingable but old handshake", name: "Pingable but old handshake",
@@ -44,44 +37,15 @@ func TestPeerStatus_IsConnected(t *testing.T) {
IsPingable: true, IsPingable: true,
LastHandshake: &past, LastHandshake: &past,
}, },
timeout: defaultTimeout, want: true,
want: true,
}, },
{ {
name: "Not pingable and ok handshake (-124s)", name: "Not pingable and old handshake",
status: PeerStatus{
IsPingable: false,
LastHandshake: &past124,
},
timeout: defaultTimeout,
want: true,
},
{
name: "Not pingable and old handshake (-125s)",
status: PeerStatus{
IsPingable: false,
LastHandshake: &past125,
},
timeout: defaultTimeout,
want: false,
},
{
name: "Not pingable and old handshake (-126s)",
status: PeerStatus{
IsPingable: false,
LastHandshake: &past126,
},
timeout: defaultTimeout,
want: false,
},
{
name: "Not pingable and old handshake (very old)",
status: PeerStatus{ status: PeerStatus{
IsPingable: false, IsPingable: false,
LastHandshake: &past, LastHandshake: &past,
}, },
timeout: defaultTimeout, want: false,
want: false,
}, },
{ {
name: "Pingable and no handshake", name: "Pingable and no handshake",
@@ -89,8 +53,7 @@ func TestPeerStatus_IsConnected(t *testing.T) {
IsPingable: true, IsPingable: true,
LastHandshake: nil, LastHandshake: nil,
}, },
timeout: defaultTimeout, want: true,
want: true,
}, },
{ {
name: "Not pingable and no handshake", name: "Not pingable and no handshake",
@@ -98,13 +61,12 @@ func TestPeerStatus_IsConnected(t *testing.T) {
IsPingable: false, IsPingable: false,
LastHandshake: nil, LastHandshake: nil,
}, },
timeout: defaultTimeout, want: false,
want: false,
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
tt.status.CalcConnected(tt.timeout) tt.status.CalcConnected()
if got := tt.status.IsConnected; got != tt.want { if got := tt.status.IsConnected; got != tt.want {
t.Errorf("IsConnected = %v, want %v", got, tt.want) t.Errorf("IsConnected = %v, want %v", got, tt.want)
} }

View File

@@ -68,7 +68,7 @@ type User struct {
WebAuthnCredentialList []UserWebauthnCredential `gorm:"foreignKey:user_identifier"` // the webauthn credentials of the user, used for webauthn authentication WebAuthnCredentialList []UserWebauthnCredential `gorm:"foreignKey:user_identifier"` // the webauthn credentials of the user, used for webauthn authentication
// API token for REST API access // API token for REST API access
ApiToken string `form:"api_token" binding:"omitempty" gorm:"serializer:encstr"` ApiToken string `form:"api_token" binding:"omitempty"`
ApiTokenCreated *time.Time ApiTokenCreated *time.Time
LinkedPeerCount int `gorm:"-"` LinkedPeerCount int `gorm:"-"`
@@ -212,33 +212,6 @@ func (u *User) CopyCalculatedAttributes(src *User, withAuthentications bool) {
} }
} }
// CopyAdminAttributes copies all attributes from the given user except password, passkey and
// api-token if apiAdminOnly is false.
func (u *User) CopyAdminAttributes(src *User, apiAdminOnly bool) {
u.BaseModel = src.BaseModel
u.Identifier = src.Identifier
u.Email = src.Email
u.Source = src.Source
u.ProviderName = src.ProviderName
u.IsAdmin = src.IsAdmin
u.Authentications = src.Authentications
u.PersistLocalChanges = src.PersistLocalChanges
u.Firstname = src.Firstname
u.Lastname = src.Lastname
u.Phone = src.Phone
u.Department = src.Department
u.Notes = src.Notes
u.Disabled = src.Disabled
u.DisabledReason = src.DisabledReason
u.Locked = src.Locked
u.LockedReason = src.LockedReason
u.LinkedPeerCount = src.LinkedPeerCount
if apiAdminOnly {
u.ApiToken = src.ApiToken
u.ApiTokenCreated = src.ApiTokenCreated
}
}
// MergeAuthSources merges the given authentication sources with the existing ones. // MergeAuthSources merges the given authentication sources with the existing ones.
// Already existing sources are not overwritten, nor will be added any duplicates. // Already existing sources are not overwritten, nor will be added any duplicates.
func (u *User) MergeAuthSources(extSources ...UserAuthentication) { func (u *User) MergeAuthSources(extSources ...UserAuthentication) {