mirror of
https://github.com/h44z/wg-portal.git
synced 2025-10-05 16:06:17 +00:00
Compare commits
15 Commits
v2.0.0-bet
...
v2.0.0-bet
Author | SHA1 | Date | |
---|---|---|---|
|
f6c8cd5ea8 | ||
|
a04eaa4bfb | ||
|
7a0a2117f5 | ||
|
2cea2e477a | ||
|
c2658534b0 | ||
|
2030c59362 | ||
|
e31c170f48 | ||
|
49a987cbce | ||
|
3526240faf | ||
|
075fd0171e | ||
|
c73ce0288e | ||
|
31c0daeba8 | ||
|
662e9c0549 | ||
|
6523a87dfb | ||
|
7ccec5db8d |
9
.github/workflows/docker-publish.yml
vendored
9
.github/workflows/docker-publish.yml
vendored
@@ -59,10 +59,15 @@ jobs:
|
||||
tags: |
|
||||
type=ref,event=tag
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
# semver tags, without v prefix
|
||||
type=semver,pattern={{version}}
|
||||
# major and major.minor tags are not available for alpha or beta releases
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern=v{{major}}
|
||||
type=semver,pattern={{major}}
|
||||
# add v{{major}} tag, even for beta releases
|
||||
type=match,pattern=(v\d),group=1,enable=${{ contains(github.ref, 'beta') }}
|
||||
# add {{major}} tag, even for beta releases
|
||||
type=match,pattern=v(\d),group=1,enable=${{ contains(github.ref, 'beta') }}
|
||||
# set latest tag for default branch
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
|
||||
|
2
.github/workflows/pages.yml
vendored
2
.github/workflows/pages.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
python-version: 3.x
|
||||
|
||||
- name: Install dependencies
|
||||
run: pip install mike mkdocs-material[imaging] mkdocs-minify-plugin
|
||||
run: pip install mike mkdocs-material[imaging] mkdocs-minify-plugin mkdocs-swagger-ui-tag
|
||||
|
||||
- name: Publish documentation
|
||||
run: mike deploy --push --update-aliases ${{ github.ref_name }} latest
|
||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@@ -32,11 +32,11 @@ ssh.key
|
||||
.testCoverage.txt
|
||||
wg_portal.db
|
||||
sqlite.db
|
||||
swagger.json
|
||||
swagger.yaml
|
||||
/config.yml
|
||||
/config/
|
||||
venv/
|
||||
.cache/
|
||||
# ignore local frontend dist directory
|
||||
internal/app/api/core/frontend-dist
|
||||
# mkdocs output directory
|
||||
site/
|
||||
|
6
Makefile
6
Makefile
@@ -133,3 +133,9 @@ build-docker:
|
||||
.PHONY: helm-docs
|
||||
helm-docs:
|
||||
docker run --rm --volume "${PWD}/deploy:/helm-docs" -u "$$(id -u)" jnorwood/helm-docs -s file
|
||||
|
||||
#< run-mkdocs: Run a local instance of MkDocs
|
||||
.PHONY: run-mkdocs
|
||||
run-mkdocs:
|
||||
python -m venv venv; source venv/bin/activate; pip install mike cairosvg mkdocs-material mkdocs-minify-plugin mkdocs-swagger-ui-tag
|
||||
venv/bin/mkdocs serve
|
||||
|
199
README.md
199
README.md
@@ -55,98 +55,108 @@ By default, WireGuard Portal uses a SQLite database. The database is stored in *
|
||||
### Configuration Options
|
||||
The following configuration options are available:
|
||||
|
||||
| configuration key | parent key | default_value | description |
|
||||
|----------------------------------|------------|--------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| admin_user | core | admin@wgportal.local | The administrator user. This user will be created as default admin if it does not yet exist. |
|
||||
| admin_password | core | wgportal | The administrator password. If unchanged, a random password will be set on first startup. |
|
||||
| editable_keys | core | true | Allow to edit key-pairs in the UI. |
|
||||
| create_default_peer | core | false | If an LDAP user logs in for the first time and has no peers associated, a new WireGuard peer will be created for all server interfaces. |
|
||||
| create_default_peer_on_creation | core | false | If an LDAP user is created (e.g. through LDAP sync), a new WireGuard peer will be created for all server interfaces. |
|
||||
| re_enable_peer_after_user_enable | core | true | Re-enable all peers that were previously disabled due to a user disable action. |
|
||||
| delete_peer_after_user_deleted | core | false | Delete all linked peers if a user gets disabled. Otherwise the peers only get disabled. |
|
||||
| self_provisioning_allowed | core | false | Allow registered users to automatically create peers via their profile page. |
|
||||
| import_existing | core | true | Import existing WireGuard interfaces and peers into WireGuard Portal. |
|
||||
| restore_state | core | true | Restore the WireGuard interface state after WireGuard Portal has started. |
|
||||
| log_level | advanced | info | The loglevel, can be one of: trace, debug, info, warn, error. |
|
||||
| log_pretty | advanced | false | Uses pretty, colorized log messages. |
|
||||
| log_json | advanced | false | Logs in JSON format. |
|
||||
| start_listen_port | advanced | 51820 | The first port number that will be used as listening port for new interfaces. |
|
||||
| start_cidr_v4 | advanced | 10.11.12.0/24 | The first IPv4 subnet that will be used for new interfaces. |
|
||||
| start_cidr_v6 | advanced | fdfd:d3ad:c0de:1234::0/64 | The first IPv6 subnet that will be used for new interfaces. |
|
||||
| use_ip_v6 | advanced | true | Enable IPv6 support. |
|
||||
| config_storage_path | advanced | | If a wg-quick style configuration should be stored to the filesystem, specify a storage directory. |
|
||||
| expiry_check_interval | advanced | 15m | The interval after which existing peers will be checked if they expired. |
|
||||
| rule_prio_offset | advanced | 20000 | The default offset for ip route rule priorities. |
|
||||
| route_table_offset | advanced | 20000 | The default offset for ip route table id's. |
|
||||
| api_admin_only | advanced | true | This flag specifies if the public REST API is available to administrators only. The API Swagger documentation is available under /api/v1/doc.html |
|
||||
| use_ping_checks | statistics | true | If enabled, peers will be pinged periodically to check if they are still connected. |
|
||||
| ping_check_workers | statistics | 10 | Number of parallel ping checks that will be executed. |
|
||||
| ping_unprivileged | statistics | false | If set to false, the ping checks will run without root permissions (BETA). |
|
||||
| ping_check_interval | statistics | 1m | The interval time between two ping check runs. |
|
||||
| data_collection_interval | statistics | 1m | The interval between the data collection cycles. |
|
||||
| collect_interface_data | statistics | true | A flag to enable interface data collection like bytes sent and received. |
|
||||
| collect_peer_data | statistics | true | A flag to enable peer data collection like bytes sent and received, last handshake and remote endpoint address. |
|
||||
| collect_audit_data | statistics | true | If enabled, some events, like portal logins, will be logged to the database. |
|
||||
| listening_address | statistics | :8787 | The listening address of the Prometheus metric server. |
|
||||
| host | mail | 127.0.0.1 | The mail-server address. |
|
||||
| port | mail | 25 | The mail-server SMTP port. |
|
||||
| encryption | mail | none | SMTP encryption type, allowed values: none, tls, starttls. |
|
||||
| cert_validation | mail | false | Validate the mail server certificate (if encryption tls is used). |
|
||||
| username | mail | | The SMTP user name. |
|
||||
| password | mail | | The SMTP password. |
|
||||
| auth_type | mail | plain | SMTP authentication type, allowed values: plain, login, crammd5. |
|
||||
| from | mail | Wireguard Portal <noreply@wireguard.local> | The address that is used to send mails. |
|
||||
| link_only | mail | false | Only send links to WireGuard Portal instead of the full configuration. |
|
||||
| oidc | auth | Empty Array - no providers configured | A list of OpenID Connect providers. See auth/oidc properties to setup a new provider. |
|
||||
| oauth | auth | Empty Array - no providers configured | A list of plain OAuth providers. See auth/oauth properties to setup a new provider. |
|
||||
| ldap | auth | Empty Array - no providers configured | A list of LDAP providers. See auth/ldap properties to setup a new provider. |
|
||||
| provider_name | auth/oidc | | A unique provider name. This name must be unique throughout all authentication providers (even other types). |
|
||||
| display_name | auth/oidc | | The display name is shown at the login page (the login button). |
|
||||
| base_url | auth/oidc | | The base_url is the URL identifier for the service. For example: "https://accounts.google.com". |
|
||||
| client_id | auth/oidc | | The OAuth client id. |
|
||||
| client_secret | auth/oidc | | The OAuth client secret. |
|
||||
| extra_scopes | auth/oidc | | Extra scopes that should be used in the OpenID Connect authentication flow. |
|
||||
| field_map | auth/oidc | | Mapping of user fields. Internal fields: user_identifier, email, firstname, lastname, phone, department and is_admin. |
|
||||
| registration_enabled | auth/oidc | | If registration is enabled, new user accounts will created in WireGuard Portal. |
|
||||
| provider_name | auth/oauth | | A unique provider name. This name must be unique throughout all authentication providers (even other types). |
|
||||
| display_name | auth/oauth | | The display name is shown at the login page (the login button). |
|
||||
| client_id | auth/oauth | | The OAuth client id. |
|
||||
| client_secret | auth/oauth | | The OAuth client secret. |
|
||||
| auth_url | auth/oauth | | The URL for the authentication endpoint. |
|
||||
| token_url | auth/oauth | | The URL for the token endpoint. |
|
||||
| user_info_url | auth/oauth | | The URL for the user information endpoint. |
|
||||
| scopes | auth/oauth | | OAuth scopes. |
|
||||
| field_map | auth/oauth | | Mapping of user fields. Internal fields: user_identifier, email, firstname, lastname, phone, department and is_admin. |
|
||||
| registration_enabled | auth/oauth | | If registration is enabled, new user accounts will created in WireGuard Portal. |
|
||||
| url | auth/ldap | | The LDAP server url. For example: ldap://srv-ad01.company.local:389 |
|
||||
| start_tls | auth/ldap | | Use STARTTLS to encrypt LDAP requests. |
|
||||
| cert_validation | auth/ldap | | Validate the LDAP server certificate. |
|
||||
| tls_certificate_path | auth/ldap | | A path to the TLS certificate. |
|
||||
| tls_key_path | auth/ldap | | A path to the TLS key. |
|
||||
| base_dn | auth/ldap | | The base DN for searching users. For example: DC=COMPANY,DC=LOCAL |
|
||||
| bind_user | auth/ldap | | The bind user. For example: company\\ldap_wireguard |
|
||||
| bind_pass | auth/ldap | | The bind password. |
|
||||
| field_map | auth/ldap | | Mapping of user fields. Internal fields: user_identifier, email, firstname, lastname, phone, department and memberof. |
|
||||
| login_filter | auth/ldap | | LDAP filters for users that should be allowed to log in. {{login_identifier}} will be replaced with the login username. |
|
||||
| admin_group | auth/ldap | | Users in this group are marked as administrators. |
|
||||
| disable_missing | auth/ldap | | If synchronization is enabled, missing LDAP users will be disabled in WireGuard Portal. |
|
||||
| sync_filter | auth/ldap | | LDAP filters for users that should be synchronized to WireGuard Portal. |
|
||||
| sync_interval | auth/ldap | | The time interval after which users will be synchronized from LDAP. Empty value or `0` disables synchronization. |
|
||||
| registration_enabled | auth/ldap | | If registration is enabled, new user accounts will created in WireGuard Portal. |
|
||||
| debug | database | false | Debug database statements (log each statement). |
|
||||
| slow_query_threshold | database | | A threshold for slow database queries. If the threshold is exceeded, a warning message will be logged. |
|
||||
| type | database | sqlite | The database type. Allowed values: sqlite, mssql, mysql or postgres. |
|
||||
| dsn | database | data/sqlite.db | The database DSN. For example: user:pass@tcp(1.2.3.4:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local |
|
||||
| request_logging | web | false | Log all HTTP requests. |
|
||||
| external_url | web | http://localhost:8888 | The URL where a client can access WireGuard Portal. |
|
||||
| listening_address | web | :8888 | The listening port of the web server. |
|
||||
| session_identifier | web | wgPortalSession | The session identifier for the web frontend. |
|
||||
| session_secret | web | very_secret | The session secret for the web frontend. |
|
||||
| csrf_secret | web | extremely_secret | The CSRF secret. |
|
||||
| site_title | web | WireGuard Portal | The title that is shown in the web frontend. |
|
||||
| site_company_name | web | WireGuard Portal | The company name that is shown at the bottom of the web frontend. |
|
||||
| cert_file | web | | (Optional) Path to the TLS certificate file |
|
||||
| key_file | web | | (Optional) Path to the TLS certificate key file |
|
||||
| configuration key | parent key | default_value | description |
|
||||
|----------------------------------|------------|--------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| admin_user | core | admin@wgportal.local | The administrator user. This user will be created as default admin if it does not yet exist. |
|
||||
| admin_password | core | wgportal | The administrator password. If unchanged, a random password will be set on first startup. |
|
||||
| editable_keys | core | true | Allow to edit key-pairs in the UI. |
|
||||
| create_default_peer | core | false | If an LDAP user logs in for the first time and has no peers associated, a new WireGuard peer will be created for all server interfaces. |
|
||||
| create_default_peer_on_creation | core | false | If an LDAP user is created (e.g. through LDAP sync), a new WireGuard peer will be created for all server interfaces. |
|
||||
| re_enable_peer_after_user_enable | core | true | Re-enable all peers that were previously disabled due to a user disable action. |
|
||||
| delete_peer_after_user_deleted | core | false | Delete all linked peers if a user gets disabled. Otherwise the peers only get disabled. |
|
||||
| self_provisioning_allowed | core | false | Allow registered users to automatically create peers via their profile page. |
|
||||
| import_existing | core | true | Import existing WireGuard interfaces and peers into WireGuard Portal. |
|
||||
| restore_state | core | true | Restore the WireGuard interface state after WireGuard Portal has started. |
|
||||
| log_level | advanced | info | The loglevel, can be one of: trace, debug, info, warn, error. |
|
||||
| log_pretty | advanced | false | Uses pretty, colorized log messages. |
|
||||
| log_json | advanced | false | Logs in JSON format. |
|
||||
| start_listen_port | advanced | 51820 | The first port number that will be used as listening port for new interfaces. |
|
||||
| start_cidr_v4 | advanced | 10.11.12.0/24 | The first IPv4 subnet that will be used for new interfaces. |
|
||||
| start_cidr_v6 | advanced | fdfd:d3ad:c0de:1234::0/64 | The first IPv6 subnet that will be used for new interfaces. |
|
||||
| use_ip_v6 | advanced | true | Enable IPv6 support. |
|
||||
| config_storage_path | advanced | | If a wg-quick style configuration should be stored to the filesystem, specify a storage directory. |
|
||||
| expiry_check_interval | advanced | 15m | The interval after which existing peers will be checked if they expired. |
|
||||
| rule_prio_offset | advanced | 20000 | The default offset for ip route rule priorities. |
|
||||
| route_table_offset | advanced | 20000 | The default offset for ip route table id's. |
|
||||
| api_admin_only | advanced | true | This flag specifies if the public REST API is available to administrators only. The API Swagger documentation is available under /api/v1/doc.html |
|
||||
| use_ping_checks | statistics | true | If enabled, peers will be pinged periodically to check if they are still connected. |
|
||||
| ping_check_workers | statistics | 10 | Number of parallel ping checks that will be executed. |
|
||||
| ping_unprivileged | statistics | false | If set to false, the ping checks will run without root permissions (BETA). |
|
||||
| ping_check_interval | statistics | 1m | The interval time between two ping check runs. |
|
||||
| data_collection_interval | statistics | 1m | The interval between the data collection cycles. |
|
||||
| collect_interface_data | statistics | true | A flag to enable interface data collection like bytes sent and received. |
|
||||
| collect_peer_data | statistics | true | A flag to enable peer data collection like bytes sent and received, last handshake and remote endpoint address. |
|
||||
| collect_audit_data | statistics | true | If enabled, some events, like portal logins, will be logged to the database. |
|
||||
| listening_address | statistics | :8787 | The listening address of the Prometheus metric server. |
|
||||
| host | mail | 127.0.0.1 | The mail-server address. |
|
||||
| port | mail | 25 | The mail-server SMTP port. |
|
||||
| encryption | mail | none | SMTP encryption type, allowed values: none, tls, starttls. |
|
||||
| cert_validation | mail | false | Validate the mail server certificate (if encryption tls is used). |
|
||||
| username | mail | | The SMTP user name. |
|
||||
| password | mail | | The SMTP password. |
|
||||
| auth_type | mail | plain | SMTP authentication type, allowed values: plain, login, crammd5. |
|
||||
| from | mail | Wireguard Portal <noreply@wireguard.local> | The address that is used to send mails. |
|
||||
| link_only | mail | false | Only send links to WireGuard Portal instead of the full configuration. |
|
||||
| oidc | auth | Empty Array - no providers configured | A list of OpenID Connect providers. See auth/oidc properties to setup a new provider. |
|
||||
| oauth | auth | Empty Array - no providers configured | A list of plain OAuth providers. See auth/oauth properties to setup a new provider. |
|
||||
| ldap | auth | Empty Array - no providers configured | A list of LDAP providers. See auth/ldap properties to setup a new provider. |
|
||||
| provider_name | auth/oidc | | A unique provider name. This name must be unique throughout all authentication providers (even other types). |
|
||||
| display_name | auth/oidc | | The display name is shown at the login page (the login button). |
|
||||
| base_url | auth/oidc | | The base_url is the URL identifier for the service. For example: "https://accounts.google.com". |
|
||||
| client_id | auth/oidc | | The OAuth client id. |
|
||||
| client_secret | auth/oidc | | The OAuth client secret. |
|
||||
| extra_scopes | auth/oidc | | Extra scopes that should be used in the OpenID Connect authentication flow. |
|
||||
| field_map | auth/oidc | | Mapping of user fields. Internal fields: user_identifier, email, firstname, lastname, phone, department, is_admin and user_groups. |
|
||||
| admin_mapping | auth/oidc | | Contains regex values admin_value_regex and admin_group_regex to map the is_admin field and user_groups respectively. |
|
||||
| registration_enabled | auth/oidc | | If registration is enabled, new user accounts will created in WireGuard Portal. |
|
||||
| log_user_info | auth/oidc | | If true, the user info retrieved from the OIDC provider will be logged in trace level. |
|
||||
| provider_name | auth/oauth | | A unique provider name. This name must be unique throughout all authentication providers (even other types). |
|
||||
| display_name | auth/oauth | | The display name is shown at the login page (the login button). |
|
||||
| client_id | auth/oauth | | The OAuth client id. |
|
||||
| client_secret | auth/oauth | | The OAuth client secret. |
|
||||
| auth_url | auth/oauth | | The URL for the authentication endpoint. |
|
||||
| token_url | auth/oauth | | The URL for the token endpoint. |
|
||||
| user_info_url | auth/oauth | | The URL for the user information endpoint. |
|
||||
| scopes | auth/oauth | | OAuth scopes. |
|
||||
| field_map | auth/oauth | | Mapping of user fields. Internal fields: user_identifier, email, firstname, lastname, phone, department and is_admin and user_groups. |
|
||||
| admin_mapping | auth/oauth | | Contains regex values admin_value_regex and admin_group_regex to map the is_admin field and user_groups respectively. |
|
||||
| registration_enabled | auth/oauth | | If registration is enabled, new user accounts will created in WireGuard Portal. |
|
||||
| log_user_info | auth/oauth | | If true, the user info retrieved from the OAuth provider will be logged in trace level. |
|
||||
| url | auth/ldap | | The LDAP server url. For example: ldap://srv-ad01.company.local:389 |
|
||||
| start_tls | auth/ldap | | Use STARTTLS to encrypt LDAP requests. |
|
||||
| cert_validation | auth/ldap | | Validate the LDAP server certificate. |
|
||||
| tls_certificate_path | auth/ldap | | A path to the TLS certificate. |
|
||||
| tls_key_path | auth/ldap | | A path to the TLS key. |
|
||||
| base_dn | auth/ldap | | The base DN for searching users. For example: DC=COMPANY,DC=LOCAL |
|
||||
| bind_user | auth/ldap | | The bind user. For example: company\\ldap_wireguard |
|
||||
| bind_pass | auth/ldap | | The bind password. |
|
||||
| field_map | auth/ldap | | Mapping of user fields. Internal fields: user_identifier, email, firstname, lastname, phone, department and memberof. |
|
||||
| login_filter | auth/ldap | | LDAP filters for users that should be allowed to log in. {{login_identifier}} will be replaced with the login username. |
|
||||
| admin_group | auth/ldap | | Users in this group are marked as administrators. |
|
||||
| disable_missing | auth/ldap | | If synchronization is enabled, missing LDAP users will be disabled in WireGuard Portal. |
|
||||
| auto_re_enable | auth/ldap | | If auto re-enable is true, users that where disabled because they were missing will be re-enabled once they are found again. |
|
||||
| sync_filter | auth/ldap | | LDAP filters for users that should be synchronized to WireGuard Portal. |
|
||||
| sync_interval | auth/ldap | | The time interval after which users will be synchronized from LDAP. Empty value or `0` disables synchronization. |
|
||||
| registration_enabled | auth/ldap | | If registration is enabled, new user accounts will created in WireGuard Portal. |
|
||||
| log_user_info | auth/ldap | | If true, the user info retrieved from the LDAP provider will be logged in trace level. |
|
||||
| debug | database | false | Debug database statements (log each statement). |
|
||||
| slow_query_threshold | database | | A threshold for slow database queries. If the threshold is exceeded, a warning message will be logged. |
|
||||
| type | database | sqlite | The database type. Allowed values: sqlite, mssql, mysql or postgres. |
|
||||
| dsn | database | data/sqlite.db | The database DSN. For example: user:pass@tcp(1.2.3.4:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local |
|
||||
| request_logging | web | false | Log all HTTP requests. |
|
||||
| external_url | web | http://localhost:8888 | The URL where a client can access WireGuard Portal. |
|
||||
| listening_address | web | :8888 | The listening port of the web server. |
|
||||
| session_identifier | web | wgPortalSession | The session identifier for the web frontend. |
|
||||
| session_secret | web | very_secret | The session secret for the web frontend. |
|
||||
| csrf_secret | web | extremely_secret | The CSRF secret. |
|
||||
| site_title | web | WireGuard Portal | The title that is shown in the web frontend. |
|
||||
| site_company_name | web | WireGuard Portal | The company name that is shown at the bottom of the web frontend. |
|
||||
| cert_file | web | | (Optional) Path to the TLS certificate file |
|
||||
| key_file | web | | (Optional) Path to the TLS certificate key file |
|
||||
|
||||
A sample config file can be found in the repository: [config.yml.sample](config.yml.sample).
|
||||
More detailed information about the configuration can be found in the [documentation](https://wgportal.org/master/documentation/overview/) on [wgportal.org](https://wgportal.org/master/documentation/overview/).
|
||||
|
||||
|
||||
## Upgrading from V1
|
||||
|
||||
@@ -173,16 +183,13 @@ Ensure that the new database does not contain any data!
|
||||
|
||||
|
||||
## V2 TODOs
|
||||
* Public REST API
|
||||
* Translations
|
||||
* Documentation
|
||||
* Audit UI
|
||||
|
||||
|
||||
## Building
|
||||
|
||||
To build a standalone application, use the Makefile provided in the repository.
|
||||
Go version 1.22 or higher has to be installed to build WireGuard Portal.
|
||||
Go version 1.23 or higher has to be installed to build WireGuard Portal.
|
||||
If you want to re-compile the frontend, NodeJS 18 and NPM >= 9 is required.
|
||||
|
||||
```shell
|
||||
|
@@ -7,11 +7,15 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/swaggo/swag"
|
||||
"github.com/swaggo/swag/gen"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
var apiRootPath = "/internal/app/api"
|
||||
var apiDocPath = "core/assets/doc"
|
||||
var apiMkDocPath = "/docs/documentation/rest-api"
|
||||
|
||||
// this replaces the call to: swag init --propertyStrategy pascalcase --parseDependency --parseInternal --generalInfo base.go
|
||||
func main() {
|
||||
wd, err := os.Getwd() // should be the project root
|
||||
@@ -19,10 +23,9 @@ func main() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
apiBasePath := filepath.Join(wd, "/internal/app/api")
|
||||
apiBasePath := filepath.Join(wd, apiRootPath)
|
||||
apis := []string{"v0", "v1"}
|
||||
|
||||
hasError := false
|
||||
for _, apiVersion := range apis {
|
||||
apiPath := filepath.Join(apiBasePath, apiVersion, "handlers")
|
||||
|
||||
@@ -33,16 +36,20 @@ func main() {
|
||||
|
||||
err := generateApi(apiBasePath, apiPath, apiVersion)
|
||||
if err != nil {
|
||||
hasError = true
|
||||
logrus.Errorf("failed to generate API docs for %s: %v", apiVersion, err)
|
||||
log.Fatalf("failed to generate API docs for %s: %v", apiVersion, err)
|
||||
}
|
||||
|
||||
// copy the latest version of the API docs for mkdocs
|
||||
if apiVersion == apis[len(apis)-1] {
|
||||
if err = copyDocForMkdocs(wd, apiBasePath, apiVersion); err != nil {
|
||||
log.Printf("failed to copy API docs for mkdocs: %v", err)
|
||||
} else {
|
||||
log.Println("Copied API docs " + apiVersion + " for mkdocs")
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("Generated swagger docs for API", apiVersion)
|
||||
}
|
||||
|
||||
if hasError {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func generateApi(basePath, apiPath, version string) error {
|
||||
@@ -51,7 +58,7 @@ func generateApi(basePath, apiPath, version string) error {
|
||||
Excludes: "",
|
||||
MainAPIFile: "base.go",
|
||||
PropNamingStrategy: swag.PascalCase,
|
||||
OutputDir: filepath.Join(basePath, "core/assets/doc"),
|
||||
OutputDir: filepath.Join(basePath, apiDocPath),
|
||||
OutputTypes: []string{"json", "yaml"},
|
||||
ParseVendor: false,
|
||||
ParseDependency: 3,
|
||||
@@ -68,3 +75,43 @@ func generateApi(basePath, apiPath, version string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyDocForMkdocs(workingDir, basePath, version string) error {
|
||||
srcPath := filepath.Join(basePath, apiDocPath, fmt.Sprintf("%s_swagger.yaml", version))
|
||||
dstPath := filepath.Join(workingDir, apiMkDocPath, "swagger.yaml")
|
||||
|
||||
// copy the file
|
||||
input, err := os.ReadFile(srcPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while reading swagger doc: %w", err)
|
||||
}
|
||||
|
||||
output, err := removeAuthorizeButton(input)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while removing authorize button: %w", err)
|
||||
}
|
||||
|
||||
err = os.WriteFile(dstPath, output, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while writing swagger doc: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func removeAuthorizeButton(input []byte) ([]byte, error) {
|
||||
var swagger map[string]interface{}
|
||||
err := yaml.Unmarshal(input, &swagger)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error while unmarshalling swagger file: %w", err)
|
||||
}
|
||||
|
||||
delete(swagger, "securityDefinitions")
|
||||
|
||||
output, err := yaml.Marshal(&swagger)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error while marshalling swagger file: %w", err)
|
||||
}
|
||||
|
||||
return output, nil
|
||||
}
|
||||
|
@@ -1,3 +1,5 @@
|
||||
# More information about the configuration can be found in the documentation: https://wgportal.org/master/documentation/overview/
|
||||
|
||||
advanced:
|
||||
log_level: trace
|
||||
|
||||
@@ -22,7 +24,7 @@ auth:
|
||||
base_dn: DC=YOURCOMPANY,DC=LOCAL
|
||||
login_filter: (&(objectClass=organizationalPerson)(mail={{login_identifier}})(!userAccountControl:1.2.840.113556.1.4.803:=2))
|
||||
admin_group: CN=WireGuardAdmins,OU=it,DC=YOURCOMPANY,DC=LOCAL
|
||||
synchronize: false
|
||||
sync_interval: 0 # sync disabled
|
||||
sync_filter: (&(objectClass=organizationalPerson)(!userAccountControl:1.2.840.113556.1.4.803:=2)(mail=*))
|
||||
registration_enabled: true
|
||||
oidc:
|
||||
@@ -63,5 +65,28 @@ auth:
|
||||
email: email
|
||||
firstname: name
|
||||
user_identifier: sub
|
||||
is_admin: roles
|
||||
registration_enabled: true
|
||||
is_admin: this-attribute-must-be-true
|
||||
registration_enabled: true
|
||||
- id: google_plain_oauth_with_groups
|
||||
provider_name: google4
|
||||
display_name: Login with</br>Google4
|
||||
client_id: another-client-id-1234.apps.googleusercontent.com
|
||||
client_secret: A_CLIENT_SECRET
|
||||
auth_url: https://accounts.google.com/o/oauth2/v2/auth
|
||||
token_url: https://oauth2.googleapis.com/token
|
||||
user_info_url: https://openidconnect.googleapis.com/v1/userinfo
|
||||
scopes:
|
||||
- openid
|
||||
- email
|
||||
- profile
|
||||
- i-want-some-groups
|
||||
field_map:
|
||||
email: email
|
||||
firstname: name
|
||||
user_identifier: sub
|
||||
user_groups: groups
|
||||
admin_mapping:
|
||||
admin_value_regex: ^true$
|
||||
admin_group_regex: ^admin-group-name$
|
||||
registration_enabled: true
|
||||
log_user_info: true
|
176
docs/documentation/configuration/examples.md
Normal file
176
docs/documentation/configuration/examples.md
Normal file
@@ -0,0 +1,176 @@
|
||||
Below are some sample YAML configurations demonstrating how to override some default values.
|
||||
|
||||
## Basic Configuration
|
||||
```yaml
|
||||
core:
|
||||
admin_user: test@example.com
|
||||
admin_password: password
|
||||
import_existing: false
|
||||
create_default_peer: true
|
||||
self_provisioning_allowed: true
|
||||
|
||||
web:
|
||||
site_title: My WireGuard Server
|
||||
site_company_name: My Company
|
||||
listening_address: :8080
|
||||
external_url: https://my.externa-domain.com
|
||||
csrf_secret: super-s3cr3t-csrf
|
||||
session_secret: super-s3cr3t-session
|
||||
request_logging: true
|
||||
|
||||
advanced:
|
||||
log_level: trace
|
||||
log_pretty: true
|
||||
log_json: false
|
||||
config_storage_path: /etc/wireguard
|
||||
expiry_check_interval: 5m
|
||||
|
||||
database:
|
||||
debug: true
|
||||
type: sqlite
|
||||
dsn: data/sqlite.db
|
||||
```
|
||||
|
||||
## LDAP Authentication and Synchronization Configuration
|
||||
```yaml
|
||||
# ... (basic configuration)
|
||||
|
||||
auth:
|
||||
ldap:
|
||||
|
||||
# a sample LDAP provider with user sync enabled
|
||||
- id: ldap
|
||||
provider_name: Active Directory
|
||||
display_name: Login with</br>AD
|
||||
url: ldap://srv-ad1.company.local:389
|
||||
bind_user: ldap_wireguard@company.local
|
||||
bind_pass: super-s3cr3t-ldap
|
||||
base_dn: DC=COMPANY,DC=LOCAL
|
||||
login_filter: (&(objectClass=organizationalPerson)(mail={{login_identifier}})(!userAccountControl:1.2.840.113556.1.4.803:=2))
|
||||
sync_interval: 15m
|
||||
sync_filter: (&(objectClass=organizationalPerson)(!userAccountControl:1.2.840.113556.1.4.803:=2)(mail=*))
|
||||
disable_missing: true
|
||||
field_map:
|
||||
user_identifier: sAMAccountName
|
||||
email: mail
|
||||
firstname: givenName
|
||||
lastname: sn
|
||||
phone: telephoneNumber
|
||||
department: department
|
||||
memberof: memberOf
|
||||
admin_group: CN=WireGuardAdmins,OU=Some-OU,DC=COMPANY,DC=LOCAL
|
||||
registration_enabled: true
|
||||
log_user_info: true
|
||||
```
|
||||
|
||||
## OpenID Connect (OIDC) Authentication Configuration
|
||||
```yaml
|
||||
# ... (basic configuration)
|
||||
|
||||
auth:
|
||||
oidc:
|
||||
|
||||
# a sample provider where users with the attribute `wg_admin` set to `true` are considered as admins
|
||||
- id: oidc-with-admin-attribute
|
||||
provider_name: google
|
||||
display_name: Login with</br>Google
|
||||
base_url: https://accounts.google.com
|
||||
client_id: the-client-id-1234.apps.googleusercontent.com
|
||||
client_secret: A_CLIENT_SECRET
|
||||
extra_scopes:
|
||||
- https://www.googleapis.com/auth/userinfo.email
|
||||
- https://www.googleapis.com/auth/userinfo.profile
|
||||
field_map:
|
||||
user_identifier: sub
|
||||
email: email
|
||||
firstname: given_name
|
||||
lastname: family_name
|
||||
phone: phone_number
|
||||
department: department
|
||||
is_admin: wg_admin
|
||||
admin_mapping:
|
||||
- admin_value_regex: ^true$
|
||||
registration_enabled: true
|
||||
log_user_info: true
|
||||
|
||||
# a sample provider where users in the group `the-admin-group` are considered as admins
|
||||
- id: oidc-with-admin-group
|
||||
provider_name: google2
|
||||
display_name: Login with</br>Google2
|
||||
base_url: https://accounts.google.com
|
||||
client_id: another-client-id-1234.apps.googleusercontent.com
|
||||
client_secret: A_CLIENT_SECRET
|
||||
extra_scopes:
|
||||
- https://www.googleapis.com/auth/userinfo.email
|
||||
- https://www.googleapis.com/auth/userinfo.profile
|
||||
field_map:
|
||||
user_identifier: sub
|
||||
email: email
|
||||
firstname: given_name
|
||||
lastname: family_name
|
||||
phone: phone_number
|
||||
department: department
|
||||
user_groups: groups
|
||||
admin_mapping:
|
||||
- admin_group_regex: ^the-admin-group$
|
||||
registration_enabled: true
|
||||
log_user_info: true
|
||||
```
|
||||
|
||||
## Plain OAuth2 Authentication Configuration
|
||||
```yaml
|
||||
# ... (basic configuration)
|
||||
|
||||
auth:
|
||||
oauth:
|
||||
|
||||
# a sample provider where users with the attribute `this-attribute-must-be-true` set to `true` or `True`
|
||||
# are considered as admins
|
||||
- id: google_plain_oauth-with-admin-attribute
|
||||
provider_name: google3
|
||||
display_name: Login with</br>Google3
|
||||
client_id: another-client-id-1234.apps.googleusercontent.com
|
||||
client_secret: A_CLIENT_SECRET
|
||||
auth_url: https://accounts.google.com/o/oauth2/v2/auth
|
||||
token_url: https://oauth2.googleapis.com/token
|
||||
user_info_url: https://openidconnect.googleapis.com/v1/userinfo
|
||||
scopes:
|
||||
- openid
|
||||
- email
|
||||
- profile
|
||||
field_map:
|
||||
user_identifier: sub
|
||||
email: email
|
||||
firstname: name
|
||||
is_admin: this-attribute-must-be-true
|
||||
admin_mapping:
|
||||
- admin_value_regex: ^(True|true)$
|
||||
registration_enabled: true
|
||||
|
||||
# a sample provider where either users with the attribute `this-attribute-must-be-true` set to `true` or
|
||||
# users in the group `admin-group-name` are considered as admins
|
||||
- id: google_plain_oauth_with_groups
|
||||
provider_name: google4
|
||||
display_name: Login with</br>Google4
|
||||
client_id: another-client-id-1234.apps.googleusercontent.com
|
||||
client_secret: A_CLIENT_SECRET
|
||||
auth_url: https://accounts.google.com/o/oauth2/v2/auth
|
||||
token_url: https://oauth2.googleapis.com/token
|
||||
user_info_url: https://openidconnect.googleapis.com/v1/userinfo
|
||||
scopes:
|
||||
- openid
|
||||
- email
|
||||
- profile
|
||||
- i-want-some-groups
|
||||
field_map:
|
||||
email: email
|
||||
firstname: name
|
||||
user_identifier: sub
|
||||
is_admin: this-attribute-must-be-true
|
||||
user_groups: groups
|
||||
admin_mapping:
|
||||
admin_value_regex: ^true$
|
||||
admin_group_regex: ^admin-group-name$
|
||||
registration_enabled: true
|
||||
log_user_info: true
|
||||
```
|
457
docs/documentation/configuration/overview.md
Normal file
457
docs/documentation/configuration/overview.md
Normal file
@@ -0,0 +1,457 @@
|
||||
# WireGuard Portal Configuration
|
||||
|
||||
This page provides an overview of **all available configuration options** for WireGuard Portal.
|
||||
You can supply these configurations in a **YAML** file (e.g. `config.yaml`) when starting the Portal.
|
||||
Complete configuration examples are available in the [Configuration Examples](./examples.md) page.
|
||||
|
||||
Below you will find sections like `core`, `advanced`, `statistics`, `mail`, `auth`, `database`, and `web`.
|
||||
Each section describes the individual configuration keys, their default values, and a brief explanation of their purpose.
|
||||
|
||||
---
|
||||
|
||||
## Core
|
||||
|
||||
These are the primary configuration options that control fundamental WireGuard Portal behavior.
|
||||
More advanced options are found in the subsequent `Advanced` section.
|
||||
|
||||
### `admin_user`
|
||||
- **Default:** `admin@wgportal.local`
|
||||
- **Description:** The administrator user. This user will be created as a default admin if it does not yet exist.
|
||||
|
||||
### `admin_password`
|
||||
- **Default:** `wgportal`
|
||||
- **Description:** The administrator password. The default password of `wgportal` should be changed immediately.
|
||||
|
||||
### `editable_keys`
|
||||
- **Default:** `true`
|
||||
- **Description:** Allow editing of WireGuard key-pairs directly in the UI.
|
||||
|
||||
### `create_default_peer`
|
||||
- **Default:** `false`
|
||||
- **Description:** If a user logs in for the first time with no existing peers, automatically create a new WireGuard peer for **all** server interfaces.
|
||||
|
||||
### `create_default_peer_on_creation`
|
||||
- **Default:** `false`
|
||||
- **Description:** If an LDAP user is created (e.g., through LDAP sync) and has no peers, automatically create a new WireGuard peer for **all** server interfaces.
|
||||
|
||||
### `re_enable_peer_after_user_enable`
|
||||
- **Default:** `true`
|
||||
- **Description:** Re-enable all peers that were previously disabled if the associated user is re-enabled.
|
||||
|
||||
### `delete_peer_after_user_deleted`
|
||||
- **Default:** `false`
|
||||
- **Description:** If a user is deleted, remove all linked peers. Otherwise, peers remain but are disabled.
|
||||
|
||||
### `self_provisioning_allowed`
|
||||
- **Default:** `false`
|
||||
- **Description:** Allow registered (non-admin) users to self-provision peers from their profile page.
|
||||
|
||||
### `import_existing`
|
||||
- **Default:** `true`
|
||||
- **Description:** On startup, import existing WireGuard interfaces and peers into WireGuard Portal.
|
||||
|
||||
### `restore_state`
|
||||
- **Default:** `true`
|
||||
- **Description:** Restore the WireGuard interface states (up/down) that existed before WireGuard Portal started.
|
||||
|
||||
---
|
||||
|
||||
## Advanced
|
||||
|
||||
Additional or more specialized configuration options for logging and interface creation details.
|
||||
|
||||
### `log_level`
|
||||
- **Default:** `info`
|
||||
- **Description:** The log level used by the application. Valid options are: `trace`, `debug`, `info`, `warn`, `error`.
|
||||
|
||||
### `log_pretty`
|
||||
- **Default:** `false`
|
||||
- **Description:** If `true`, log messages are colorized and formatted for readability (pretty-print).
|
||||
|
||||
### `log_json`
|
||||
- **Default:** `false`
|
||||
- **Description:** If `true`, log messages are structured in JSON format.
|
||||
|
||||
### `start_listen_port`
|
||||
- **Default:** `51820`
|
||||
- **Description:** The first port to use when automatically creating new WireGuard interfaces.
|
||||
|
||||
### `start_cidr_v4`
|
||||
- **Default:** `10.11.12.0/24`
|
||||
- **Description:** The initial IPv4 subnet to use when automatically creating new WireGuard interfaces.
|
||||
|
||||
### `start_cidr_v6`
|
||||
- **Default:** `fdfd:d3ad:c0de:1234::0/64`
|
||||
- **Description:** The initial IPv6 subnet to use when automatically creating new WireGuard interfaces.
|
||||
|
||||
### `use_ip_v6`
|
||||
- **Default:** `true`
|
||||
- **Description:** Enable or disable IPv6 support.
|
||||
|
||||
### `config_storage_path`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** Path to a directory where `wg-quick` style configuration files will be stored (if you need local filesystem configs).
|
||||
|
||||
### `expiry_check_interval`
|
||||
- **Default:** `15m`
|
||||
- **Description:** Interval after which existing peers are checked if they are expired. Format uses `s`, `m`, `h`, `d` for seconds, minutes, hours, days, see [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration).
|
||||
|
||||
### `rule_prio_offset`
|
||||
- **Default:** `20000`
|
||||
- **Description:** Offset for IP route rule priorities when configuring routing.
|
||||
|
||||
### `route_table_offset`
|
||||
- **Default:** `20000`
|
||||
- **Description:** Offset for IP route table IDs when configuring routing.
|
||||
|
||||
### `api_admin_only`
|
||||
- **Default:** `true`
|
||||
- **Description:** If `true`, the public REST API is accessible only to admin users. The API docs live at [`/api/v1/doc.html`](../rest-api/api-doc.md).
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Database
|
||||
|
||||
Configuration for the underlying database used by WireGuard Portal.
|
||||
Supported databases include SQLite, MySQL, Microsoft SQL Server, and Postgres.
|
||||
|
||||
### `debug`
|
||||
- **Default:** `false`
|
||||
- **Description:** If `true`, logs all database statements (verbose).
|
||||
|
||||
### `slow_query_threshold`
|
||||
- **Default:** 0
|
||||
- **Description:** A time threshold (e.g., `100ms`) above which queries are considered slow and logged as warnings. If empty or zero, slow query logging is disabled. Format uses `s`, `ms` for seconds, milliseconds, see [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration).
|
||||
|
||||
### `type`
|
||||
- **Default:** `sqlite`
|
||||
- **Description:** The database type. Valid options: `sqlite`, `mssql`, `mysql`, `postgres`.
|
||||
|
||||
### `dsn`
|
||||
- **Default:** `data/sqlite.db`
|
||||
- **Description:** The Data Source Name (DSN) for connecting to the database.
|
||||
For example:
|
||||
```text
|
||||
user:pass@tcp(1.2.3.4:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Statistics
|
||||
|
||||
Controls how WireGuard Portal collects and reports usage statistics, including ping checks and Prometheus metrics.
|
||||
|
||||
### `use_ping_checks`
|
||||
- **Default:** `true`
|
||||
- **Description:** Enable periodic ping checks to verify that peers remain responsive.
|
||||
|
||||
### `ping_check_workers`
|
||||
- **Default:** `10`
|
||||
- **Description:** Number of parallel worker processes for ping checks.
|
||||
|
||||
### `ping_unprivileged`
|
||||
- **Default:** `false`
|
||||
- **Description:** If `false`, ping checks run without root privileges. This is currently considered BETA.
|
||||
|
||||
### `ping_check_interval`
|
||||
- **Default:** `1m`
|
||||
- **Description:** Interval between consecutive ping checks for all peers. Format uses `s`, `m`, `h`, `d` for seconds, minutes, hours, days, see [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration).
|
||||
|
||||
### `data_collection_interval`
|
||||
- **Default:** `1m`
|
||||
- **Description:** Interval between data collection cycles (bytes sent/received, handshake times, etc.). Format uses `s`, `m`, `h`, `d` for seconds, minutes, hours, days, see [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration).
|
||||
|
||||
### `collect_interface_data`
|
||||
- **Default:** `true`
|
||||
- **Description:** If `true`, collects interface-level data (bytes in/out) for monitoring and statistics.
|
||||
|
||||
### `collect_peer_data`
|
||||
- **Default:** `true`
|
||||
- **Description:** If `true`, collects peer-level data (bytes, last handshake, endpoint, etc.).
|
||||
|
||||
### `collect_audit_data`
|
||||
- **Default:** `true`
|
||||
- **Description:** If `true`, logs certain portal events (such as user logins) to the database.
|
||||
|
||||
### `listening_address`
|
||||
- **Default:** `:8787`
|
||||
- **Description:** Address and port for the integrated Prometheus metric server (e.g., `:8787`).
|
||||
|
||||
---
|
||||
|
||||
## Mail
|
||||
|
||||
Options for configuring email notifications or sending peer configurations via email.
|
||||
|
||||
### `host`
|
||||
- **Default:** `127.0.0.1`
|
||||
- **Description:** Hostname or IP of the SMTP server.
|
||||
|
||||
### `port`
|
||||
- **Default:** `25`
|
||||
- **Description:** Port number for the SMTP server.
|
||||
|
||||
### `encryption`
|
||||
- **Default:** `none`
|
||||
- **Description:** SMTP encryption type. Valid values: `none`, `tls`, `starttls`.
|
||||
|
||||
### `cert_validation`
|
||||
- **Default:** `false`
|
||||
- **Description:** If `true`, validate the SMTP server certificate (relevant if `encryption` = `tls`).
|
||||
|
||||
### `username`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** Optional SMTP username for authentication.
|
||||
|
||||
### `password`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** Optional SMTP password for authentication.
|
||||
|
||||
### `auth_type`
|
||||
- **Default:** `plain`
|
||||
- **Description:** SMTP authentication type. Valid values: `plain`, `login`, `crammd5`.
|
||||
|
||||
### `from`
|
||||
- **Default:** `Wireguard Portal <noreply@wireguard.local>`
|
||||
- **Description:** The default "From" address when sending emails.
|
||||
|
||||
### `link_only`
|
||||
- **Default:** `false`
|
||||
- **Description:** If `true`, emails only contain a link to WireGuard Portal, rather than attaching the full configuration.
|
||||
|
||||
---
|
||||
|
||||
## Auth
|
||||
|
||||
WireGuard Portal supports multiple authentication strategies, including **OpenID Connect** (`oidc`), **OAuth** (`oauth`), and **LDAP** (`ldap`).
|
||||
Each can have multiple providers configured. Below are the relevant keys.
|
||||
|
||||
---
|
||||
|
||||
### OIDC Provider Properties
|
||||
|
||||
The `oidc` array contains a list of OpenID Connect providers.
|
||||
Below are the properties for each OIDC provider entry inside `auth.oidc`:
|
||||
|
||||
#### `provider_name`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** A **unique** name for this provider. Must not conflict with other providers.
|
||||
|
||||
#### `display_name`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** A user-friendly name shown on the login page (e.g., "Login with Google").
|
||||
|
||||
#### `base_url`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** The OIDC provider’s base URL (e.g., `https://accounts.google.com`).
|
||||
|
||||
#### `client_id`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** The OAuth client ID from the OIDC provider.
|
||||
|
||||
#### `client_secret`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** The OAuth client secret from the OIDC provider.
|
||||
|
||||
#### `extra_scopes`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** A list of additional OIDC scopes (e.g., `profile`, `email`).
|
||||
|
||||
#### `field_map`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** Maps OIDC claims to WireGuard Portal user fields.
|
||||
- Available fields: `user_identifier`, `email`, `firstname`, `lastname`, `phone`, `department`, `is_admin`, `user_groups`.
|
||||
|
||||
| **Field** | **Typical OIDC Claim** | **Explanation** |
|
||||
|-------------------|-----------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `user_identifier` | `sub` or `preferred_username` | A unique identifier for the user. Often the OIDC `sub` claim is used because it’s guaranteed to be unique for the user within the IdP. Some providers also support `preferred_username` if it’s unique. |
|
||||
| `email` | `email` | The user’s email address as provided by the IdP. Not always verified, depending on IdP settings. |
|
||||
| `firstname` | `given_name` | The user’s first name, typically provided by the IdP in the `given_name` claim. |
|
||||
| `lastname` | `family_name` | The user’s last (family) name, typically provided by the IdP in the `family_name` claim. |
|
||||
| `phone` | `phone_number` | The user’s phone number. This may require additional scopes/permissions from the IdP to access. |
|
||||
| `department` | Custom claim (e.g., `department`) | If the IdP can provide organizational data, it may store it in a custom claim. Adjust accordingly (e.g., `department`, `org`, or another attribute). |
|
||||
| `is_admin` | Custom claim or derived role | If the IdP returns a role or admin flag, you can map that to `is_admin`. Often this is managed through custom claims or group membership. |
|
||||
| `user_groups` | `groups` or another custom claim | A list of group memberships for the user. Some IdPs provide `groups` out of the box; others require custom claims or directory lookups. |
|
||||
|
||||
#### `admin_mapping`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** WgPortal can grant a user admin rights by matching the value of the `is_admin` claim against a regular expression. Alternatively, a regular expression can be used to check if a user is member of a specific group listed in the `user_group` claim. The regular expressions are defined in `admin_value_regex` and `admin_group_regex`.
|
||||
- `admin_value_regex`: A regular expression to match the `is_admin` claim. By default, this expression matches the string "true" (`^true$`).
|
||||
- `admin_group_regex`: A regular expression to match the `user_groups` claim. Each entry in the `user_groups` claim is checked against this regex.
|
||||
|
||||
#### `registration_enabled`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** If `true`, a new user will be created in WireGuard Portal if not already present.
|
||||
|
||||
#### `log_user_info`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** If `true`, OIDC user data is logged at the trace level upon login (for debugging).
|
||||
|
||||
---
|
||||
|
||||
### OAuth Provider Properties
|
||||
|
||||
The `oauth` array contains a list of plain OAuth2 providers.
|
||||
Below are the properties for each OAuth provider entry inside `auth.oauth`:
|
||||
|
||||
#### `provider_name`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** A **unique** name for this provider. Must not conflict with other providers.
|
||||
|
||||
#### `display_name`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** A user-friendly name shown on the login page.
|
||||
|
||||
#### `client_id`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** The OAuth client ID for the provider.
|
||||
|
||||
#### `client_secret`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** The OAuth client secret for the provider.
|
||||
|
||||
#### `auth_url`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** URL of the authentication endpoint.
|
||||
|
||||
#### `token_url`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** URL of the token endpoint.
|
||||
|
||||
#### `user_info_url`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** URL of the user information endpoint.
|
||||
|
||||
#### `scopes`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** A list of OAuth scopes.
|
||||
|
||||
#### `field_map`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** Maps OAuth attributes to WireGuard Portal fields.
|
||||
- Available fields: `user_identifier`, `email`, `firstname`, `lastname`, `phone`, `department`, `is_admin`, `user_groups`.
|
||||
|
||||
| **Field** | **Typical Claim** | **Explanation** |
|
||||
|-------------------|-----------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `user_identifier` | `sub` or `preferred_username` | A unique identifier for the user. Often the OIDC `sub` claim is used because it’s guaranteed to be unique for the user within the IdP. Some providers also support `preferred_username` if it’s unique. |
|
||||
| `email` | `email` | The user’s email address as provided by the IdP. Not always verified, depending on IdP settings. |
|
||||
| `firstname` | `given_name` | The user’s first name, typically provided by the IdP in the `given_name` claim. |
|
||||
| `lastname` | `family_name` | The user’s last (family) name, typically provided by the IdP in the `family_name` claim. |
|
||||
| `phone` | `phone_number` | The user’s phone number. This may require additional scopes/permissions from the IdP to access. |
|
||||
| `department` | Custom claim (e.g., `department`) | If the IdP can provide organizational data, it may store it in a custom claim. Adjust accordingly (e.g., `department`, `org`, or another attribute). |
|
||||
| `is_admin` | Custom claim or derived role | If the IdP returns a role or admin flag, you can map that to `is_admin`. Often this is managed through custom claims or group membership. |
|
||||
| `user_groups` | `groups` or another custom claim | A list of group memberships for the user. Some IdPs provide `groups` out of the box; others require custom claims or directory lookups. |
|
||||
|
||||
#### `admin_mapping`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** WgPortal can grant a user admin rights by matching the value of the `is_admin` claim against a regular expression. Alternatively, a regular expression can be used to check if a user is member of a specific group listed in the `user_group` claim. The regular expressions are defined in `admin_value_regex` and `admin_group_regex`.
|
||||
- `admin_value_regex`: A regular expression to match the `is_admin` claim. By default, this expression matches the string "true" (`^true$`).
|
||||
- `admin_group_regex`: A regular expression to match the `user_groups` claim. Each entry in the `user_groups` claim is checked against this regex.
|
||||
|
||||
#### `registration_enabled`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** If `true`, new users are created automatically on successful login.
|
||||
|
||||
#### `log_user_info`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** If `true`, logs user info at the trace level upon login.
|
||||
|
||||
---
|
||||
|
||||
### LDAP Provider Properties
|
||||
|
||||
The `ldap` array contains a list of LDAP authentication providers.
|
||||
Below are the properties for each LDAP provider entry inside `auth.ldap`:
|
||||
|
||||
#### `url`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** The LDAP server URL (e.g., `ldap://srv-ad01.company.local:389`).
|
||||
|
||||
#### `start_tls`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** If `true`, use STARTTLS to secure the LDAP connection.
|
||||
|
||||
#### `cert_validation`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** If `true`, validate the LDAP server’s TLS certificate.
|
||||
|
||||
#### `tls_certificate_path`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** Path to a TLS certificate if needed for LDAP connections.
|
||||
|
||||
#### `tls_key_path`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** Path to the corresponding TLS certificate key.
|
||||
|
||||
#### `base_dn`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** The base DN for user searches (e.g., `DC=COMPANY,DC=LOCAL`).
|
||||
|
||||
#### `bind_user`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** The bind user for LDAP (e.g., `company\\ldap_wireguard` or `ldap_wireguard@company.local`).
|
||||
|
||||
#### `bind_pass`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** The bind password for LDAP authentication.
|
||||
|
||||
#### `field_map`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** Maps LDAP attributes to WireGuard Portal fields.
|
||||
- Available fields: `user_identifier`, `email`, `firstname`, `lastname`, `phone`, `department`, `memberof`.
|
||||
|
||||
| **WireGuard Portal Field** | **Typical LDAP Attribute** | **Short Description** |
|
||||
|----------------------------|----------------------------|--------------------------------------------------------------|
|
||||
| user_identifier | sAMAccountName / uid | Uniquely identifies the user within the LDAP directory. |
|
||||
| email | mail / userPrincipalName | Stores the user's primary email address. |
|
||||
| firstname | givenName | Contains the user's first (given) name. |
|
||||
| lastname | sn | Contains the user's last (surname) name. |
|
||||
| phone | telephoneNumber / mobile | Holds the user's phone or mobile number. |
|
||||
| department | departmentNumber / ou | Specifies the department or organizational unit of the user. |
|
||||
| memberof | memberOf | Lists the groups and roles to which the user belongs. |
|
||||
|
||||
#### `login_filter`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** An LDAP filter to restrict which users can log in. Use `{{login_identifier}}` to insert the username.
|
||||
For example:
|
||||
```text
|
||||
(&(objectClass=organizationalPerson)(mail={{login_identifier}})(!userAccountControl:1.2.840.113556.1.4.803:=2))
|
||||
```
|
||||
|
||||
#### `admin_group`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** A specific LDAP group whose members are considered administrators in WireGuard Portal.
|
||||
For example:
|
||||
```text
|
||||
CN=WireGuardAdmins,OU=Some-OU,DC=YOURDOMAIN,DC=LOCAL
|
||||
```
|
||||
|
||||
#### `sync_interval`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** How frequently (in duration, e.g. `30m`) to synchronize users from LDAP. Empty or `0` disables sync. Format uses `s`, `m`, `h`, `d` for seconds, minutes, hours, days, see [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration).
|
||||
Only users that match the `sync_filter` are synchronized, if `disable_missing` is `true`, users not found in LDAP are disabled.
|
||||
|
||||
#### `sync_filter`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** An LDAP filter to select which users get synchronized into WireGuard Portal.
|
||||
For example:
|
||||
```text
|
||||
(&(objectClass=organizationalPerson)(!userAccountControl:1.2.840.113556.1.4.803:=2)(mail=*))
|
||||
```
|
||||
|
||||
#### `disable_missing`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** If `true`, any user **not** found in LDAP (during sync) is disabled in WireGuard Portal.
|
||||
|
||||
#### `auto_re_enable`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** If `true`, users that where disabled because they were missing (see `disable_missing`) will be re-enabled once they are found again.
|
||||
|
||||
#### `registration_enabled`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** If `true`, new user accounts are created in WireGuard Portal upon first login.
|
||||
|
||||
#### `log_user_info`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** If `true`, logs LDAP user data at the trace level upon login.
|
@@ -1,5 +1,5 @@
|
||||
To build a standalone application, use the Makefile provided in the repository.
|
||||
Go version **1.22** or higher has to be installed to build WireGuard Portal.
|
||||
Go version **1.23** or higher has to be installed to build WireGuard Portal.
|
||||
If you want to re-compile the frontend, NodeJS **18** and NPM >= **9** is required.
|
||||
|
||||
```shell
|
||||
|
@@ -8,7 +8,7 @@ A sample docker-compose.yml:
|
||||
version: '3.6'
|
||||
services:
|
||||
wg-portal:
|
||||
image: wgportal/wg-portal:v2
|
||||
image: wgportal/wg-portal:latest
|
||||
restart: unless-stopped
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
@@ -64,18 +64,4 @@ You should mount those directories as a volume:
|
||||
- /app/data
|
||||
- /app/config
|
||||
|
||||
### Configuration Options
|
||||
All available YAML configuration options are available [here](https://github.com/h44z/wg-portal#configuration).
|
||||
|
||||
A very basic example:
|
||||
|
||||
```yaml
|
||||
core:
|
||||
admin_user: test@wg-portal.local
|
||||
admin_password: secret
|
||||
|
||||
web:
|
||||
external_url: http://localhost:8888
|
||||
request_logging: true
|
||||
```
|
||||
|
||||
A detailed description of the configuration options can be found [here](../configuration/overview.md).
|
||||
|
@@ -22,4 +22,15 @@ For example:
|
||||
```
|
||||
|
||||
The upgrade will transform the old, existing database and store the values in the new database specified in the **config.yml** configuration file.
|
||||
Ensure that the new database does not contain any data!
|
||||
Ensure that the new database does not contain any data!
|
||||
|
||||
If you are using Docker, you can adapt the docker-compose.yml file to start the upgrade process:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
wg-portal:
|
||||
image: wgportal/wg-portal:latest
|
||||
# ... other settings
|
||||
restart: no
|
||||
command: ["-migrateFrom=/app/data/wg_portal.db"]
|
||||
```
|
@@ -21,7 +21,7 @@ The configuration portal supports using a database (SQLite, MySQL, MsSQL or Post
|
||||
* Support for multiple WireGuard interfaces
|
||||
* Peer Expiry Feature
|
||||
* Handle route and DNS settings like wg-quick does
|
||||
* ~~REST API for management and client deployment~~ (coming soon)
|
||||
* REST API for management and client deployment
|
||||
|
||||
## Quick-Start
|
||||
|
||||
|
1
docs/documentation/rest-api/api-doc.md
Normal file
1
docs/documentation/rest-api/api-doc.md
Normal file
@@ -0,0 +1 @@
|
||||
<swagger-ui src="./swagger.yaml"/>
|
1543
docs/documentation/rest-api/swagger.yaml
Normal file
1543
docs/documentation/rest-api/swagger.yaml
Normal file
File diff suppressed because it is too large
Load Diff
@@ -165,7 +165,7 @@ async function del() {
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="flex-fill text-start">
|
||||
<button v-if="props.userId!=='#NEW#'&&formData.Source==='db'" class="btn btn-danger me-1" type="button" @click.prevent="del">{{ $t('general.delete') }}</button>
|
||||
<button v-if="props.userId!=='#NEW#'" class="btn btn-danger me-1" type="button" @click.prevent="del">{{ $t('general.delete') }}</button>
|
||||
</div>
|
||||
<button class="btn btn-primary me-1" type="button" @click.prevent="save">{{ $t('general.save') }}</button>
|
||||
<button class="btn btn-secondary" type="button" @click.prevent="close">{{ $t('general.close') }}</button>
|
||||
|
1
go.mod
1
go.mod
@@ -42,7 +42,6 @@ require (
|
||||
github.com/bytedance/sonic/loader v0.2.2 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dchest/uniuri v1.2.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
|
35
go.sum
35
go.sum
@@ -34,20 +34,15 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
|
||||
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60=
|
||||
github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20181103040241-659414f458e1/go.mod h1:dkChI7Tbtx7H1Tj7TqGSZMOeGpMP5gLHtjroHd4agiI=
|
||||
github.com/bytedance/sonic v1.12.6 h1:/isNmCUF2x3Sh8RAp/4mh4ZGkcFAX/hLrzrK3AvpRzk=
|
||||
github.com/bytedance/sonic v1.12.6/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
|
||||
github.com/bytedance/sonic v1.12.7 h1:CQU8pxOy9HToxhndH0Kx/S1qU/CuS9GnKYrGioDcU1Q=
|
||||
github.com/bytedance/sonic v1.12.7/go.mod h1:tnbal4mxOMju17EGfknm2XyYcpyCnIROYOEYuemj13I=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E=
|
||||
github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/bytedance/sonic/loader v0.2.2 h1:jxAJuN9fOot/cyz5Q6dUuMJF5OqQ6+5GfA8FjjQ0R4o=
|
||||
github.com/bytedance/sonic/loader v0.2.2/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/coreos/go-oidc/v3 v3.12.0 h1:sJk+8G2qq94rDI6ehZ71Bol3oUHy63qNYmkiSjrc/Jo=
|
||||
github.com/coreos/go-oidc/v3 v3.12.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
|
||||
@@ -100,8 +95,6 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o=
|
||||
github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/go-playground/validator/v10 v10.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE/HH+qdL2cBpCmg=
|
||||
github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
|
||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
@@ -308,12 +301,9 @@ golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329 h1:9kj3STMvgqy3YA4VQXBrN7925ICMxD5wzMRcgA30588=
|
||||
golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
|
||||
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
|
||||
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
@@ -340,7 +330,6 @@ golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
@@ -413,8 +402,6 @@ 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.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.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
|
||||
golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
|
||||
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
|
||||
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -422,8 +409,6 @@ golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uI
|
||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
|
||||
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=
|
||||
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
|
||||
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
google.golang.org/protobuf v1.36.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU=
|
||||
google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@@ -451,36 +436,28 @@ gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy
|
||||
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
||||
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||
modernc.org/cc/v4 v4.24.2 h1:uektamHbSXU7egelXcyVpMaaAsrRH4/+uMKUQAQUdOw=
|
||||
modernc.org/cc/v4 v4.24.2/go.mod h1:T1lKJZhXIi2VSqGBiB4LIbKs9NsKTbUXj4IDrmGqtTI=
|
||||
modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0=
|
||||
modernc.org/ccgo/v4 v4.23.5 h1:6uAwu8u3pnla3l/+UVUrDDO1HIGxHTYmFH6w+X9nsyw=
|
||||
modernc.org/ccgo/v4 v4.23.5/go.mod h1:FogrWfBdzqLWm1ku6cfr4IzEFouq2fSAPf6aSAHdAJQ=
|
||||
modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.23.10 h1:DnDZT/H6TtoJvQmVf7d8W+lVqEZpIJY/+0ENFh1LIHE=
|
||||
modernc.org/ccgo/v4 v4.23.10/go.mod h1:vdN4h2WR5aEoNondUx26K7G8X+nuBscYnAEWSRmN2/0=
|
||||
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
||||
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
|
||||
modernc.org/gc/v2 v2.6.0 h1:Tiw3pezQj7PfV8k4Dzyu/vhRHR2e92kOXtTFU8pbCl4=
|
||||
modernc.org/gc/v2 v2.6.0/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
|
||||
modernc.org/gc/v2 v2.6.1 h1:+Qf6xdG8l7B27TQ8D8lw/iFMUj1RXRBOuMUWziJOsk8=
|
||||
modernc.org/libc v1.61.6 h1:L2jW0wxHPCyHK0YSHaGaVlY0WxjpG/TTVdg6gRJOPqw=
|
||||
modernc.org/libc v1.61.6/go.mod h1:G+DzuaCcReUYYg4nNSfigIfTDCENdj9EByglvaRx53A=
|
||||
modernc.org/gc/v2 v2.6.1/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/libc v1.61.7 h1:exz8rasFniviSgh3dH7QBnQHqYh9lolA5hVYfsiwkfo=
|
||||
modernc.org/libc v1.61.7/go.mod h1:xspSrXRNVSfWfcfqgvZDVe/Hw5kv4FVC6IRfoms5v/0=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.8.1 h1:HS1HRg1jEohnuONobEq2WrLEhLyw8+J42yLFTnllm2A=
|
||||
modernc.org/memory v1.8.1/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU=
|
||||
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
||||
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
|
||||
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
|
||||
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/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.34.4 h1:sjdARozcL5KJBvYQvLlZEmctRgW9xqIZc2ncN7PU0P8=
|
||||
modernc.org/sqlite v1.34.4/go.mod h1:3QQFCG2SEMtc2nv+Wq4cQCH7Hjcg+p/RMlS1XK+zwbk=
|
||||
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
|
||||
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
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/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
|
@@ -10,6 +10,7 @@ import (
|
||||
"net/url"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/h44z/wg-portal/internal"
|
||||
"github.com/h44z/wg-portal/internal/app"
|
||||
"github.com/h44z/wg-portal/internal/app/api/v0/model"
|
||||
)
|
||||
@@ -70,7 +71,7 @@ func (e configEndpoint) handleConfigJsGet() gin.HandlerFunc {
|
||||
buf := &bytes.Buffer{}
|
||||
err := e.tpl.ExecuteTemplate(buf, "frontend_config.js.gotpl", gin.H{
|
||||
"BackendUrl": backendUrl,
|
||||
"Version": "unknown",
|
||||
"Version": internal.Version,
|
||||
"SiteTitle": e.app.Config.Web.SiteTitle,
|
||||
"SiteCompanyName": e.app.Config.Web.SiteCompanyName,
|
||||
})
|
||||
|
@@ -373,7 +373,7 @@ func (a *Authenticator) processUserInfo(
|
||||
case err != nil:
|
||||
return nil, fmt.Errorf("registration disabled, cannot create missing user: %w", err)
|
||||
default:
|
||||
err = a.updateExternalUser(ctx, user, userInfo)
|
||||
err = a.updateExternalUser(ctx, user, userInfo, source, provider)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update user: %w", err)
|
||||
}
|
||||
@@ -432,6 +432,8 @@ func (a *Authenticator) updateExternalUser(
|
||||
ctx context.Context,
|
||||
existingUser *domain.User,
|
||||
userInfo *domain.AuthenticatorUserInfo,
|
||||
source domain.UserSource,
|
||||
provider string,
|
||||
) error {
|
||||
if existingUser.IsLocked() || existingUser.IsDisabled() {
|
||||
return nil // user is locked or disabled, do not update
|
||||
@@ -462,6 +464,14 @@ func (a *Authenticator) updateExternalUser(
|
||||
existingUser.IsAdmin = userInfo.IsAdmin
|
||||
isChanged = true
|
||||
}
|
||||
if existingUser.Source != source {
|
||||
existingUser.Source = source
|
||||
isChanged = true
|
||||
}
|
||||
if existingUser.ProviderName != provider {
|
||||
existingUser.ProviderName = provider
|
||||
isChanged = true
|
||||
}
|
||||
|
||||
if !isChanged {
|
||||
return nil // nothing to update
|
||||
|
@@ -2,6 +2,7 @@ package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
@@ -9,6 +10,7 @@ import (
|
||||
"github.com/h44z/wg-portal/internal"
|
||||
"github.com/h44z/wg-portal/internal/config"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type LdapAuthenticator struct {
|
||||
@@ -78,7 +80,10 @@ func (l LdapAuthenticator) PlaintextAuthentication(userId domain.UserIdentifier,
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l LdapAuthenticator) GetUserInfo(_ context.Context, userId domain.UserIdentifier) (map[string]interface{}, error) {
|
||||
func (l LdapAuthenticator) GetUserInfo(_ context.Context, userId domain.UserIdentifier) (
|
||||
map[string]interface{},
|
||||
error,
|
||||
) {
|
||||
conn, err := internal.LdapConnect(l.cfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to setup connection: %w", err)
|
||||
@@ -109,6 +114,11 @@ func (l LdapAuthenticator) GetUserInfo(_ context.Context, userId domain.UserIden
|
||||
|
||||
users := internal.LdapConvertEntries(sr, &l.cfg.FieldMap)
|
||||
|
||||
if l.cfg.LogUserInfo {
|
||||
contents, _ := json.Marshal(users[0])
|
||||
logrus.Tracef("User info from LDAP source %s for %s: %v", l.GetName(), userId, string(contents))
|
||||
}
|
||||
|
||||
return users[0], nil
|
||||
}
|
||||
|
||||
|
@@ -6,12 +6,11 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/h44z/wg-portal/internal"
|
||||
"github.com/h44z/wg-portal/internal/config"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
@@ -21,10 +20,16 @@ type PlainOauthAuthenticator struct {
|
||||
userInfoEndpoint string
|
||||
client *http.Client
|
||||
userInfoMapping config.OauthFields
|
||||
userAdminMapping *config.OauthAdminMapping
|
||||
registrationEnabled bool
|
||||
userInfoLogging bool
|
||||
}
|
||||
|
||||
func newPlainOauthAuthenticator(_ context.Context, callbackUrl string, cfg *config.OAuthProvider) (*PlainOauthAuthenticator, error) {
|
||||
func newPlainOauthAuthenticator(
|
||||
_ context.Context,
|
||||
callbackUrl string,
|
||||
cfg *config.OAuthProvider,
|
||||
) (*PlainOauthAuthenticator, error) {
|
||||
var provider = &PlainOauthAuthenticator{}
|
||||
|
||||
provider.name = cfg.ProviderName
|
||||
@@ -44,7 +49,9 @@ func newPlainOauthAuthenticator(_ context.Context, callbackUrl string, cfg *conf
|
||||
}
|
||||
provider.userInfoEndpoint = cfg.UserInfoURL
|
||||
provider.userInfoMapping = getOauthFieldMapping(cfg.FieldMap)
|
||||
provider.userAdminMapping = &cfg.AdminMapping
|
||||
provider.registrationEnabled = cfg.RegistrationEnabled
|
||||
provider.userInfoLogging = cfg.LogUserInfo
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
@@ -65,11 +72,19 @@ func (p PlainOauthAuthenticator) AuthCodeURL(state string, opts ...oauth2.AuthCo
|
||||
return p.cfg.AuthCodeURL(state, opts...)
|
||||
}
|
||||
|
||||
func (p PlainOauthAuthenticator) Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
|
||||
func (p PlainOauthAuthenticator) Exchange(
|
||||
ctx context.Context,
|
||||
code string,
|
||||
opts ...oauth2.AuthCodeOption,
|
||||
) (*oauth2.Token, error) {
|
||||
return p.cfg.Exchange(ctx, code, opts...)
|
||||
}
|
||||
|
||||
func (p PlainOauthAuthenticator) GetUserInfo(ctx context.Context, token *oauth2.Token, _ string) (map[string]interface{}, error) {
|
||||
func (p PlainOauthAuthenticator) GetUserInfo(
|
||||
ctx context.Context,
|
||||
token *oauth2.Token,
|
||||
_ string,
|
||||
) (map[string]interface{}, error) {
|
||||
req, err := http.NewRequest("GET", p.userInfoEndpoint, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create user info get request: %w", err)
|
||||
@@ -93,57 +108,13 @@ func (p PlainOauthAuthenticator) GetUserInfo(ctx context.Context, token *oauth2.
|
||||
return nil, fmt.Errorf("failed to parse user info: %w", err)
|
||||
}
|
||||
|
||||
if p.userInfoLogging {
|
||||
logrus.Tracef("User info from OAuth source %s: %v", p.name, string(contents))
|
||||
}
|
||||
|
||||
return userFields, nil
|
||||
}
|
||||
|
||||
func (p PlainOauthAuthenticator) ParseUserInfo(raw map[string]interface{}) (*domain.AuthenticatorUserInfo, error) {
|
||||
isAdmin, _ := strconv.ParseBool(internal.MapDefaultString(raw, p.userInfoMapping.IsAdmin, ""))
|
||||
userInfo := &domain.AuthenticatorUserInfo{
|
||||
Identifier: domain.UserIdentifier(internal.MapDefaultString(raw, p.userInfoMapping.UserIdentifier, "")),
|
||||
Email: internal.MapDefaultString(raw, p.userInfoMapping.Email, ""),
|
||||
Firstname: internal.MapDefaultString(raw, p.userInfoMapping.Firstname, ""),
|
||||
Lastname: internal.MapDefaultString(raw, p.userInfoMapping.Lastname, ""),
|
||||
Phone: internal.MapDefaultString(raw, p.userInfoMapping.Phone, ""),
|
||||
Department: internal.MapDefaultString(raw, p.userInfoMapping.Department, ""),
|
||||
IsAdmin: isAdmin,
|
||||
}
|
||||
|
||||
return userInfo, nil
|
||||
}
|
||||
|
||||
func getOauthFieldMapping(f config.OauthFields) config.OauthFields {
|
||||
defaultMap := config.OauthFields{
|
||||
BaseFields: config.BaseFields{
|
||||
UserIdentifier: "sub",
|
||||
Email: "email",
|
||||
Firstname: "given_name",
|
||||
Lastname: "family_name",
|
||||
Phone: "phone",
|
||||
Department: "department",
|
||||
},
|
||||
IsAdmin: "admin_flag",
|
||||
}
|
||||
if f.UserIdentifier != "" {
|
||||
defaultMap.UserIdentifier = f.UserIdentifier
|
||||
}
|
||||
if f.Email != "" {
|
||||
defaultMap.Email = f.Email
|
||||
}
|
||||
if f.Firstname != "" {
|
||||
defaultMap.Firstname = f.Firstname
|
||||
}
|
||||
if f.Lastname != "" {
|
||||
defaultMap.Lastname = f.Lastname
|
||||
}
|
||||
if f.Phone != "" {
|
||||
defaultMap.Phone = f.Phone
|
||||
}
|
||||
if f.Department != "" {
|
||||
defaultMap.Department = f.Department
|
||||
}
|
||||
if f.IsAdmin != "" {
|
||||
defaultMap.IsAdmin = f.IsAdmin
|
||||
}
|
||||
|
||||
return defaultMap
|
||||
return parseOauthUserInfo(p.userInfoMapping, p.userAdminMapping, raw)
|
||||
}
|
||||
|
@@ -2,14 +2,14 @@ package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/h44z/wg-portal/internal"
|
||||
"github.com/h44z/wg-portal/internal/config"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
@@ -19,15 +19,22 @@ type OidcAuthenticator struct {
|
||||
verifier *oidc.IDTokenVerifier
|
||||
cfg *oauth2.Config
|
||||
userInfoMapping config.OauthFields
|
||||
userAdminMapping *config.OauthAdminMapping
|
||||
registrationEnabled bool
|
||||
userInfoLogging bool
|
||||
}
|
||||
|
||||
func newOidcAuthenticator(ctx context.Context, callbackUrl string, cfg *config.OpenIDConnectProvider) (*OidcAuthenticator, error) {
|
||||
func newOidcAuthenticator(
|
||||
_ context.Context,
|
||||
callbackUrl string,
|
||||
cfg *config.OpenIDConnectProvider,
|
||||
) (*OidcAuthenticator, error) {
|
||||
var err error
|
||||
var provider = &OidcAuthenticator{}
|
||||
|
||||
provider.name = cfg.ProviderName
|
||||
provider.provider, err = oidc.NewProvider(context.Background(), cfg.BaseUrl) // use new context here, see https://github.com/coreos/go-oidc/issues/339
|
||||
provider.provider, err = oidc.NewProvider(context.Background(),
|
||||
cfg.BaseUrl) // use new context here, see https://github.com/coreos/go-oidc/issues/339
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create new oidc provider: %w", err)
|
||||
}
|
||||
@@ -45,7 +52,9 @@ func newOidcAuthenticator(ctx context.Context, callbackUrl string, cfg *config.O
|
||||
Scopes: scopes,
|
||||
}
|
||||
provider.userInfoMapping = getOauthFieldMapping(cfg.FieldMap)
|
||||
provider.userAdminMapping = &cfg.AdminMapping
|
||||
provider.registrationEnabled = cfg.RegistrationEnabled
|
||||
provider.userInfoLogging = cfg.LogUserInfo
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
@@ -66,11 +75,17 @@ func (o OidcAuthenticator) AuthCodeURL(state string, opts ...oauth2.AuthCodeOpti
|
||||
return o.cfg.AuthCodeURL(state, opts...)
|
||||
}
|
||||
|
||||
func (o OidcAuthenticator) Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
|
||||
func (o OidcAuthenticator) Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (
|
||||
*oauth2.Token,
|
||||
error,
|
||||
) {
|
||||
return o.cfg.Exchange(ctx, code, opts...)
|
||||
}
|
||||
|
||||
func (o OidcAuthenticator) GetUserInfo(ctx context.Context, token *oauth2.Token, nonce string) (map[string]interface{}, error) {
|
||||
func (o OidcAuthenticator) GetUserInfo(ctx context.Context, token *oauth2.Token, nonce string) (
|
||||
map[string]interface{},
|
||||
error,
|
||||
) {
|
||||
rawIDToken, ok := token.Extra("id_token").(string)
|
||||
if !ok {
|
||||
return nil, errors.New("token does not contain id_token")
|
||||
@@ -88,20 +103,14 @@ func (o OidcAuthenticator) GetUserInfo(ctx context.Context, token *oauth2.Token,
|
||||
return nil, fmt.Errorf("failed to parse extra claims: %w", err)
|
||||
}
|
||||
|
||||
if o.userInfoLogging {
|
||||
contents, _ := json.Marshal(tokenFields)
|
||||
logrus.Tracef("User info from OIDC source %s: %v", o.name, string(contents))
|
||||
}
|
||||
|
||||
return tokenFields, nil
|
||||
}
|
||||
|
||||
func (o OidcAuthenticator) ParseUserInfo(raw map[string]interface{}) (*domain.AuthenticatorUserInfo, error) {
|
||||
isAdmin, _ := strconv.ParseBool(internal.MapDefaultString(raw, o.userInfoMapping.IsAdmin, ""))
|
||||
userInfo := &domain.AuthenticatorUserInfo{
|
||||
Identifier: domain.UserIdentifier(internal.MapDefaultString(raw, o.userInfoMapping.UserIdentifier, "")),
|
||||
Email: internal.MapDefaultString(raw, o.userInfoMapping.Email, ""),
|
||||
Firstname: internal.MapDefaultString(raw, o.userInfoMapping.Firstname, ""),
|
||||
Lastname: internal.MapDefaultString(raw, o.userInfoMapping.Lastname, ""),
|
||||
Phone: internal.MapDefaultString(raw, o.userInfoMapping.Phone, ""),
|
||||
Department: internal.MapDefaultString(raw, o.userInfoMapping.Department, ""),
|
||||
IsAdmin: isAdmin,
|
||||
}
|
||||
|
||||
return userInfo, nil
|
||||
return parseOauthUserInfo(o.userInfoMapping, o.userAdminMapping, raw)
|
||||
}
|
||||
|
92
internal/app/auth/oauth_common.go
Normal file
92
internal/app/auth/oauth_common.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/h44z/wg-portal/internal"
|
||||
"github.com/h44z/wg-portal/internal/config"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
)
|
||||
|
||||
// parseOauthUserInfo parses the raw user info from the oauth provider and maps it to the internal user info struct
|
||||
func parseOauthUserInfo(
|
||||
mapping config.OauthFields,
|
||||
adminMapping *config.OauthAdminMapping,
|
||||
raw map[string]interface{},
|
||||
) (*domain.AuthenticatorUserInfo, error) {
|
||||
var isAdmin bool
|
||||
|
||||
// first try to match the is_admin field against the given regex
|
||||
if mapping.IsAdmin != "" {
|
||||
re := adminMapping.GetAdminValueRegex()
|
||||
if re.MatchString(strings.TrimSpace(internal.MapDefaultString(raw, mapping.IsAdmin, ""))) {
|
||||
isAdmin = true
|
||||
}
|
||||
}
|
||||
|
||||
// next try to parse the user's groups
|
||||
if !isAdmin && mapping.UserGroups != "" && adminMapping.AdminGroupRegex != "" {
|
||||
userGroups := internal.MapDefaultStringSlice(raw, mapping.UserGroups, nil)
|
||||
re := adminMapping.GetAdminGroupRegex()
|
||||
for _, group := range userGroups {
|
||||
if re.MatchString(strings.TrimSpace(group)) {
|
||||
isAdmin = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
userInfo := &domain.AuthenticatorUserInfo{
|
||||
Identifier: domain.UserIdentifier(internal.MapDefaultString(raw, mapping.UserIdentifier, "")),
|
||||
Email: internal.MapDefaultString(raw, mapping.Email, ""),
|
||||
Firstname: internal.MapDefaultString(raw, mapping.Firstname, ""),
|
||||
Lastname: internal.MapDefaultString(raw, mapping.Lastname, ""),
|
||||
Phone: internal.MapDefaultString(raw, mapping.Phone, ""),
|
||||
Department: internal.MapDefaultString(raw, mapping.Department, ""),
|
||||
IsAdmin: isAdmin,
|
||||
}
|
||||
|
||||
return userInfo, nil
|
||||
}
|
||||
|
||||
// getOauthFieldMapping returns the default field mapping for the oauth provider
|
||||
func getOauthFieldMapping(f config.OauthFields) config.OauthFields {
|
||||
defaultMap := config.OauthFields{
|
||||
BaseFields: config.BaseFields{
|
||||
UserIdentifier: "sub",
|
||||
Email: "email",
|
||||
Firstname: "given_name",
|
||||
Lastname: "family_name",
|
||||
Phone: "phone",
|
||||
Department: "department",
|
||||
},
|
||||
IsAdmin: "admin_flag",
|
||||
UserGroups: "", // by default, do not use user groups
|
||||
}
|
||||
if f.UserIdentifier != "" {
|
||||
defaultMap.UserIdentifier = f.UserIdentifier
|
||||
}
|
||||
if f.Email != "" {
|
||||
defaultMap.Email = f.Email
|
||||
}
|
||||
if f.Firstname != "" {
|
||||
defaultMap.Firstname = f.Firstname
|
||||
}
|
||||
if f.Lastname != "" {
|
||||
defaultMap.Lastname = f.Lastname
|
||||
}
|
||||
if f.Phone != "" {
|
||||
defaultMap.Phone = f.Phone
|
||||
}
|
||||
if f.Department != "" {
|
||||
defaultMap.Department = f.Department
|
||||
}
|
||||
if f.IsAdmin != "" {
|
||||
defaultMap.IsAdmin = f.IsAdmin
|
||||
}
|
||||
if f.UserGroups != "" {
|
||||
defaultMap.UserGroups = f.UserGroups
|
||||
}
|
||||
|
||||
return defaultMap
|
||||
}
|
57
internal/app/auth/oauth_common_test.go
Normal file
57
internal/app/auth/oauth_common_test.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/h44z/wg-portal/internal/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_parseOauthUserInfo(t *testing.T) {
|
||||
userInfoStr := `
|
||||
{
|
||||
"at_hash": "REDACTED",
|
||||
"aud": "REDACTED",
|
||||
"c_hash": "REDACTED",
|
||||
"email": "test@mydomain.net",
|
||||
"email_verified": true,
|
||||
"exp": 1737404259,
|
||||
"groups": [
|
||||
"abuse@mydomain.net",
|
||||
"postmaster@mydomain.net",
|
||||
"wgportal-admins@mydomain.net"
|
||||
],
|
||||
"iat": 1737317859,
|
||||
"iss": "https://dex.mydomain.net",
|
||||
"name": "Test User",
|
||||
"nonce": "REDACTED",
|
||||
"sub": "REDACTED"
|
||||
}
|
||||
`
|
||||
|
||||
userInfo := map[string]interface{}{}
|
||||
err := json.Unmarshal([]byte(userInfoStr), &userInfo)
|
||||
require.NoError(t, err)
|
||||
|
||||
fieldMapping := getOauthFieldMapping(config.OauthFields{
|
||||
BaseFields: config.BaseFields{
|
||||
UserIdentifier: "email",
|
||||
Email: "email",
|
||||
Firstname: "name",
|
||||
Lastname: "family_name",
|
||||
},
|
||||
UserGroups: "groups",
|
||||
})
|
||||
adminMapping := &config.OauthAdminMapping{
|
||||
AdminGroupRegex: "^wgportal-admins@mydomain.net$",
|
||||
}
|
||||
|
||||
info, err := parseOauthUserInfo(fieldMapping, adminMapping, userInfo)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, info.IsAdmin)
|
||||
assert.Equal(t, info.Firstname, "Test User")
|
||||
assert.Equal(t, info.Lastname, "")
|
||||
assert.Equal(t, info.Email, "test@mydomain.net")
|
||||
}
|
@@ -71,5 +71,9 @@ func userChangedInLdap(dbUser, ldapUser *domain.User) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
if dbUser.ProviderName != ldapUser.ProviderName {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
@@ -73,11 +73,16 @@ func (m Manager) NewUser(ctx context.Context, user *domain.User) error {
|
||||
u.Identifier = user.Identifier
|
||||
u.Email = user.Email
|
||||
u.Source = user.Source
|
||||
u.ProviderName = user.ProviderName
|
||||
u.IsAdmin = user.IsAdmin
|
||||
u.Firstname = user.Firstname
|
||||
u.Lastname = user.Lastname
|
||||
u.Phone = user.Phone
|
||||
u.Department = user.Department
|
||||
u.Notes = user.Notes
|
||||
u.ApiToken = user.ApiToken
|
||||
u.ApiTokenCreated = user.ApiTokenCreated
|
||||
|
||||
return u, nil
|
||||
})
|
||||
if err != nil {
|
||||
@@ -421,13 +426,14 @@ func (m Manager) runLdapSynchronizationService(ctx context.Context) {
|
||||
logrus.Debugf("sync disabled for LDAP server: %s", cfg.ProviderName)
|
||||
return
|
||||
}
|
||||
|
||||
running := true
|
||||
for running {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
running = false
|
||||
continue
|
||||
case <-time.After(syncInterval * time.Second):
|
||||
case <-time.After(syncInterval):
|
||||
// select blocks until one of the cases evaluate to true
|
||||
}
|
||||
|
||||
@@ -460,10 +466,10 @@ func (m Manager) synchronizeLdapUsers(ctx context.Context, provider *config.Ldap
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Tracef("fetched %d raw ldap users...", len(rawUsers))
|
||||
logrus.Tracef("fetched %d raw ldap users from provider %s...", len(rawUsers), provider.ProviderName)
|
||||
|
||||
// Update existing LDAP users
|
||||
err = m.updateLdapUsers(ctx, provider.ProviderName, rawUsers, &provider.FieldMap, provider.ParsedAdminGroupDN)
|
||||
err = m.updateLdapUsers(ctx, provider, rawUsers, &provider.FieldMap, provider.ParsedAdminGroupDN)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -481,13 +487,13 @@ func (m Manager) synchronizeLdapUsers(ctx context.Context, provider *config.Ldap
|
||||
|
||||
func (m Manager) updateLdapUsers(
|
||||
ctx context.Context,
|
||||
providerName string,
|
||||
provider *config.LdapProvider,
|
||||
rawUsers []internal.RawLdapUser,
|
||||
fields *config.LdapFields,
|
||||
adminGroupDN *ldap.DN,
|
||||
) error {
|
||||
for _, rawUser := range rawUsers {
|
||||
user, err := convertRawLdapUser(providerName, rawUser, fields, adminGroupDN)
|
||||
user, err := convertRawLdapUser(provider.ProviderName, rawUser, fields, adminGroupDN)
|
||||
if err != nil && !errors.Is(err, domain.ErrNotFound) {
|
||||
return fmt.Errorf("failed to convert LDAP data for %v: %w", rawUser["dn"], err)
|
||||
}
|
||||
@@ -497,37 +503,57 @@ func (m Manager) updateLdapUsers(
|
||||
return fmt.Errorf("find error for user id %s: %w", user.Identifier, err)
|
||||
}
|
||||
|
||||
tctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
tctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
tctx = domain.SetUserInfo(tctx, domain.SystemAdminContextUserInfo())
|
||||
|
||||
// create new user
|
||||
if existingUser == nil {
|
||||
err := m.NewUser(tctx, user)
|
||||
if err != nil {
|
||||
cancel()
|
||||
return fmt.Errorf("create error for user id %s: %w", user.Identifier, err)
|
||||
}
|
||||
|
||||
cancel()
|
||||
return nil
|
||||
}
|
||||
|
||||
if existingUser != nil && existingUser.Source == domain.UserSourceLdap && userChangedInLdap(existingUser,
|
||||
user) {
|
||||
|
||||
// update existing user
|
||||
if provider.AutoReEnable && existingUser.DisabledReason == domain.DisabledReasonLdapMissing {
|
||||
user.Disabled = nil
|
||||
user.DisabledReason = ""
|
||||
} else {
|
||||
user.Disabled = existingUser.Disabled
|
||||
user.DisabledReason = existingUser.DisabledReason
|
||||
}
|
||||
if existingUser.Source == domain.UserSourceLdap && userChangedInLdap(existingUser, user) {
|
||||
err := m.users.SaveUser(tctx, user.Identifier, func(u *domain.User) (*domain.User, error) {
|
||||
u.UpdatedAt = time.Now()
|
||||
u.UpdatedBy = domain.CtxSystemLdapSyncer
|
||||
u.Source = user.Source
|
||||
u.ProviderName = user.ProviderName
|
||||
u.Email = user.Email
|
||||
u.Firstname = user.Firstname
|
||||
u.Lastname = user.Lastname
|
||||
u.Phone = user.Phone
|
||||
u.Department = user.Department
|
||||
u.IsAdmin = user.IsAdmin
|
||||
u.Disabled = user.Disabled
|
||||
u.Disabled = nil
|
||||
u.DisabledReason = ""
|
||||
|
||||
return u, nil
|
||||
})
|
||||
if err != nil {
|
||||
cancel()
|
||||
return fmt.Errorf("update error for user id %s: %w", user.Identifier, err)
|
||||
}
|
||||
|
||||
if existingUser.IsDisabled() && !user.IsDisabled() {
|
||||
m.bus.Publish(app.TopicUserEnabled, *user)
|
||||
}
|
||||
}
|
||||
|
||||
cancel()
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -567,10 +593,15 @@ func (m Manager) disableMissingLdapUsers(
|
||||
continue
|
||||
}
|
||||
|
||||
logrus.Tracef("user %s is missing in ldap provider %s, disabling", user.Identifier, providerName)
|
||||
|
||||
now := time.Now()
|
||||
user.Disabled = &now
|
||||
user.DisabledReason = domain.DisabledReasonLdapMissing
|
||||
|
||||
err := m.users.SaveUser(ctx, user.Identifier, func(u *domain.User) (*domain.User, error) {
|
||||
now := time.Now()
|
||||
u.Disabled = &now
|
||||
u.DisabledReason = "missing in ldap"
|
||||
u.Disabled = user.Disabled
|
||||
u.DisabledReason = user.DisabledReason
|
||||
return u, nil
|
||||
})
|
||||
if err != nil {
|
||||
|
@@ -1,9 +1,11 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Auth struct {
|
||||
@@ -23,7 +25,67 @@ type BaseFields struct {
|
||||
|
||||
type OauthFields struct {
|
||||
BaseFields `yaml:",inline"`
|
||||
IsAdmin string `yaml:"is_admin"` // If the value is "true", the user is an admin.
|
||||
IsAdmin string `yaml:"is_admin"` // If the value is "true", the user is an admin.
|
||||
UserGroups string `yaml:"user_groups"` // This value specifies the claim name that contains the users groups.
|
||||
}
|
||||
|
||||
// OauthAdminMapping contains all necessary information to extract information about administrative privileges
|
||||
// from the user info fields.
|
||||
//
|
||||
// WgPortal can grant a user admin rights by matching the value of the `is_admin` claim against a regular expression.
|
||||
// Alternatively, a regular expression can be used to check if a user is member of a specific group listed in the
|
||||
// `user_group` claim.
|
||||
// If one of the cases evaluates to true, the user is granted admin rights.
|
||||
type OauthAdminMapping struct {
|
||||
// If the regex specified in that field matches the contents of the is_admin field, the user is an admin.
|
||||
AdminValueRegex string `yaml:"admin_value_regex"`
|
||||
|
||||
// If any of the groups listed in the groups field matches the group specified in the admin_group_regex field, ]
|
||||
// the user is an admin.
|
||||
AdminGroupRegex string `yaml:"admin_group_regex"`
|
||||
|
||||
// internal cache fields
|
||||
|
||||
adminValueRegex *regexp.Regexp
|
||||
adminGroupRegex *regexp.Regexp
|
||||
}
|
||||
|
||||
func (o *OauthAdminMapping) GetAdminValueRegex() *regexp.Regexp {
|
||||
if o.adminValueRegex != nil {
|
||||
return o.adminValueRegex // return cached value
|
||||
}
|
||||
|
||||
if o.AdminValueRegex == "" {
|
||||
o.adminValueRegex = regexp.MustCompile("^true$") // default value is "true"
|
||||
return o.adminValueRegex
|
||||
}
|
||||
|
||||
adminRegex, err := regexp.Compile(o.AdminValueRegex)
|
||||
if err != nil {
|
||||
logrus.Fatalf("failed to compile admin_value_regex: %v", err)
|
||||
}
|
||||
o.adminValueRegex = adminRegex
|
||||
|
||||
return o.adminValueRegex
|
||||
}
|
||||
|
||||
func (o *OauthAdminMapping) GetAdminGroupRegex() *regexp.Regexp {
|
||||
if o.adminGroupRegex != nil {
|
||||
return o.adminGroupRegex // return cached value
|
||||
}
|
||||
|
||||
if o.AdminGroupRegex == "" {
|
||||
o.adminGroupRegex = regexp.MustCompile("^wg_portal_default_admin_group$") // default value is "wg_portal_default_admin_group"
|
||||
return o.adminGroupRegex
|
||||
}
|
||||
|
||||
groupRegex, err := regexp.Compile(o.AdminGroupRegex)
|
||||
if err != nil {
|
||||
logrus.Fatalf("failed to compile admin_group_regex: %v", err)
|
||||
}
|
||||
o.adminGroupRegex = groupRegex
|
||||
|
||||
return o.adminGroupRegex
|
||||
}
|
||||
|
||||
type LdapFields struct {
|
||||
@@ -52,12 +114,17 @@ type LdapProvider struct {
|
||||
ParsedAdminGroupDN *ldap.DN `yaml:"-"`
|
||||
|
||||
// If DisableMissing is true, missing users will be deactivated
|
||||
DisableMissing bool `yaml:"disable_missing"`
|
||||
SyncFilter string `yaml:"sync_filter"`
|
||||
SyncInterval time.Duration `yaml:"sync_interval"`
|
||||
DisableMissing bool `yaml:"disable_missing"`
|
||||
// If AutoReEnable is true, users that where disabled because they were missing will be re-enabled once they are found again
|
||||
AutoReEnable bool `yaml:"auto_re_enable"`
|
||||
SyncFilter string `yaml:"sync_filter"`
|
||||
SyncInterval time.Duration `yaml:"sync_interval"`
|
||||
|
||||
// If RegistrationEnabled is set to true, wg-portal will create new users that do not exist in the database.
|
||||
RegistrationEnabled bool `yaml:"registration_enabled"`
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
type OpenIDConnectProvider struct {
|
||||
@@ -81,8 +148,15 @@ type OpenIDConnectProvider struct {
|
||||
// FieldMap is used to map the names of the user-info endpoint fields to wg-portal fields
|
||||
FieldMap OauthFields `yaml:"field_map"`
|
||||
|
||||
// AdminMapping contains all necessary information to extract information about administrative privileges
|
||||
// from the user info fields.
|
||||
AdminMapping OauthAdminMapping `yaml:"admin_mapping"`
|
||||
|
||||
// If RegistrationEnabled is set to true, missing users will be created in the database
|
||||
RegistrationEnabled bool `yaml:"registration_enabled"`
|
||||
|
||||
// If LogUserInfo is set to true, the user info retrieved from the OIDC provider will be logged in trace level.
|
||||
LogUserInfo bool `yaml:"log_user_info"`
|
||||
}
|
||||
|
||||
type OAuthProvider struct {
|
||||
@@ -108,6 +182,13 @@ type OAuthProvider struct {
|
||||
// FieldMap is used to map the names of the user-info endpoint fields to wg-portal fields
|
||||
FieldMap OauthFields `yaml:"field_map"`
|
||||
|
||||
// AdminMapping contains all necessary information to extract information about administrative privileges
|
||||
// from the user info fields.
|
||||
AdminMapping OauthAdminMapping `yaml:"admin_mapping"`
|
||||
|
||||
// If RegistrationEnabled is set to true, wg-portal will create new users that do not exist in the database.
|
||||
RegistrationEnabled bool `yaml:"registration_enabled"`
|
||||
|
||||
// If LogUserInfo is set to true, the user info retrieved from the OAuth provider will be logged in trace level.
|
||||
LogUserInfo bool `yaml:"log_user_info"`
|
||||
}
|
||||
|
@@ -101,11 +101,7 @@ func (u *User) EditAllowed(new *User) error {
|
||||
}
|
||||
|
||||
func (u *User) DeleteAllowed() error {
|
||||
if u.Source == UserSourceDatabase {
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.New("delete only allowed for database source")
|
||||
return nil // all users can be deleted, OAuth and LDAP users might still be recreated
|
||||
}
|
||||
|
||||
func (u *User) CheckPassword(password string) error {
|
||||
|
@@ -79,6 +79,38 @@ func MapDefaultString(m map[string]interface{}, key string, dflt string) string
|
||||
}
|
||||
}
|
||||
|
||||
// MapDefaultStringSlice returns the string slice value for the given key or a default value
|
||||
func MapDefaultStringSlice(m map[string]interface{}, key string, dflt []string) []string {
|
||||
if m == nil {
|
||||
return dflt
|
||||
}
|
||||
if tmp, ok := m[key]; !ok {
|
||||
return dflt
|
||||
} else {
|
||||
switch v := tmp.(type) {
|
||||
case []any:
|
||||
result := make([]string, 0, len(v))
|
||||
for _, elem := range v {
|
||||
switch vElem := elem.(type) {
|
||||
case string:
|
||||
result = append(result, vElem)
|
||||
default:
|
||||
result = append(result, fmt.Sprintf("%v", vElem))
|
||||
}
|
||||
}
|
||||
return result
|
||||
case []string:
|
||||
return v
|
||||
case string:
|
||||
return []string{v}
|
||||
case nil:
|
||||
return dflt
|
||||
default:
|
||||
return []string{fmt.Sprintf("%v", v)}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UniqueStringSlice removes duplicates in the given string slice
|
||||
func UniqueStringSlice(slice []string) []string {
|
||||
keys := make(map[string]struct{})
|
||||
|
@@ -4,7 +4,7 @@ site_description: Manage WireGuard Peers and Interface using a beautiful and sim
|
||||
site_url: https://wgportal.org/
|
||||
repo_name: h44z/wg-portal
|
||||
repo_url: https://github.com/h44z/wg-portal
|
||||
copyright: Copyright © 2023-2024 WireGuard Portal Project
|
||||
copyright: Copyright © 2023-2025 WireGuard Portal Project
|
||||
|
||||
extra_css:
|
||||
- stylesheets/extra.css
|
||||
@@ -21,12 +21,14 @@ theme:
|
||||
features:
|
||||
- navigation.instant
|
||||
- navigation.tabs
|
||||
- navigation.expand
|
||||
|
||||
plugins:
|
||||
- search
|
||||
- social
|
||||
- minify:
|
||||
minify_html: true
|
||||
- swagger-ui-tag
|
||||
|
||||
extra:
|
||||
version:
|
||||
@@ -60,3 +62,7 @@ nav:
|
||||
- Building: documentation/getting-started/building.md
|
||||
- Docker Container: documentation/getting-started/docker.md
|
||||
- Upgrade from V1: documentation/getting-started/upgrade.md
|
||||
- Configuration:
|
||||
- Overview: documentation/configuration/overview.md
|
||||
- Examples: documentation/configuration/examples.md
|
||||
- REST API: documentation/rest-api/api-doc.md
|
||||
|
Reference in New Issue
Block a user