From 662e9c0549eb68f0df4ba56e43c2c8dcd5bb0969 Mon Sep 17 00:00:00 2001 From: Christoph Haas Date: Sat, 18 Jan 2025 11:55:56 +0100 Subject: [PATCH] Improve admin privilege handling for OAuth. Update documentation. --- Makefile | 6 + README.md | 198 ++++---- config.yml.sample | 31 +- docs/documentation/configuration/examples.md | 176 +++++++ docs/documentation/configuration/overview.md | 453 ++++++++++++++++++ .../documentation/getting-started/building.md | 2 +- docs/documentation/getting-started/docker.md | 18 +- docs/documentation/getting-started/upgrade.md | 13 +- internal/app/auth/auth_ldap.go | 12 +- internal/app/auth/auth_oauth.go | 79 +-- internal/app/auth/auth_oidc.go | 45 +- internal/app/auth/oauth_common.go | 88 ++++ internal/config/auth.go | 81 +++- internal/util.go | 21 + mkdocs.yml | 4 + 15 files changed, 1036 insertions(+), 191 deletions(-) create mode 100644 docs/documentation/configuration/examples.md create mode 100644 docs/documentation/configuration/overview.md create mode 100644 internal/app/auth/oauth_common.go diff --git a/Makefile b/Makefile index 0b72e163..2bf202bb 100644 --- a/Makefile +++ b/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 diff --git a/README.md b/README.md index 6c53eaf2..1d826d21 100644 --- a/README.md +++ b/README.md @@ -55,98 +55,107 @@ 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 | 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 | 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. | +| 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 +182,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 diff --git a/config.yml.sample b/config.yml.sample index e2337e5f..043d3614 100644 --- a/config.yml.sample +++ b/config.yml.sample @@ -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 \ No newline at end of file + is_admin: this-attribute-must-be-true + registration_enabled: true + - id: google_plain_oauth_with_groups + provider_name: google4 + display_name: Login with
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 \ No newline at end of file diff --git a/docs/documentation/configuration/examples.md b/docs/documentation/configuration/examples.md new file mode 100644 index 00000000..9e7eae6c --- /dev/null +++ b/docs/documentation/configuration/examples.md @@ -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
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
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
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
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
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 +``` \ No newline at end of file diff --git a/docs/documentation/configuration/overview.md b/docs/documentation/configuration/overview.md new file mode 100644 index 00000000..432898ab --- /dev/null +++ b/docs/documentation/configuration/overview.md @@ -0,0 +1,453 @@ +# 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 ` +- **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. + +#### `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. diff --git a/docs/documentation/getting-started/building.md b/docs/documentation/getting-started/building.md index 0c372bed..25f9e9b9 100644 --- a/docs/documentation/getting-started/building.md +++ b/docs/documentation/getting-started/building.md @@ -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 diff --git a/docs/documentation/getting-started/docker.md b/docs/documentation/getting-started/docker.md index 34fd2f66..518a9318 100644 --- a/docs/documentation/getting-started/docker.md +++ b/docs/documentation/getting-started/docker.md @@ -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). diff --git a/docs/documentation/getting-started/upgrade.md b/docs/documentation/getting-started/upgrade.md index fd00cc97..eb37c99f 100644 --- a/docs/documentation/getting-started/upgrade.md +++ b/docs/documentation/getting-started/upgrade.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! \ No newline at end of file +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"] +``` \ No newline at end of file diff --git a/internal/app/auth/auth_ldap.go b/internal/app/auth/auth_ldap.go index 5a1b1ed8..177c9208 100644 --- a/internal/app/auth/auth_ldap.go +++ b/internal/app/auth/auth_ldap.go @@ -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 } diff --git a/internal/app/auth/auth_oauth.go b/internal/app/auth/auth_oauth.go index db80d3aa..14b3ae72 100644 --- a/internal/app/auth/auth_oauth.go +++ b/internal/app/auth/auth_oauth.go @@ -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) } - 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, + if p.userInfoLogging { + logrus.Tracef("User info from OAuth source %s: %v", p.name, string(contents)) } - return userInfo, nil + return userFields, 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 +func (p PlainOauthAuthenticator) ParseUserInfo(raw map[string]interface{}) (*domain.AuthenticatorUserInfo, error) { + return parseOauthUserInfo(p.userInfoMapping, p.userAdminMapping, raw) } diff --git a/internal/app/auth/auth_oidc.go b/internal/app/auth/auth_oidc.go index 60ebbd77..974bdf7f 100644 --- a/internal/app/auth/auth_oidc.go +++ b/internal/app/auth/auth_oidc.go @@ -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) } diff --git a/internal/app/auth/oauth_common.go b/internal/app/auth/oauth_common.go new file mode 100644 index 00000000..e0b19c9d --- /dev/null +++ b/internal/app/auth/oauth_common.go @@ -0,0 +1,88 @@ +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", + } + 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 +} diff --git a/internal/config/auth.go b/internal/config/auth.go index 42697255..9a61847c 100644 --- a/internal/config/auth.go +++ b/internal/config/auth.go @@ -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 { @@ -58,6 +120,9 @@ type LdapProvider struct { // 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 +146,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 +180,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"` } diff --git a/internal/util.go b/internal/util.go index ae1e2017..e096abf3 100644 --- a/internal/util.go +++ b/internal/util.go @@ -79,6 +79,27 @@ 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 []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{}) diff --git a/mkdocs.yml b/mkdocs.yml index 83160d6c..a128ddba 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -21,6 +21,7 @@ theme: features: - navigation.instant - navigation.tabs + - navigation.expand plugins: - search @@ -61,4 +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