chore: get rid of static code warnings

This commit is contained in:
Christoph Haas 2025-02-28 16:11:55 +01:00
parent e24acfa57d
commit fdb436b135
34 changed files with 261 additions and 117 deletions

View File

@ -5,7 +5,7 @@ labels: bug
--- ---
<!-- Tip: you can use code blocks <!-- Tip: you can use code blocks
for better better formatting of yaml config or logs for better formatting of yaml config or logs
```yaml ```yaml
# config.yaml # config.yaml

View File

@ -4,17 +4,17 @@ If you believe you've found a security issue in one of the supported versions of
## Supported Versions ## Supported Versions
| Version | Supported | | Version | Supported |
| ------- | -------------------- | |---------|--------------------|
| v2.x | :white_check_mark: | | v2.x | :white_check_mark: |
| v1.x | :white_check_mark: | | v1.x | :white_check_mark: |
## Reporting a Vulnerability ## Reporting a Vulnerability
Please do not report security vulnerabilities through public GitHub issues. Please do not report security vulnerabilities through public GitHub issues.
Instead, we encourage you to submit a report through Github [private vulnerability reporting](https://github.com/h44z/wg-portal/security). Instead, we encourage you to submit a report through GitHub [private vulnerability reporting](https://github.com/h44z/wg-portal/security).
If you prefer to submit a report without logging in to Github, please email *info (at) wgportal.org*. If you prefer to submit a report without logging in to GitHub, please email *info (at) wgportal.org*.
We will respond as soon as possible, but as only two people currently maintain this project, we cannot guarantee specific response times. We will respond as soon as possible, but as only two people currently maintain this project, we cannot guarantee specific response times.
We prefer all communications to be in English. We prefer all communications to be in English.

View File

@ -57,14 +57,14 @@ func main() {
cfgFileSystem, err := adapters.NewFileSystemRepository(cfg.Advanced.ConfigStoragePath) cfgFileSystem, err := adapters.NewFileSystemRepository(cfg.Advanced.ConfigStoragePath)
internal.AssertNoError(err) internal.AssertNoError(err)
shouldExit, err := app.HandleProgramArgs(cfg, rawDb) shouldExit, err := app.HandleProgramArgs(rawDb)
switch { switch {
case shouldExit && err == nil: case shouldExit && err == nil:
return return
case shouldExit && err != nil: case shouldExit:
logrus.Errorf("Failed to process program args: %v", err) logrus.Errorf("Failed to process program args: %v", err)
os.Exit(1) os.Exit(1)
case !shouldExit: default:
internal.AssertNoError(err) internal.AssertNoError(err)
} }

View File

@ -356,7 +356,7 @@ Below are the properties for each OIDC provider entry inside `auth.oidc`:
- Available fields: `user_identifier`, `email`, `firstname`, `lastname`, `phone`, `department`, `is_admin`, `user_groups`. - Available fields: `user_identifier`, `email`, `firstname`, `lastname`, `phone`, `department`, `is_admin`, `user_groups`.
| **Field** | **Typical OIDC Claim** | **Explanation** | | **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 its guaranteed to be unique for the user within the IdP. Some providers also support `preferred_username` if its unique. | | `user_identifier` | `sub` or `preferred_username` | A unique identifier for the user. Often the OIDC `sub` claim is used because its guaranteed to be unique for the user within the IdP. Some providers also support `preferred_username` if its unique. |
| `email` | `email` | The users email address as provided by the IdP. Not always verified, depending on IdP settings. | | `email` | `email` | The users email address as provided by the IdP. Not always verified, depending on IdP settings. |
| `firstname` | `given_name` | The users first name, typically provided by the IdP in the `given_name` claim. | | `firstname` | `given_name` | The users first name, typically provided by the IdP in the `given_name` claim. |
@ -425,7 +425,7 @@ Below are the properties for each OAuth provider entry inside `auth.oauth`:
- Available fields: `user_identifier`, `email`, `firstname`, `lastname`, `phone`, `department`, `is_admin`, `user_groups`. - Available fields: `user_identifier`, `email`, `firstname`, `lastname`, `phone`, `department`, `is_admin`, `user_groups`.
| **Field** | **Typical Claim** | **Explanation** | | **Field** | **Typical Claim** | **Explanation** |
| ----------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | |-------------------|-----------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `user_identifier` | `sub` or `preferred_username` | A unique identifier for the user. Often the OIDC `sub` claim is used because its guaranteed to be unique for the user within the IdP. Some providers also support `preferred_username` if its unique. | | `user_identifier` | `sub` or `preferred_username` | A unique identifier for the user. Often the OIDC `sub` claim is used because its guaranteed to be unique for the user within the IdP. Some providers also support `preferred_username` if its unique. |
| `email` | `email` | The users email address as provided by the IdP. Not always verified, depending on IdP settings. | | `email` | `email` | The users email address as provided by the IdP. Not always verified, depending on IdP settings. |
| `firstname` | `given_name` | The users first name, typically provided by the IdP in the `given_name` claim. | | `firstname` | `given_name` | The users first name, typically provided by the IdP in the `given_name` claim. |
@ -494,7 +494,7 @@ Below are the properties for each LDAP provider entry inside `auth.ldap`:
- Available fields: `user_identifier`, `email`, `firstname`, `lastname`, `phone`, `department`, `memberof`. - Available fields: `user_identifier`, `email`, `firstname`, `lastname`, `phone`, `department`, `memberof`.
| **WireGuard Portal Field** | **Typical LDAP Attribute** | **Short Description** | | **WireGuard Portal Field** | **Typical LDAP Attribute** | **Short Description** |
| -------------------------- | -------------------------- | ------------------------------------------------------------ | |----------------------------|----------------------------|--------------------------------------------------------------|
| user_identifier | sAMAccountName / uid | Uniquely identifies the user within the LDAP directory. | | user_identifier | sAMAccountName / uid | Uniquely identifies the user within the LDAP directory. |
| email | mail / userPrincipalName | Stores the user's primary email address. | | email | mail / userPrincipalName | Stores the user's primary email address. |
| firstname | givenName | Contains the user's first (given) name. | | firstname | givenName | Contains the user's first (given) name. |

View File

@ -1,4 +1,4 @@
By default WG-Portal exposes Prometheus metrics on port `8787` if interface/peer statistic data collection is enabled. By default, WG-Portal exposes Prometheus metrics on port `8787` if interface/peer statistic data collection is enabled.
## Exposed Metrics ## Exposed Metrics

View File

@ -4,6 +4,7 @@ import { computed, getCurrentInstance, onMounted, ref } from "vue";
import { authStore } from "./stores/auth"; import { authStore } from "./stores/auth";
import { securityStore } from "./stores/security"; import { securityStore } from "./stores/security";
import { settingsStore } from "@/stores/settings"; import { settingsStore } from "@/stores/settings";
import { Notifications } from "@kyvg/vue3-notification";
const appGlobal = getCurrentInstance().appContext.config.globalProperties const appGlobal = getCurrentInstance().appContext.config.globalProperties
const auth = authStore() const auth = authStore()

View File

@ -111,6 +111,7 @@ func (l *GormLogger) Trace(ctx context.Context, begin time.Time, fc func() (stri
} }
} }
// NewDatabase creates a new database connection and returns a Gorm database instance.
func NewDatabase(cfg config.DatabaseConfig) (*gorm.DB, error) { func NewDatabase(cfg config.DatabaseConfig) (*gorm.DB, error) {
var gormDb *gorm.DB var gormDb *gorm.DB
var err error var err error
@ -172,6 +173,7 @@ type SqlRepo struct {
db *gorm.DB db *gorm.DB
} }
// NewSqlRepository creates a new SqlRepo instance.
func NewSqlRepository(db *gorm.DB) (*SqlRepo, error) { func NewSqlRepository(db *gorm.DB) (*SqlRepo, error) {
repo := &SqlRepo{ repo := &SqlRepo{
db: db, db: db,
@ -236,6 +238,8 @@ func (r *SqlRepo) migrate() error {
// region interfaces // region interfaces
// GetInterface returns the interface with the given id.
// If no interface is found, an error domain.ErrNotFound is returned.
func (r *SqlRepo) GetInterface(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, error) { func (r *SqlRepo) GetInterface(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, error) {
var in domain.Interface var in domain.Interface
@ -251,6 +255,8 @@ func (r *SqlRepo) GetInterface(ctx context.Context, id domain.InterfaceIdentifie
return &in, nil return &in, nil
} }
// GetInterfaceAndPeers returns the interface with the given id and all peers associated with it.
// If no interface is found, an error domain.ErrNotFound is returned.
func (r *SqlRepo) GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceIdentifier) ( func (r *SqlRepo) GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceIdentifier) (
*domain.Interface, *domain.Interface,
[]domain.Peer, []domain.Peer,
@ -269,6 +275,7 @@ func (r *SqlRepo) GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceI
return in, peers, nil return in, peers, nil
} }
// GetPeersStats returns the stats for the given peer ids. The order of the returned stats is not guaranteed.
func (r *SqlRepo) GetPeersStats(ctx context.Context, ids ...domain.PeerIdentifier) ([]domain.PeerStatus, error) { func (r *SqlRepo) GetPeersStats(ctx context.Context, ids ...domain.PeerIdentifier) ([]domain.PeerStatus, error) {
if len(ids) == 0 { if len(ids) == 0 {
return nil, nil return nil, nil
@ -284,6 +291,7 @@ func (r *SqlRepo) GetPeersStats(ctx context.Context, ids ...domain.PeerIdentifie
return stats, nil return stats, nil
} }
// GetAllInterfaces returns all interfaces.
func (r *SqlRepo) GetAllInterfaces(ctx context.Context) ([]domain.Interface, error) { func (r *SqlRepo) GetAllInterfaces(ctx context.Context) ([]domain.Interface, error) {
var interfaces []domain.Interface var interfaces []domain.Interface
@ -295,6 +303,8 @@ func (r *SqlRepo) GetAllInterfaces(ctx context.Context) ([]domain.Interface, err
return interfaces, nil return interfaces, nil
} }
// GetInterfaceStats returns the stats for the given interface id.
// If no stats are found, an error domain.ErrNotFound is returned.
func (r *SqlRepo) GetInterfaceStats(ctx context.Context, id domain.InterfaceIdentifier) ( func (r *SqlRepo) GetInterfaceStats(ctx context.Context, id domain.InterfaceIdentifier) (
*domain.InterfaceStatus, *domain.InterfaceStatus,
error, error,
@ -319,6 +329,8 @@ func (r *SqlRepo) GetInterfaceStats(ctx context.Context, id domain.InterfaceIden
return &stat, nil return &stat, nil
} }
// FindInterfaces returns all interfaces that match the given search string.
// The search string is matched against the interface identifier and display name.
func (r *SqlRepo) FindInterfaces(ctx context.Context, search string) ([]domain.Interface, error) { func (r *SqlRepo) FindInterfaces(ctx context.Context, search string) ([]domain.Interface, error) {
var users []domain.Interface var users []domain.Interface
@ -335,6 +347,7 @@ func (r *SqlRepo) FindInterfaces(ctx context.Context, search string) ([]domain.I
return users, nil return users, nil
} }
// SaveInterface updates the interface with the given id.
func (r *SqlRepo) SaveInterface( func (r *SqlRepo) SaveInterface(
ctx context.Context, ctx context.Context,
id domain.InterfaceIdentifier, id domain.InterfaceIdentifier,
@ -410,6 +423,7 @@ func (r *SqlRepo) upsertInterface(ui *domain.ContextUserInfo, tx *gorm.DB, in *d
return nil return nil
} }
// DeleteInterface deletes the interface with the given id.
func (r *SqlRepo) DeleteInterface(ctx context.Context, id domain.InterfaceIdentifier) error { func (r *SqlRepo) DeleteInterface(ctx context.Context, id domain.InterfaceIdentifier) error {
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
err := tx.Where("interface_identifier = ?", id).Delete(&domain.Peer{}).Error err := tx.Where("interface_identifier = ?", id).Delete(&domain.Peer{}).Error
@ -436,6 +450,7 @@ func (r *SqlRepo) DeleteInterface(ctx context.Context, id domain.InterfaceIdenti
return nil return nil
} }
// GetInterfaceIps returns a map of interface identifiers to their respective IP addresses.
func (r *SqlRepo) GetInterfaceIps(ctx context.Context) (map[domain.InterfaceIdentifier][]domain.Cidr, error) { func (r *SqlRepo) GetInterfaceIps(ctx context.Context) (map[domain.InterfaceIdentifier][]domain.Cidr, error) {
var ips []struct { var ips []struct {
domain.Cidr domain.Cidr
@ -461,6 +476,8 @@ func (r *SqlRepo) GetInterfaceIps(ctx context.Context) (map[domain.InterfaceIden
// region peers // region peers
// GetPeer returns the peer with the given id.
// If no peer is found, an error domain.ErrNotFound is returned.
func (r *SqlRepo) GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error) { func (r *SqlRepo) GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error) {
var peer domain.Peer var peer domain.Peer
@ -476,6 +493,7 @@ func (r *SqlRepo) GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domai
return &peer, nil return &peer, nil
} }
// GetInterfacePeers returns all peers associated with the given interface id.
func (r *SqlRepo) GetInterfacePeers(ctx context.Context, id domain.InterfaceIdentifier) ([]domain.Peer, error) { func (r *SqlRepo) GetInterfacePeers(ctx context.Context, id domain.InterfaceIdentifier) ([]domain.Peer, error) {
var peers []domain.Peer var peers []domain.Peer
@ -487,6 +505,8 @@ func (r *SqlRepo) GetInterfacePeers(ctx context.Context, id domain.InterfaceIden
return peers, nil return peers, nil
} }
// FindInterfacePeers returns all peers associated with the given interface id that match the given search string.
// The search string is matched against the peer identifier, display name and IP address.
func (r *SqlRepo) FindInterfacePeers(ctx context.Context, id domain.InterfaceIdentifier, search string) ( func (r *SqlRepo) FindInterfacePeers(ctx context.Context, id domain.InterfaceIdentifier, search string) (
[]domain.Peer, []domain.Peer,
error, error,
@ -506,6 +526,7 @@ func (r *SqlRepo) FindInterfacePeers(ctx context.Context, id domain.InterfaceIde
return peers, nil return peers, nil
} }
// GetUserPeers returns all peers associated with the given user id.
func (r *SqlRepo) GetUserPeers(ctx context.Context, id domain.UserIdentifier) ([]domain.Peer, error) { func (r *SqlRepo) GetUserPeers(ctx context.Context, id domain.UserIdentifier) ([]domain.Peer, error) {
var peers []domain.Peer var peers []domain.Peer
@ -517,6 +538,8 @@ func (r *SqlRepo) GetUserPeers(ctx context.Context, id domain.UserIdentifier) ([
return peers, nil return peers, nil
} }
// FindUserPeers returns all peers associated with the given user id that match the given search string.
// The search string is matched against the peer identifier, display name and IP address.
func (r *SqlRepo) FindUserPeers(ctx context.Context, id domain.UserIdentifier, search string) ([]domain.Peer, error) { func (r *SqlRepo) FindUserPeers(ctx context.Context, id domain.UserIdentifier, search string) ([]domain.Peer, error) {
var peers []domain.Peer var peers []domain.Peer
@ -533,6 +556,8 @@ func (r *SqlRepo) FindUserPeers(ctx context.Context, id domain.UserIdentifier, s
return peers, nil return peers, nil
} }
// SavePeer updates the peer with the given id.
// If no existing peer is found, a new peer is created.
func (r *SqlRepo) SavePeer( func (r *SqlRepo) SavePeer(
ctx context.Context, ctx context.Context,
id domain.PeerIdentifier, id domain.PeerIdentifier,
@ -607,6 +632,7 @@ func (r *SqlRepo) upsertPeer(ui *domain.ContextUserInfo, tx *gorm.DB, peer *doma
return nil return nil
} }
// DeletePeer deletes the peer with the given id.
func (r *SqlRepo) DeletePeer(ctx context.Context, id domain.PeerIdentifier) error { func (r *SqlRepo) DeletePeer(ctx context.Context, id domain.PeerIdentifier) error {
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
err := tx.Delete(&domain.PeerStatus{PeerId: id}).Error err := tx.Delete(&domain.PeerStatus{PeerId: id}).Error
@ -628,6 +654,7 @@ func (r *SqlRepo) DeletePeer(ctx context.Context, id domain.PeerIdentifier) erro
return nil return nil
} }
// GetPeerIps returns a map of peer identifiers to their respective IP addresses.
func (r *SqlRepo) GetPeerIps(ctx context.Context) (map[domain.PeerIdentifier][]domain.Cidr, error) { func (r *SqlRepo) GetPeerIps(ctx context.Context) (map[domain.PeerIdentifier][]domain.Cidr, error) {
var ips []struct { var ips []struct {
domain.Cidr domain.Cidr
@ -649,6 +676,7 @@ func (r *SqlRepo) GetPeerIps(ctx context.Context) (map[domain.PeerIdentifier][]d
return result, nil return result, nil
} }
// GetUsedIpsPerSubnet returns a map of subnets to their respective used IP addresses.
func (r *SqlRepo) GetUsedIpsPerSubnet(ctx context.Context, subnets []domain.Cidr) ( func (r *SqlRepo) GetUsedIpsPerSubnet(ctx context.Context, subnets []domain.Cidr) (
map[domain.Cidr][]domain.Cidr, map[domain.Cidr][]domain.Cidr,
error, error,
@ -707,6 +735,8 @@ func (r *SqlRepo) GetUsedIpsPerSubnet(ctx context.Context, subnets []domain.Cidr
// region users // region users
// GetUser returns the user with the given id.
// If no user is found, an error domain.ErrNotFound is returned.
func (r *SqlRepo) GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error) { func (r *SqlRepo) GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error) {
var user domain.User var user domain.User
@ -722,6 +752,9 @@ func (r *SqlRepo) GetUser(ctx context.Context, id domain.UserIdentifier) (*domai
return &user, nil return &user, nil
} }
// GetUserByEmail returns the user with the given email.
// If no user is found, an error domain.ErrNotFound is returned.
// If multiple users are found, an error domain.ErrNotUnique is returned.
func (r *SqlRepo) GetUserByEmail(ctx context.Context, email string) (*domain.User, error) { func (r *SqlRepo) GetUserByEmail(ctx context.Context, email string) (*domain.User, error) {
var users []domain.User var users []domain.User
@ -746,6 +779,7 @@ func (r *SqlRepo) GetUserByEmail(ctx context.Context, email string) (*domain.Use
return &user, nil return &user, nil
} }
// GetAllUsers returns all users.
func (r *SqlRepo) GetAllUsers(ctx context.Context) ([]domain.User, error) { func (r *SqlRepo) GetAllUsers(ctx context.Context) ([]domain.User, error) {
var users []domain.User var users []domain.User
@ -757,6 +791,8 @@ func (r *SqlRepo) GetAllUsers(ctx context.Context) ([]domain.User, error) {
return users, nil return users, nil
} }
// FindUsers returns all users that match the given search string.
// The search string is matched against the user identifier, firstname, lastname and email.
func (r *SqlRepo) FindUsers(ctx context.Context, search string) ([]domain.User, error) { func (r *SqlRepo) FindUsers(ctx context.Context, search string) ([]domain.User, error) {
var users []domain.User var users []domain.User
@ -774,6 +810,8 @@ func (r *SqlRepo) FindUsers(ctx context.Context, search string) ([]domain.User,
return users, nil return users, nil
} }
// SaveUser updates the user with the given id.
// If no user is found, a new user is created.
func (r *SqlRepo) SaveUser( func (r *SqlRepo) SaveUser(
ctx context.Context, ctx context.Context,
id domain.UserIdentifier, id domain.UserIdentifier,
@ -807,6 +845,7 @@ func (r *SqlRepo) SaveUser(
return nil return nil
} }
// DeleteUser deletes the user with the given id.
func (r *SqlRepo) DeleteUser(ctx context.Context, id domain.UserIdentifier) error { func (r *SqlRepo) DeleteUser(ctx context.Context, id domain.UserIdentifier) error {
err := r.db.WithContext(ctx).Delete(&domain.User{}, id).Error err := r.db.WithContext(ctx).Delete(&domain.User{}, id).Error
if err != nil { if err != nil {
@ -859,6 +898,8 @@ func (r *SqlRepo) upsertUser(ui *domain.ContextUserInfo, tx *gorm.DB, user *doma
// region statistics // region statistics
// UpdateInterfaceStatus updates the interface status with the given id.
// If no interface status is found, a new one is created.
func (r *SqlRepo) UpdateInterfaceStatus( func (r *SqlRepo) UpdateInterfaceStatus(
ctx context.Context, ctx context.Context,
id domain.InterfaceIdentifier, id domain.InterfaceIdentifier,
@ -919,6 +960,8 @@ func (r *SqlRepo) upsertInterfaceStatus(tx *gorm.DB, in *domain.InterfaceStatus)
return nil return nil
} }
// UpdatePeerStatus updates the peer status with the given id.
// If no peer status is found, a new one is created.
func (r *SqlRepo) UpdatePeerStatus( func (r *SqlRepo) UpdatePeerStatus(
ctx context.Context, ctx context.Context,
id domain.PeerIdentifier, id domain.PeerIdentifier,
@ -976,6 +1019,7 @@ func (r *SqlRepo) upsertPeerStatus(tx *gorm.DB, in *domain.PeerStatus) error {
return nil return nil
} }
// DeletePeerStatus deletes the peer status with the given id.
func (r *SqlRepo) DeletePeerStatus(ctx context.Context, id domain.PeerIdentifier) error { func (r *SqlRepo) DeletePeerStatus(ctx context.Context, id domain.PeerIdentifier) error {
err := r.db.WithContext(ctx).Delete(&domain.PeerStatus{}, id).Error err := r.db.WithContext(ctx).Delete(&domain.PeerStatus{}, id).Error
if err != nil { if err != nil {
@ -989,6 +1033,7 @@ func (r *SqlRepo) DeletePeerStatus(ctx context.Context, id domain.PeerIdentifier
// region audit // region audit
// SaveAuditEntry saves the given audit entry.
func (r *SqlRepo) SaveAuditEntry(ctx context.Context, entry *domain.AuditEntry) error { func (r *SqlRepo) SaveAuditEntry(ctx context.Context, entry *domain.AuditEntry) error {
err := r.db.WithContext(ctx).Save(entry).Error err := r.db.WithContext(ctx).Save(entry).Error
if err != nil { if err != nil {

View File

@ -13,6 +13,7 @@ type FilesystemRepo struct {
basePath string basePath string
} }
// NewFileSystemRepository creates a new FilesystemRepo instance.
func NewFileSystemRepository(basePath string) (*FilesystemRepo, error) { func NewFileSystemRepository(basePath string) (*FilesystemRepo, error) {
if basePath == "" { if basePath == "" {
return nil, nil // no path, return empty repository return nil, nil // no path, return empty repository
@ -27,6 +28,10 @@ func NewFileSystemRepository(basePath string) (*FilesystemRepo, error) {
return r, nil return r, nil
} }
// WriteFile writes the given contents to the given path.
// The path is relative to the base path of the repository.
// If the parent directory does not exist, it is created.
// If the file already exists, it is overwritten.
func (r *FilesystemRepo) WriteFile(path string, contents io.Reader) error { func (r *FilesystemRepo) WriteFile(path string, contents io.Reader) error {
filePath := filepath.Join(r.basePath, path) filePath := filepath.Join(r.basePath, path)
parentDirectory := filepath.Dir(filePath) parentDirectory := filepath.Dir(filePath)
@ -51,5 +56,4 @@ func (r *FilesystemRepo) WriteFile(path string, contents io.Reader) error {
} }
return nil return nil
} }

View File

@ -19,11 +19,12 @@ type MailRepo struct {
cfg *config.MailConfig cfg *config.MailConfig
} }
// NewSmtpMailRepo creates a new MailRepo instance.
func NewSmtpMailRepo(cfg config.MailConfig) MailRepo { func NewSmtpMailRepo(cfg config.MailConfig) MailRepo {
return MailRepo{cfg: &cfg} return MailRepo{cfg: &cfg}
} }
// Send sends a mail. // Send sends a mail using SMTP.
func (r MailRepo) Send(_ context.Context, subject, body string, to []string, options *domain.MailOptions) error { func (r MailRepo) Send(_ context.Context, subject, body string, to []string, options *domain.MailOptions) error {
if options == nil { if options == nil {
options = &domain.MailOptions{} options = &domain.MailOptions{}

View File

@ -86,7 +86,7 @@ func NewMetricsServer(cfg *config.Config) *MetricsServer {
} }
} }
// Run starts the metrics server // Run starts the metrics server. The function blocks until the context is cancelled.
func (m *MetricsServer) Run(ctx context.Context) { func (m *MetricsServer) Run(ctx context.Context) {
// Run the metrics server in a goroutine // Run the metrics server in a goroutine
go func() { go func() {
@ -104,7 +104,7 @@ func (m *MetricsServer) Run(ctx context.Context) {
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() defer cancel()
// Attempt to gracefully shutdown the metrics server // Attempt to gracefully shut down the metrics server
if err := m.Shutdown(shutdownCtx); err != nil { if err := m.Shutdown(shutdownCtx); err != nil {
logrus.Errorf("metrics service on %s shutdown failed: %v", m.Addr, err) logrus.Errorf("metrics service on %s shutdown failed: %v", m.Addr, err)
} else { } else {
@ -123,9 +123,9 @@ func (m *MetricsServer) UpdateInterfaceMetrics(status domain.InterfaceStatus) {
func (m *MetricsServer) UpdatePeerMetrics(peer *domain.Peer, status domain.PeerStatus) { func (m *MetricsServer) UpdatePeerMetrics(peer *domain.Peer, status domain.PeerStatus) {
labels := []string{ labels := []string{
string(peer.InterfaceIdentifier), string(peer.InterfaceIdentifier),
string(peer.Interface.AddressStr()), peer.Interface.AddressStr(),
string(status.PeerId), string(status.PeerId),
string(peer.DisplayName), peer.DisplayName,
} }
if status.LastHandshake != nil { if status.LastHandshake != nil {

View File

@ -18,6 +18,7 @@ type WgQuickRepo struct {
resolvConfIfacePrefix string resolvConfIfacePrefix string
} }
// NewWgQuickRepo creates a new WgQuickRepo instance.
func NewWgQuickRepo() *WgQuickRepo { func NewWgQuickRepo() *WgQuickRepo {
return &WgQuickRepo{ return &WgQuickRepo{
shellCmd: "bash", shellCmd: "bash",
@ -25,6 +26,10 @@ func NewWgQuickRepo() *WgQuickRepo {
} }
} }
// ExecuteInterfaceHook executes the given hook command.
// The hook command can contain the following placeholders:
//
// %i: the interface identifier.
func (r *WgQuickRepo) ExecuteInterfaceHook(id domain.InterfaceIdentifier, hookCmd string) error { func (r *WgQuickRepo) ExecuteInterfaceHook(id domain.InterfaceIdentifier, hookCmd string) error {
if hookCmd == "" { if hookCmd == "" {
return nil return nil
@ -39,6 +44,7 @@ func (r *WgQuickRepo) ExecuteInterfaceHook(id domain.InterfaceIdentifier, hookCm
return nil return nil
} }
// SetDNS sets the DNS settings for the given interface. It uses resolvconf to set the DNS settings.
func (r *WgQuickRepo) SetDNS(id domain.InterfaceIdentifier, dnsStr, dnsSearchStr string) error { func (r *WgQuickRepo) SetDNS(id domain.InterfaceIdentifier, dnsStr, dnsSearchStr string) error {
if dnsStr == "" && dnsSearchStr == "" { if dnsStr == "" && dnsSearchStr == "" {
return nil return nil
@ -68,6 +74,7 @@ func (r *WgQuickRepo) SetDNS(id domain.InterfaceIdentifier, dnsStr, dnsSearchStr
return nil return nil
} }
// UnsetDNS unsets the DNS settings for the given interface. It uses resolvconf to unset the DNS settings.
func (r *WgQuickRepo) UnsetDNS(id domain.InterfaceIdentifier) error { func (r *WgQuickRepo) UnsetDNS(id domain.InterfaceIdentifier) error {
dnsCommand := "resolvconf -d %resPref%i -f" dnsCommand := "resolvconf -d %resPref%i -f"

View File

@ -20,6 +20,8 @@ type WgRepo struct {
nl lowlevel.NetlinkClient nl lowlevel.NetlinkClient
} }
// NewWireGuardRepository creates a new WgRepo instance.
// This repository is used to interact with the WireGuard kernel or userspace module.
func NewWireGuardRepository() *WgRepo { func NewWireGuardRepository() *WgRepo {
wg, err := wgctrl.New() wg, err := wgctrl.New()
if err != nil { if err != nil {
@ -36,6 +38,7 @@ func NewWireGuardRepository() *WgRepo {
return repo return repo
} }
// GetInterfaces returns all existing WireGuard interfaces.
func (r *WgRepo) GetInterfaces(_ context.Context) ([]domain.PhysicalInterface, error) { func (r *WgRepo) GetInterfaces(_ context.Context) ([]domain.PhysicalInterface, error) {
devices, err := r.wg.Devices() devices, err := r.wg.Devices()
if err != nil { if err != nil {
@ -54,10 +57,14 @@ func (r *WgRepo) GetInterfaces(_ context.Context) ([]domain.PhysicalInterface, e
return interfaces, nil return interfaces, nil
} }
// GetInterface returns the interface with the given id.
// If no interface is found, an error os.ErrNotExist is returned.
func (r *WgRepo) GetInterface(_ context.Context, id domain.InterfaceIdentifier) (*domain.PhysicalInterface, error) { func (r *WgRepo) GetInterface(_ context.Context, id domain.InterfaceIdentifier) (*domain.PhysicalInterface, error) {
return r.getInterface(id) return r.getInterface(id)
} }
// GetPeers returns all peers associated with the given interface id.
// If the requested interface is found, an error os.ErrNotExist is returned.
func (r *WgRepo) GetPeers(_ context.Context, deviceId domain.InterfaceIdentifier) ([]domain.PhysicalPeer, error) { func (r *WgRepo) GetPeers(_ context.Context, deviceId domain.InterfaceIdentifier) ([]domain.PhysicalPeer, error) {
device, err := r.wg.Device(string(deviceId)) device, err := r.wg.Device(string(deviceId))
if err != nil { if err != nil {
@ -76,6 +83,8 @@ func (r *WgRepo) GetPeers(_ context.Context, deviceId domain.InterfaceIdentifier
return peers, nil return peers, nil
} }
// GetPeer returns the peer with the given id.
// If the requested interface or peer is found, an error os.ErrNotExist is returned.
func (r *WgRepo) GetPeer( func (r *WgRepo) GetPeer(
_ context.Context, _ context.Context,
deviceId domain.InterfaceIdentifier, deviceId domain.InterfaceIdentifier,
@ -157,6 +166,9 @@ func (r *WgRepo) convertWireGuardPeer(peer *wgtypes.Peer) (domain.PhysicalPeer,
return peerModel, nil return peerModel, nil
} }
// SaveInterface updates the interface with the given id.
// If no existing interface is found, a new interface is created.
// Updating the interface does not interrupt any existing connections.
func (r *WgRepo) SaveInterface( func (r *WgRepo) SaveInterface(
_ context.Context, _ context.Context,
id domain.InterfaceIdentifier, id domain.InterfaceIdentifier,
@ -187,10 +199,10 @@ func (r *WgRepo) SaveInterface(
func (r *WgRepo) getOrCreateInterface(id domain.InterfaceIdentifier) (*domain.PhysicalInterface, error) { func (r *WgRepo) getOrCreateInterface(id domain.InterfaceIdentifier) (*domain.PhysicalInterface, error) {
device, err := r.getInterface(id) device, err := r.getInterface(id)
if err == nil { if err == nil {
return device, nil return device, nil // interface exists
} }
if err != nil && !errors.Is(err, os.ErrNotExist) { if !errors.Is(err, os.ErrNotExist) {
return nil, fmt.Errorf("device error: %w", err) return nil, fmt.Errorf("device error: %w", err) // unknown error
} }
// create new device // create new device
@ -308,6 +320,8 @@ func (r *WgRepo) updateWireGuardInterface(pi *domain.PhysicalInterface) error {
return nil return nil
} }
// DeleteInterface deletes the interface with the given id.
// If the requested interface is found, no error is returned.
func (r *WgRepo) DeleteInterface(_ context.Context, id domain.InterfaceIdentifier) error { func (r *WgRepo) DeleteInterface(_ context.Context, id domain.InterfaceIdentifier) error {
if err := r.deleteLowLevelInterface(id); err != nil { if err := r.deleteLowLevelInterface(id); err != nil {
return err return err
@ -334,6 +348,8 @@ func (r *WgRepo) deleteLowLevelInterface(id domain.InterfaceIdentifier) error {
return nil return nil
} }
// SavePeer updates the peer with the given id.
// If no existing peer is found, a new peer is created.
func (r *WgRepo) SavePeer( func (r *WgRepo) SavePeer(
_ context.Context, _ context.Context,
deviceId domain.InterfaceIdentifier, deviceId domain.InterfaceIdentifier,
@ -363,10 +379,10 @@ func (r *WgRepo) getOrCreatePeer(deviceId domain.InterfaceIdentifier, id domain.
) { ) {
peer, err := r.getPeer(deviceId, id) peer, err := r.getPeer(deviceId, id)
if err == nil { if err == nil {
return peer, nil return peer, nil // peer exists
} }
if err != nil && !errors.Is(err, os.ErrNotExist) { if !errors.Is(err, os.ErrNotExist) {
return nil, fmt.Errorf("peer error: %w", err) return nil, fmt.Errorf("peer error: %w", err) // unknown error
} }
// create new peer // create new peer
@ -425,6 +441,8 @@ func (r *WgRepo) updatePeer(deviceId domain.InterfaceIdentifier, pp *domain.Phys
return nil return nil
} }
// DeletePeer deletes the peer with the given id.
// If the requested interface or peer is found, no error is returned.
func (r *WgRepo) DeletePeer(_ context.Context, deviceId domain.InterfaceIdentifier, id domain.PeerIdentifier) error { func (r *WgRepo) DeletePeer(_ context.Context, deviceId domain.InterfaceIdentifier, id domain.PeerIdentifier) error {
if !id.IsPublicKey() { if !id.IsPublicKey() {
return errors.New("invalid public key") return errors.New("invalid public key")

View File

@ -15,6 +15,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/h44z/wg-portal/internal"
"github.com/h44z/wg-portal/internal/domain" "github.com/h44z/wg-portal/internal/domain"
) )
@ -42,12 +43,12 @@ func Test_wgRepository_GetInterfaces(t *testing.T) {
mgr := setup(t) mgr := setup(t)
interfaceName := domain.InterfaceIdentifier("wg_test_001") interfaceName := domain.InterfaceIdentifier("wg_test_001")
defer mgr.DeleteInterface(context.Background(), interfaceName) defer internal.LogError(mgr.DeleteInterface(context.Background(), interfaceName))
err := mgr.SaveInterface(context.Background(), interfaceName, nil) err := mgr.SaveInterface(context.Background(), interfaceName, nil)
require.NoError(t, err) require.NoError(t, err)
interfaceName2 := domain.InterfaceIdentifier("wg_test_002") interfaceName2 := domain.InterfaceIdentifier("wg_test_002")
defer mgr.DeleteInterface(context.Background(), interfaceName2) defer internal.LogError(mgr.DeleteInterface(context.Background(), interfaceName2))
err = mgr.SaveInterface(context.Background(), interfaceName2, nil) err = mgr.SaveInterface(context.Background(), interfaceName2, nil)
require.NoError(t, err) require.NoError(t, err)
@ -65,7 +66,7 @@ func TestWireGuardCreateInterface(t *testing.T) {
interfaceName := domain.InterfaceIdentifier("wg_test_001") interfaceName := domain.InterfaceIdentifier("wg_test_001")
ipAddress := "10.11.12.13" ipAddress := "10.11.12.13"
ipV6Address := "1337:d34d:b33f::2" ipV6Address := "1337:d34d:b33f::2"
defer mgr.DeleteInterface(context.Background(), interfaceName) defer internal.LogError(mgr.DeleteInterface(context.Background(), interfaceName))
err := mgr.SaveInterface(context.Background(), interfaceName, err := mgr.SaveInterface(context.Background(), interfaceName,
func(pi *domain.PhysicalInterface) (*domain.PhysicalInterface, error) { func(pi *domain.PhysicalInterface) (*domain.PhysicalInterface, error) {
@ -90,7 +91,7 @@ func TestWireGuardUpdateInterface(t *testing.T) {
mgr := setup(t) mgr := setup(t)
interfaceName := domain.InterfaceIdentifier("wg_test_001") interfaceName := domain.InterfaceIdentifier("wg_test_001")
defer mgr.DeleteInterface(context.Background(), interfaceName) defer internal.LogError(mgr.DeleteInterface(context.Background(), interfaceName))
err := mgr.SaveInterface(context.Background(), interfaceName, nil) err := mgr.SaveInterface(context.Background(), interfaceName, nil)
require.NoError(t, err) require.NoError(t, err)

View File

@ -40,7 +40,7 @@ func (e configEndpoint) GetName() string {
return "ConfigEndpoint" return "ConfigEndpoint"
} }
func (e configEndpoint) RegisterRoutes(g *gin.RouterGroup, authenticator *authenticationHandler) { func (e configEndpoint) RegisterRoutes(g *gin.RouterGroup, _ *authenticationHandler) {
apiGroup := g.Group("/config") apiGroup := g.Group("/config")
apiGroup.GET("/frontend.js", e.handleConfigJsGet()) apiGroup.GET("/frontend.js", e.handleConfigJsGet())

View File

@ -20,7 +20,7 @@ func (e interfaceEndpoint) GetName() string {
return "InterfaceEndpoint" return "InterfaceEndpoint"
} }
func (e interfaceEndpoint) RegisterRoutes(g *gin.RouterGroup, authenticator *authenticationHandler) { func (e interfaceEndpoint) RegisterRoutes(g *gin.RouterGroup, _ *authenticationHandler) {
apiGroup := g.Group("/interface", e.authenticator.LoggedIn(ScopeAdmin)) apiGroup := g.Group("/interface", e.authenticator.LoggedIn(ScopeAdmin))
apiGroup.GET("/prepare", e.handlePrepareGet()) apiGroup.GET("/prepare", e.handlePrepareGet())

View File

@ -20,7 +20,7 @@ func (e peerEndpoint) GetName() string {
return "PeerEndpoint" return "PeerEndpoint"
} }
func (e peerEndpoint) RegisterRoutes(g *gin.RouterGroup, authenticator *authenticationHandler) { func (e peerEndpoint) RegisterRoutes(g *gin.RouterGroup, _ *authenticationHandler) {
apiGroup := g.Group("/peer", e.authenticator.LoggedIn()) apiGroup := g.Group("/peer", e.authenticator.LoggedIn())
apiGroup.GET("/iface/:iface/all", e.authenticator.LoggedIn(ScopeAdmin), e.handleAllGet()) apiGroup.GET("/iface/:iface/all", e.authenticator.LoggedIn(ScopeAdmin), e.handleAllGet())

View File

@ -16,7 +16,7 @@ func (e testEndpoint) GetName() string {
return "TestEndpoint" return "TestEndpoint"
} }
func (e testEndpoint) RegisterRoutes(g *gin.RouterGroup, authenticator *authenticationHandler) { func (e testEndpoint) RegisterRoutes(g *gin.RouterGroup, _ *authenticationHandler) {
g.GET("/now", e.handleCurrentTimeGet()) g.GET("/now", e.handleCurrentTimeGet())
g.GET("/hostname", e.handleHostnameGet()) g.GET("/hostname", e.handleHostnameGet())
} }

View File

@ -19,7 +19,7 @@ func (e userEndpoint) GetName() string {
return "UserEndpoint" return "UserEndpoint"
} }
func (e userEndpoint) RegisterRoutes(g *gin.RouterGroup, authenticator *authenticationHandler) { func (e userEndpoint) RegisterRoutes(g *gin.RouterGroup, _ *authenticationHandler) {
apiGroup := g.Group("/user", e.authenticator.LoggedIn()) apiGroup := g.Group("/user", e.authenticator.LoggedIn())
apiGroup.GET("/all", e.authenticator.LoggedIn(ScopeAdmin), e.handleAllGet()) apiGroup.GET("/all", e.authenticator.LoggedIn(ScopeAdmin), e.handleAllGet())

View File

@ -13,9 +13,7 @@ import (
type Scope string type Scope string
const ( const (
ScopeAdmin Scope = "ADMIN" // Admin scope contains all other scopes ScopeAdmin Scope = "ADMIN" // Admin scope contains all other scopes
ScopeSwagger Scope = "SWAGGER"
ScopeUser Scope = "USER"
) )
type authenticationHandler struct { type authenticationHandler struct {

View File

@ -10,13 +10,6 @@ type ConfigOption[T any] struct {
Overridable bool `json:"Overridable"` Overridable bool `json:"Overridable"`
} }
func NewConfigOption[T any](value T, overridable bool) ConfigOption[T] {
return ConfigOption[T]{
Value: value,
Overridable: overridable,
}
}
func ConfigOptionFromDomain[T any](opt domain.ConfigOption[T]) ConfigOption[T] { func ConfigOptionFromDomain[T any](opt domain.ConfigOption[T]) ConfigOption[T] {
return ConfigOption[T]{ return ConfigOption[T]{
Value: opt.Value, Value: opt.Value,

View File

@ -10,13 +10,6 @@ type ConfigOption[T any] struct {
Overridable bool `json:"Overridable,omitempty"` Overridable bool `json:"Overridable,omitempty"`
} }
func NewConfigOption[T any](value T, overridable bool) ConfigOption[T] {
return ConfigOption[T]{
Value: value,
Overridable: overridable,
}
}
func ConfigOptionFromDomain[T any](opt domain.ConfigOption[T]) ConfigOption[T] { func ConfigOptionFromDomain[T any](opt domain.ConfigOption[T]) ConfigOption[T] {
return ConfigOption[T]{ return ConfigOption[T]{
Value: opt.Value, Value: opt.Value,

View File

@ -8,14 +8,15 @@ import (
"github.com/h44z/wg-portal/internal/config" "github.com/h44z/wg-portal/internal/config"
) )
func HandleProgramArgs(cfg *config.Config, db *gorm.DB) (exit bool, err error) { // HandleProgramArgs handles program arguments and returns true if the program should exit.
func HandleProgramArgs(db *gorm.DB) (exit bool, err error) {
migrationSource := flag.String("migrateFrom", "", "path to v1 database file or DSN") migrationSource := flag.String("migrateFrom", "", "path to v1 database file or DSN")
migrationDbType := flag.String("migrateFromType", string(config.DatabaseSQLite), migrationDbType := flag.String("migrateFromType", string(config.DatabaseSQLite),
"old database type, either mysql, mssql, postgres or sqlite") "old database type, either mysql, mssql, postgres or sqlite")
flag.Parse() flag.Parse()
if *migrationSource != "" { if *migrationSource != "" {
err = migrateFromV1(cfg, db, *migrationSource, *migrationDbType) err = migrateFromV1(db, *migrationSource, *migrationDbType)
exit = true exit = true
} }

View File

@ -14,7 +14,7 @@ import (
"github.com/h44z/wg-portal/internal/domain" "github.com/h44z/wg-portal/internal/domain"
) )
func migrateFromV1(cfg *config.Config, db *gorm.DB, source, typ string) error { func migrateFromV1(db *gorm.DB, source, typ string) error {
sourceType := config.SupportedDatabase(typ) sourceType := config.SupportedDatabase(typ)
switch sourceType { switch sourceType {
case config.DatabaseMySQL, config.DatabasePostgres, config.DatabaseMsSQL: case config.DatabaseMySQL, config.DatabasePostgres, config.DatabaseMsSQL:

View File

@ -63,7 +63,7 @@ func (m Manager) connectToMessageBus() {
_ = m.bus.Subscribe(app.TopicRouteRemove, m.handleRouteRemoveEvent) _ = m.bus.Subscribe(app.TopicRouteRemove, m.handleRouteRemoveEvent)
} }
func (m Manager) StartBackgroundJobs(ctx context.Context) { func (m Manager) StartBackgroundJobs(_ context.Context) {
} }
func (m Manager) handleRouteUpdateEvent(srcDescription string) { func (m Manager) handleRouteUpdateEvent(srcDescription string) {
@ -124,7 +124,7 @@ func (m Manager) syncRoutes(ctx context.Context) error {
return fmt.Errorf("failed to find physical link for %s: %w", iface.Identifier, err) return fmt.Errorf("failed to find physical link for %s: %w", iface.Identifier, err)
} }
table, fwmark, err := m.getRoutingTableAndFwMark(&iface, allowedIPs, link) table, fwmark, err := m.getRoutingTableAndFwMark(&iface, link)
if err != nil { if err != nil {
return fmt.Errorf("failed to get table and fwmark for %s: %w", iface.Identifier, err) return fmt.Errorf("failed to get table and fwmark for %s: %w", iface.Identifier, err)
} }
@ -426,11 +426,11 @@ func (m Manager) removeDeprecatedRoutes(link netlink.Link, family int, allowedIP
return nil return nil
} }
func (m Manager) getRoutingTableAndFwMark( func (m Manager) getRoutingTableAndFwMark(iface *domain.Interface, link netlink.Link) (
iface *domain.Interface, table int,
allowedIPs []domain.Cidr, fwmark uint32,
link netlink.Link, err error,
) (table int, fwmark uint32, err error) { ) {
table = iface.GetRoutingTable() table = iface.GetRoutingTable()
fwmark = iface.FirewallMark fwmark = iface.FirewallMark

View File

@ -71,7 +71,8 @@ func (m Manager) GetAllInterfacesAndPeers(ctx context.Context) ([]domain.Interfa
// GetUserInterfaces returns all interfaces that are available for users to create new peers. // GetUserInterfaces returns all interfaces that are available for users to create new peers.
// If self-provisioning is disabled, this function will return an empty list. // If self-provisioning is disabled, this function will return an empty list.
func (m Manager) GetUserInterfaces(ctx context.Context, id domain.UserIdentifier) ([]domain.Interface, error) { // At the moment, there are no interfaces specific to single users, thus the user id is not used.
func (m Manager) GetUserInterfaces(ctx context.Context, _ domain.UserIdentifier) ([]domain.Interface, error) {
if !m.cfg.Core.SelfProvisioningAllowed { if !m.cfg.Core.SelfProvisioningAllowed {
return nil, nil // self-provisioning is disabled - no interfaces for users return nil, nil // self-provisioning is disabled - no interfaces for users
} }
@ -837,7 +838,7 @@ func (m Manager) deleteInterfacePeers(ctx context.Context, id domain.InterfaceId
return nil return nil
} }
func (m Manager) validateInterfaceModifications(ctx context.Context, old, new *domain.Interface) error { func (m Manager) validateInterfaceModifications(ctx context.Context, _, _ *domain.Interface) error {
currentUser := domain.GetUserInfo(ctx) currentUser := domain.GetUserInfo(ctx)
if !currentUser.IsAdmin { if !currentUser.IsAdmin {
@ -847,7 +848,7 @@ func (m Manager) validateInterfaceModifications(ctx context.Context, old, new *d
return nil return nil
} }
func (m Manager) validateInterfaceCreation(ctx context.Context, old, new *domain.Interface) error { func (m Manager) validateInterfaceCreation(ctx context.Context, _, new *domain.Interface) error {
currentUser := domain.GetUserInfo(ctx) currentUser := domain.GetUserInfo(ctx)
if new.Identifier == "" { if new.Identifier == "" {
@ -868,7 +869,7 @@ func (m Manager) validateInterfaceCreation(ctx context.Context, old, new *domain
return nil return nil
} }
func (m Manager) validateInterfaceDeletion(ctx context.Context, del *domain.Interface) error { func (m Manager) validateInterfaceDeletion(ctx context.Context, _ *domain.Interface) error {
currentUser := domain.GetUserInfo(ctx) currentUser := domain.GetUserInfo(ctx)
if !currentUser.IsAdmin { if !currentUser.IsAdmin {

View File

@ -475,7 +475,7 @@ func (m Manager) getFreshPeerIpConfig(ctx context.Context, iface *domain.Interfa
return return
} }
func (m Manager) validatePeerModifications(ctx context.Context, old, new *domain.Peer) error { func (m Manager) validatePeerModifications(ctx context.Context, _, _ *domain.Peer) error {
currentUser := domain.GetUserInfo(ctx) currentUser := domain.GetUserInfo(ctx)
if !currentUser.IsAdmin && !m.cfg.Core.SelfProvisioningAllowed { if !currentUser.IsAdmin && !m.cfg.Core.SelfProvisioningAllowed {
@ -485,7 +485,7 @@ func (m Manager) validatePeerModifications(ctx context.Context, old, new *domain
return nil return nil
} }
func (m Manager) validatePeerCreation(ctx context.Context, old, new *domain.Peer) error { func (m Manager) validatePeerCreation(ctx context.Context, _, new *domain.Peer) error {
currentUser := domain.GetUserInfo(ctx) currentUser := domain.GetUserInfo(ctx)
if new.Identifier == "" { if new.Identifier == "" {
@ -504,7 +504,7 @@ func (m Manager) validatePeerCreation(ctx context.Context, old, new *domain.Peer
return nil return nil
} }
func (m Manager) validatePeerDeletion(ctx context.Context, del *domain.Peer) error { func (m Manager) validatePeerDeletion(ctx context.Context, _ *domain.Peer) error {
currentUser := domain.GetUserInfo(ctx) currentUser := domain.GetUserInfo(ctx)
if !currentUser.IsAdmin && !m.cfg.Core.SelfProvisioningAllowed { if !currentUser.IsAdmin && !m.cfg.Core.SelfProvisioningAllowed {

View File

@ -9,24 +9,37 @@ import (
) )
type Auth struct { type Auth struct {
// OpenIDConnect contains a list of OpenID Connect providers.
OpenIDConnect []OpenIDConnectProvider `yaml:"oidc"` OpenIDConnect []OpenIDConnectProvider `yaml:"oidc"`
OAuth []OAuthProvider `yaml:"oauth"` // OAuth contains a list of plain OAuth providers.
Ldap []LdapProvider `yaml:"ldap"` OAuth []OAuthProvider `yaml:"oauth"`
// Ldap contains a list of LDAP providers.
Ldap []LdapProvider `yaml:"ldap"`
} }
type BaseFields struct { type BaseFields struct {
// UserIdentifier is the name of the field that contains the user identifier.
UserIdentifier string `yaml:"user_identifier"` UserIdentifier string `yaml:"user_identifier"`
Email string `yaml:"email"` // Email is the name of the field that contains the user's email address.
Firstname string `yaml:"firstname"` Email string `yaml:"email"`
Lastname string `yaml:"lastname"` // Firstname is the name of the field that contains the user's first name.
Phone string `yaml:"phone"` Firstname string `yaml:"firstname"`
Department string `yaml:"department"` // Lastname is the name of the field that contains the user's last name.
Lastname string `yaml:"lastname"`
// Phone is the name of the field that contains the user's phone number.
Phone string `yaml:"phone"`
// Department is the name of the field that contains the user's department.
Department string `yaml:"department"`
} }
type OauthFields struct { type OauthFields struct {
BaseFields `yaml:",inline"` BaseFields `yaml:",inline"`
IsAdmin string `yaml:"is_admin"` // If the value is "true", the user is an admin. // IsAdmin is the name of the field that contains the admin flag.
UserGroups string `yaml:"user_groups"` // This value specifies the claim name that contains the users groups. // If the value matches the admin_value_regex, the user is an admin. See OauthAdminMapping for more details.
IsAdmin string `yaml:"is_admin"`
// UserGroups is the name of the field that contains the user's groups.
// If the value matches the admin_group_regex, the user is an admin. See OauthAdminMapping for more details.
UserGroups string `yaml:"user_groups"`
} }
// OauthAdminMapping contains all necessary information to extract information about administrative privileges // OauthAdminMapping contains all necessary information to extract information about administrative privileges
@ -40,7 +53,7 @@ type OauthAdminMapping struct {
// If the regex specified in that field matches the contents of the is_admin field, the user is an admin. // 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"` 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, ] // 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. // the user is an admin.
AdminGroupRegex string `yaml:"admin_group_regex"` AdminGroupRegex string `yaml:"admin_group_regex"`
@ -50,6 +63,8 @@ type OauthAdminMapping struct {
adminGroupRegex *regexp.Regexp adminGroupRegex *regexp.Regexp
} }
// GetAdminValueRegex returns the compiled regular expression for the admin_value_regex field.
// If the field is empty, the default value "^true$" is used.
func (o *OauthAdminMapping) GetAdminValueRegex() *regexp.Regexp { func (o *OauthAdminMapping) GetAdminValueRegex() *regexp.Regexp {
if o.adminValueRegex != nil { if o.adminValueRegex != nil {
return o.adminValueRegex // return cached value return o.adminValueRegex // return cached value
@ -69,6 +84,8 @@ func (o *OauthAdminMapping) GetAdminValueRegex() *regexp.Regexp {
return o.adminValueRegex return o.adminValueRegex
} }
// GetAdminGroupRegex returns the compiled regular expression for the admin_group_regex field.
// If the field is empty, the default value "^wg_portal_default_admin_group$" is used.
func (o *OauthAdminMapping) GetAdminGroupRegex() *regexp.Regexp { func (o *OauthAdminMapping) GetAdminGroupRegex() *regexp.Regexp {
if o.adminGroupRegex != nil { if o.adminGroupRegex != nil {
return o.adminGroupRegex // return cached value return o.adminGroupRegex // return cached value
@ -89,7 +106,8 @@ func (o *OauthAdminMapping) GetAdminGroupRegex() *regexp.Regexp {
} }
type LdapFields struct { type LdapFields struct {
BaseFields `yaml:",inline"` BaseFields `yaml:",inline"`
// GroupMembership is the name of the LDAP field that contains the groups to which the user belongs.
GroupMembership string `yaml:"memberof"` GroupMembership string `yaml:"memberof"`
} }
@ -97,27 +115,43 @@ type LdapProvider struct {
// ProviderName is an internal name that is used to distinguish LDAP servers. It must not contain spaces or special characters. // ProviderName is an internal name that is used to distinguish LDAP servers. It must not contain spaces or special characters.
ProviderName string `yaml:"provider_name"` ProviderName string `yaml:"provider_name"`
URL string `yaml:"url"` // URL is the LDAP server URL, e.g. ldap://srv-ad01.company.local:389
StartTLS bool `yaml:"start_tls"` URL string `yaml:"url"`
CertValidation bool `yaml:"cert_validation"` // StartTLS specifies whether STARTTLS should be used to secure the LDAP connection
StartTLS bool `yaml:"start_tls"`
// CertValidation specifies whether the LDAP server's TLS certificate should be validated
CertValidation bool `yaml:"cert_validation"`
// TlsCertificatePath is the path to a TLS certificate if needed for LDAP connections
TlsCertificatePath string `yaml:"tls_certificate_path"` TlsCertificatePath string `yaml:"tls_certificate_path"`
TlsKeyPath string `yaml:"tls_key_path"` // TlsKeyPath is the path to the corresponding TLS certificate key
TlsKeyPath string `yaml:"tls_key_path"`
BaseDN string `yaml:"base_dn"` // BaseDN is the base DN for user searches
BaseDN string `yaml:"base_dn"`
// BindUser is the bind user for LDAP. It is used to search for users.
BindUser string `yaml:"bind_user"` BindUser string `yaml:"bind_user"`
// BindPass is the bind password for LDAP
BindPass string `yaml:"bind_pass"` BindPass string `yaml:"bind_pass"`
// FieldMap is used to map the names of the LDAP fields to wg-portal fields
FieldMap LdapFields `yaml:"field_map"` FieldMap LdapFields `yaml:"field_map"`
LoginFilter string `yaml:"login_filter"` // {{login_identifier}} gets replaced with the login email address / username // LoginFilter is used to select which users can log in.
AdminGroupDN string `yaml:"admin_group"` // Members of this group receive admin rights in WG-Portal // Use the placeholder {{login_identifier}} to insert the username.
LoginFilter string `yaml:"login_filter"`
// AdminGroupDN is the DN of the group that contains the administrators.
// Members of this group receive admin rights in wg-portal
AdminGroupDN string `yaml:"admin_group"`
// ParsedAdminGroupDN is the parsed version of AdminGroupDN
ParsedAdminGroupDN *ldap.DN `yaml:"-"` ParsedAdminGroupDN *ldap.DN `yaml:"-"`
// If DisableMissing is true, missing users will be deactivated // If DisableMissing is true, missing users will be deactivated
DisableMissing bool `yaml:"disable_missing"` 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 // 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"` AutoReEnable bool `yaml:"auto_re_enable"`
SyncFilter string `yaml:"sync_filter"` // SyncFilter is used to select which users get synchronized into wg-portal
SyncFilter string `yaml:"sync_filter"`
// SyncInterval is the interval between consecutive LDAP user syncs. If it is 0, sync is disabled.
SyncInterval time.Duration `yaml:"sync_interval"` 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. // If RegistrationEnabled is set to true, wg-portal will create new users that do not exist in the database.
@ -134,6 +168,7 @@ type OpenIDConnectProvider struct {
// DisplayName is shown to the user on the login page. If it is empty, ProviderName will be displayed. // DisplayName is shown to the user on the login page. If it is empty, ProviderName will be displayed.
DisplayName string `yaml:"display_name"` DisplayName string `yaml:"display_name"`
// BaseUrl is the base URL of the OIDC provider.
BaseUrl string `yaml:"base_url"` BaseUrl string `yaml:"base_url"`
// ClientID is the application's ID. // ClientID is the application's ID.
@ -172,8 +207,11 @@ type OAuthProvider struct {
// ClientSecret is the application's secret. // ClientSecret is the application's secret.
ClientSecret string `yaml:"client_secret"` ClientSecret string `yaml:"client_secret"`
AuthURL string `yaml:"auth_url"` // AuthURL is the URL to request OAuth user authorization.
TokenURL string `yaml:"token_url"` AuthURL string `yaml:"auth_url"`
// TokenURL is the URL to request a token.
TokenURL string `yaml:"token_url"`
// UserInfoURL is the URL to request user information.
UserInfoURL string `yaml:"user_info_url"` UserInfoURL string `yaml:"user_info_url"`
// Scope specifies optional requested permissions. // Scope specifies optional requested permissions.

View File

@ -63,6 +63,7 @@ type Config struct {
Web WebConfig `yaml:"web"` Web WebConfig `yaml:"web"`
} }
// LogStartupValues logs the startup values of the configuration in debug level
func (c *Config) LogStartupValues() { func (c *Config) LogStartupValues() {
logrus.Infof("Log Level: %s", c.Advanced.LogLevel) logrus.Infof("Log Level: %s", c.Advanced.LogLevel)
@ -89,6 +90,7 @@ func (c *Config) LogStartupValues() {
logrus.Debugf(" - Ldap Providers: %d", len(c.Auth.Ldap)) logrus.Debugf(" - Ldap Providers: %d", len(c.Auth.Ldap))
} }
// defaultConfig returns the default configuration
func defaultConfig() *Config { func defaultConfig() *Config {
cfg := &Config{} cfg := &Config{}
@ -155,6 +157,8 @@ func defaultConfig() *Config {
return cfg return cfg
} }
// GetConfig returns the configuration from the config file.
// Environment variable substitution is supported.
func GetConfig() (*Config, error) { func GetConfig() (*Config, error) {
cfg := defaultConfig() cfg := defaultConfig()

View File

@ -12,8 +12,14 @@ const (
) )
type DatabaseConfig struct { type DatabaseConfig struct {
Debug bool `yaml:"debug"` // Debug enables logging of all database statements
SlowQueryThreshold time.Duration `yaml:"slow_query_threshold"` // 0 means no logging of slow queries Debug bool `yaml:"debug"`
Type SupportedDatabase `yaml:"type"` // SlowQueryThreshold enables logging of slow queries which take longer than the specified duration
DSN string `yaml:"dsn"` // On SQLite: the database file-path, otherwise the dsn (see: https://gorm.io/docs/connecting_to_the_database.html) SlowQueryThreshold time.Duration `yaml:"slow_query_threshold"` // 0 means no logging of slow queries
// Type is the database type. Supported: mysql, mssql, postgres, sqlite
Type SupportedDatabase `yaml:"type"`
// DSN is the database connection string.
// For SQLite, it is the path to the database file.
// For other databases, it is the connection string, see: https://gorm.io/docs/connecting_to_the_database.html
DSN string `yaml:"dsn"`
} }

View File

@ -17,14 +17,23 @@ const (
) )
type MailConfig struct { type MailConfig struct {
Host string `yaml:"host"` // Host is the hostname or IP of the SMTP server
Port int `yaml:"port"` Host string `yaml:"host"`
Encryption MailEncryption `yaml:"encryption"` // Port is the port number for the SMTP server
CertValidation bool `yaml:"cert_validation"` Port int `yaml:"port"`
Username string `yaml:"username"` // Encryption is the SMTP encryption type
Password string `yaml:"password"` Encryption MailEncryption `yaml:"encryption"`
AuthType MailAuthType `yaml:"auth_type"` // CertValidation specifies whether the SMTP server certificate should be validated
CertValidation bool `yaml:"cert_validation"`
// Username is the optional SMTP username for authentication
Username string `yaml:"username"`
// Password is the optional SMTP password for authentication
Password string `yaml:"password"`
// AuthType is the SMTP authentication type
AuthType MailAuthType `yaml:"auth_type"`
From string `yaml:"from"` // From is the default "From" address when sending emails
LinkOnly bool `yaml:"link_only"` From string `yaml:"from"`
// LinkOnly specifies whether emails should only contain a link to WireGuard Portal or attach the full configuration
LinkOnly bool `yaml:"link_only"`
} }

View File

@ -1,14 +1,25 @@
package config package config
type WebConfig struct { type WebConfig struct {
RequestLogging bool `yaml:"request_logging"` // RequestLogging enables logging of all HTTP requests.
ExternalUrl string `yaml:"external_url"` RequestLogging bool `yaml:"request_logging"`
ListeningAddress string `yaml:"listening_address"` // ExternalUrl is the URL where a client can access WireGuard Portal.
// This is used for the callback URL of the OAuth providers.
ExternalUrl string `yaml:"external_url"`
// ListeningAddress is the address and port for the web server.
ListeningAddress string `yaml:"listening_address"`
// SessionIdentifier is the session identifier for the web frontend.
SessionIdentifier string `yaml:"session_identifier"` SessionIdentifier string `yaml:"session_identifier"`
SessionSecret string `yaml:"session_secret"` // SessionSecret is the session secret for the web frontend.
CsrfSecret string `yaml:"csrf_secret"` SessionSecret string `yaml:"session_secret"`
SiteTitle string `yaml:"site_title"` // CsrfSecret is the CSRF secret.
SiteCompanyName string `yaml:"site_company_name"` CsrfSecret string `yaml:"csrf_secret"`
CertFile string `yaml:"cert_file"` // SiteTitle is the title that is shown in the web frontend.
KeyFile string `yaml:"key_file"` SiteTitle string `yaml:"site_title"`
// SiteCompanyName is the company name that is shown at the bottom of the web frontend.
SiteCompanyName string `yaml:"site_company_name"`
// CertFile is the path to the TLS certificate file.
CertFile string `yaml:"cert_file"`
// KeyFile is the path to the TLS certificate key file.
KeyFile string `yaml:"key_file"`
} }

View File

@ -5,8 +5,6 @@ import "time"
type AuditSeverityLevel string type AuditSeverityLevel string
const AuditSeverityLevelLow AuditSeverityLevel = "low" const AuditSeverityLevelLow AuditSeverityLevel = "low"
const AuditSeverityLevelMedium AuditSeverityLevel = "medium"
const AuditSeverityLevelHigh AuditSeverityLevel = "high"
type AuditEntry struct { type AuditEntry struct {
UniqueId uint64 `gorm:"primaryKey;autoIncrement:true;column:id"` UniqueId uint64 `gorm:"primaryKey;autoIncrement:true;column:id"`

View File

@ -42,7 +42,7 @@ func (ps *PrivateString) Scan(value any) error {
case string: case string:
*ps = PrivateString(v) *ps = PrivateString(v)
case []byte: case []byte:
*ps = PrivateString(string(v)) *ps = PrivateString(v)
default: default:
return errors.New("invalid type for PrivateString") return errors.New("invalid type for PrivateString")
} }
@ -57,7 +57,6 @@ const (
DisabledReasonAdmin = "disabled by admin" DisabledReasonAdmin = "disabled by admin"
DisabledReasonApi = "disabled through api" DisabledReasonApi = "disabled through api"
DisabledReasonLdapMissing = "missing in ldap" DisabledReasonLdapMissing = "missing in ldap"
DisabledReasonUserMissing = "missing user"
DisabledReasonMigrationDummy = "migration dummy user" DisabledReasonMigrationDummy = "migration dummy user"
DisabledReasonInterfaceMissing = "missing WireGuard interface" DisabledReasonInterfaceMissing = "missing WireGuard interface"

View File

@ -19,6 +19,22 @@ func LogClose(c io.Closer) {
} }
} }
// LogError logs the given error if it is not nil.
// If a message is given, it is prepended to the error message.
// Only the first message is used.
func LogError(err error, msg ...string) {
if err == nil {
return
}
if len(msg) > 0 {
logrus.Errorf("%s: %v", msg[0], err)
return
}
logrus.Errorf("error: %v", err)
}
// SignalAwareContext returns a context that gets closed once a given signal is retrieved. // SignalAwareContext returns a context that gets closed once a given signal is retrieved.
// By default, the following signals are handled: syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP // By default, the following signals are handled: syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP
func SignalAwareContext(ctx context.Context, sig ...os.Signal) context.Context { func SignalAwareContext(ctx context.Context, sig ...os.Signal) context.Context {