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
for better better formatting of yaml config or logs
for better formatting of yaml config or logs
```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
| Version | Supported |
| ------- | -------------------- |
| v2.x | :white_check_mark: |
| v1.x | :white_check_mark: |
| Version | Supported |
|---------|--------------------|
| v2.x | :white_check_mark: |
| v1.x | :white_check_mark: |
## Reporting a Vulnerability
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).
If you prefer to submit a report without logging in to Github, please email *info (at) wgportal.org*.
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*.
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.

View File

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

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

View File

@ -4,6 +4,7 @@ import { computed, getCurrentInstance, onMounted, ref } from "vue";
import { authStore } from "./stores/auth";
import { securityStore } from "./stores/security";
import { settingsStore } from "@/stores/settings";
import { Notifications } from "@kyvg/vue3-notification";
const appGlobal = getCurrentInstance().appContext.config.globalProperties
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) {
var gormDb *gorm.DB
var err error
@ -172,6 +173,7 @@ type SqlRepo struct {
db *gorm.DB
}
// NewSqlRepository creates a new SqlRepo instance.
func NewSqlRepository(db *gorm.DB) (*SqlRepo, error) {
repo := &SqlRepo{
db: db,
@ -236,6 +238,8 @@ func (r *SqlRepo) migrate() error {
// 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) {
var in domain.Interface
@ -251,6 +255,8 @@ func (r *SqlRepo) GetInterface(ctx context.Context, id domain.InterfaceIdentifie
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) (
*domain.Interface,
[]domain.Peer,
@ -269,6 +275,7 @@ func (r *SqlRepo) GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceI
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) {
if len(ids) == 0 {
return nil, nil
@ -284,6 +291,7 @@ func (r *SqlRepo) GetPeersStats(ctx context.Context, ids ...domain.PeerIdentifie
return stats, nil
}
// GetAllInterfaces returns all interfaces.
func (r *SqlRepo) GetAllInterfaces(ctx context.Context) ([]domain.Interface, error) {
var interfaces []domain.Interface
@ -295,6 +303,8 @@ func (r *SqlRepo) GetAllInterfaces(ctx context.Context) ([]domain.Interface, err
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) (
*domain.InterfaceStatus,
error,
@ -319,6 +329,8 @@ func (r *SqlRepo) GetInterfaceStats(ctx context.Context, id domain.InterfaceIden
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) {
var users []domain.Interface
@ -335,6 +347,7 @@ func (r *SqlRepo) FindInterfaces(ctx context.Context, search string) ([]domain.I
return users, nil
}
// SaveInterface updates the interface with the given id.
func (r *SqlRepo) SaveInterface(
ctx context.Context,
id domain.InterfaceIdentifier,
@ -410,6 +423,7 @@ func (r *SqlRepo) upsertInterface(ui *domain.ContextUserInfo, tx *gorm.DB, in *d
return nil
}
// DeleteInterface deletes the interface with the given id.
func (r *SqlRepo) DeleteInterface(ctx context.Context, id domain.InterfaceIdentifier) error {
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) 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
}
// 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) {
var ips []struct {
domain.Cidr
@ -461,6 +476,8 @@ func (r *SqlRepo) GetInterfaceIps(ctx context.Context) (map[domain.InterfaceIden
// 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) {
var peer domain.Peer
@ -476,6 +493,7 @@ func (r *SqlRepo) GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domai
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) {
var peers []domain.Peer
@ -487,6 +505,8 @@ func (r *SqlRepo) GetInterfacePeers(ctx context.Context, id domain.InterfaceIden
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) (
[]domain.Peer,
error,
@ -506,6 +526,7 @@ func (r *SqlRepo) FindInterfacePeers(ctx context.Context, id domain.InterfaceIde
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) {
var peers []domain.Peer
@ -517,6 +538,8 @@ func (r *SqlRepo) GetUserPeers(ctx context.Context, id domain.UserIdentifier) ([
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) {
var peers []domain.Peer
@ -533,6 +556,8 @@ func (r *SqlRepo) FindUserPeers(ctx context.Context, id domain.UserIdentifier, s
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(
ctx context.Context,
id domain.PeerIdentifier,
@ -607,6 +632,7 @@ func (r *SqlRepo) upsertPeer(ui *domain.ContextUserInfo, tx *gorm.DB, peer *doma
return nil
}
// DeletePeer deletes the peer with the given id.
func (r *SqlRepo) DeletePeer(ctx context.Context, id domain.PeerIdentifier) error {
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) 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
}
// 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) {
var ips []struct {
domain.Cidr
@ -649,6 +676,7 @@ func (r *SqlRepo) GetPeerIps(ctx context.Context) (map[domain.PeerIdentifier][]d
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) (
map[domain.Cidr][]domain.Cidr,
error,
@ -707,6 +735,8 @@ func (r *SqlRepo) GetUsedIpsPerSubnet(ctx context.Context, subnets []domain.Cidr
// 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) {
var user domain.User
@ -722,6 +752,9 @@ func (r *SqlRepo) GetUser(ctx context.Context, id domain.UserIdentifier) (*domai
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) {
var users []domain.User
@ -746,6 +779,7 @@ func (r *SqlRepo) GetUserByEmail(ctx context.Context, email string) (*domain.Use
return &user, nil
}
// GetAllUsers returns all users.
func (r *SqlRepo) GetAllUsers(ctx context.Context) ([]domain.User, error) {
var users []domain.User
@ -757,6 +791,8 @@ func (r *SqlRepo) GetAllUsers(ctx context.Context) ([]domain.User, error) {
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) {
var users []domain.User
@ -774,6 +810,8 @@ func (r *SqlRepo) FindUsers(ctx context.Context, search string) ([]domain.User,
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(
ctx context.Context,
id domain.UserIdentifier,
@ -807,6 +845,7 @@ func (r *SqlRepo) SaveUser(
return nil
}
// DeleteUser deletes the user with the given id.
func (r *SqlRepo) DeleteUser(ctx context.Context, id domain.UserIdentifier) error {
err := r.db.WithContext(ctx).Delete(&domain.User{}, id).Error
if err != nil {
@ -859,6 +898,8 @@ func (r *SqlRepo) upsertUser(ui *domain.ContextUserInfo, tx *gorm.DB, user *doma
// 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(
ctx context.Context,
id domain.InterfaceIdentifier,
@ -919,6 +960,8 @@ func (r *SqlRepo) upsertInterfaceStatus(tx *gorm.DB, in *domain.InterfaceStatus)
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(
ctx context.Context,
id domain.PeerIdentifier,
@ -976,6 +1019,7 @@ func (r *SqlRepo) upsertPeerStatus(tx *gorm.DB, in *domain.PeerStatus) error {
return nil
}
// DeletePeerStatus deletes the peer status with the given id.
func (r *SqlRepo) DeletePeerStatus(ctx context.Context, id domain.PeerIdentifier) error {
err := r.db.WithContext(ctx).Delete(&domain.PeerStatus{}, id).Error
if err != nil {
@ -989,6 +1033,7 @@ func (r *SqlRepo) DeletePeerStatus(ctx context.Context, id domain.PeerIdentifier
// region audit
// SaveAuditEntry saves the given audit entry.
func (r *SqlRepo) SaveAuditEntry(ctx context.Context, entry *domain.AuditEntry) error {
err := r.db.WithContext(ctx).Save(entry).Error
if err != nil {

View File

@ -13,6 +13,7 @@ type FilesystemRepo struct {
basePath string
}
// NewFileSystemRepository creates a new FilesystemRepo instance.
func NewFileSystemRepository(basePath string) (*FilesystemRepo, error) {
if basePath == "" {
return nil, nil // no path, return empty repository
@ -27,6 +28,10 @@ func NewFileSystemRepository(basePath string) (*FilesystemRepo, error) {
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 {
filePath := filepath.Join(r.basePath, path)
parentDirectory := filepath.Dir(filePath)
@ -51,5 +56,4 @@ func (r *FilesystemRepo) WriteFile(path string, contents io.Reader) error {
}
return nil
}

View File

@ -19,11 +19,12 @@ type MailRepo struct {
cfg *config.MailConfig
}
// NewSmtpMailRepo creates a new MailRepo instance.
func NewSmtpMailRepo(cfg config.MailConfig) MailRepo {
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 {
if options == nil {
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) {
// Run the metrics server in a goroutine
go func() {
@ -104,7 +104,7 @@ func (m *MetricsServer) Run(ctx context.Context) {
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Attempt to gracefully shutdown the metrics server
// Attempt to gracefully shut down the metrics server
if err := m.Shutdown(shutdownCtx); err != nil {
logrus.Errorf("metrics service on %s shutdown failed: %v", m.Addr, err)
} else {
@ -123,9 +123,9 @@ func (m *MetricsServer) UpdateInterfaceMetrics(status domain.InterfaceStatus) {
func (m *MetricsServer) UpdatePeerMetrics(peer *domain.Peer, status domain.PeerStatus) {
labels := []string{
string(peer.InterfaceIdentifier),
string(peer.Interface.AddressStr()),
peer.Interface.AddressStr(),
string(status.PeerId),
string(peer.DisplayName),
peer.DisplayName,
}
if status.LastHandshake != nil {

View File

@ -18,6 +18,7 @@ type WgQuickRepo struct {
resolvConfIfacePrefix string
}
// NewWgQuickRepo creates a new WgQuickRepo instance.
func NewWgQuickRepo() *WgQuickRepo {
return &WgQuickRepo{
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 {
if hookCmd == "" {
return nil
@ -39,6 +44,7 @@ func (r *WgQuickRepo) ExecuteInterfaceHook(id domain.InterfaceIdentifier, hookCm
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 {
if dnsStr == "" && dnsSearchStr == "" {
return nil
@ -68,6 +74,7 @@ func (r *WgQuickRepo) SetDNS(id domain.InterfaceIdentifier, dnsStr, dnsSearchStr
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 {
dnsCommand := "resolvconf -d %resPref%i -f"

View File

@ -20,6 +20,8 @@ type WgRepo struct {
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 {
wg, err := wgctrl.New()
if err != nil {
@ -36,6 +38,7 @@ func NewWireGuardRepository() *WgRepo {
return repo
}
// GetInterfaces returns all existing WireGuard interfaces.
func (r *WgRepo) GetInterfaces(_ context.Context) ([]domain.PhysicalInterface, error) {
devices, err := r.wg.Devices()
if err != nil {
@ -54,10 +57,14 @@ func (r *WgRepo) GetInterfaces(_ context.Context) ([]domain.PhysicalInterface, e
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) {
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) {
device, err := r.wg.Device(string(deviceId))
if err != nil {
@ -76,6 +83,8 @@ func (r *WgRepo) GetPeers(_ context.Context, deviceId domain.InterfaceIdentifier
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(
_ context.Context,
deviceId domain.InterfaceIdentifier,
@ -157,6 +166,9 @@ func (r *WgRepo) convertWireGuardPeer(peer *wgtypes.Peer) (domain.PhysicalPeer,
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(
_ context.Context,
id domain.InterfaceIdentifier,
@ -187,10 +199,10 @@ func (r *WgRepo) SaveInterface(
func (r *WgRepo) getOrCreateInterface(id domain.InterfaceIdentifier) (*domain.PhysicalInterface, error) {
device, err := r.getInterface(id)
if err == nil {
return device, nil
return device, nil // interface exists
}
if err != nil && !errors.Is(err, os.ErrNotExist) {
return nil, fmt.Errorf("device error: %w", err)
if !errors.Is(err, os.ErrNotExist) {
return nil, fmt.Errorf("device error: %w", err) // unknown error
}
// create new device
@ -308,6 +320,8 @@ func (r *WgRepo) updateWireGuardInterface(pi *domain.PhysicalInterface) error {
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 {
if err := r.deleteLowLevelInterface(id); err != nil {
return err
@ -334,6 +348,8 @@ func (r *WgRepo) deleteLowLevelInterface(id domain.InterfaceIdentifier) error {
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(
_ context.Context,
deviceId domain.InterfaceIdentifier,
@ -363,10 +379,10 @@ func (r *WgRepo) getOrCreatePeer(deviceId domain.InterfaceIdentifier, id domain.
) {
peer, err := r.getPeer(deviceId, id)
if err == nil {
return peer, nil
return peer, nil // peer exists
}
if err != nil && !errors.Is(err, os.ErrNotExist) {
return nil, fmt.Errorf("peer error: %w", err)
if !errors.Is(err, os.ErrNotExist) {
return nil, fmt.Errorf("peer error: %w", err) // unknown error
}
// create new peer
@ -425,6 +441,8 @@ func (r *WgRepo) updatePeer(deviceId domain.InterfaceIdentifier, pp *domain.Phys
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 {
if !id.IsPublicKey() {
return errors.New("invalid public key")

View File

@ -15,6 +15,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/h44z/wg-portal/internal"
"github.com/h44z/wg-portal/internal/domain"
)
@ -42,12 +43,12 @@ func Test_wgRepository_GetInterfaces(t *testing.T) {
mgr := setup(t)
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)
require.NoError(t, err)
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)
require.NoError(t, err)
@ -65,7 +66,7 @@ func TestWireGuardCreateInterface(t *testing.T) {
interfaceName := domain.InterfaceIdentifier("wg_test_001")
ipAddress := "10.11.12.13"
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,
func(pi *domain.PhysicalInterface) (*domain.PhysicalInterface, error) {
@ -90,7 +91,7 @@ func TestWireGuardUpdateInterface(t *testing.T) {
mgr := setup(t)
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)
require.NoError(t, err)

View File

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

View File

@ -20,7 +20,7 @@ func (e interfaceEndpoint) GetName() string {
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.GET("/prepare", e.handlePrepareGet())

View File

@ -20,7 +20,7 @@ func (e peerEndpoint) GetName() string {
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.GET("/iface/:iface/all", e.authenticator.LoggedIn(ScopeAdmin), e.handleAllGet())

View File

@ -16,7 +16,7 @@ func (e testEndpoint) GetName() string {
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("/hostname", e.handleHostnameGet())
}

View File

@ -19,7 +19,7 @@ func (e userEndpoint) GetName() string {
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.GET("/all", e.authenticator.LoggedIn(ScopeAdmin), e.handleAllGet())

View File

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

View File

@ -10,13 +10,6 @@ type ConfigOption[T any] struct {
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] {
return ConfigOption[T]{
Value: opt.Value,

View File

@ -10,13 +10,6 @@ type ConfigOption[T any] struct {
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] {
return ConfigOption[T]{
Value: opt.Value,

View File

@ -8,14 +8,15 @@ import (
"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")
migrationDbType := flag.String("migrateFromType", string(config.DatabaseSQLite),
"old database type, either mysql, mssql, postgres or sqlite")
flag.Parse()
if *migrationSource != "" {
err = migrateFromV1(cfg, db, *migrationSource, *migrationDbType)
err = migrateFromV1(db, *migrationSource, *migrationDbType)
exit = true
}

View File

@ -14,7 +14,7 @@ import (
"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)
switch sourceType {
case config.DatabaseMySQL, config.DatabasePostgres, config.DatabaseMsSQL:

View File

@ -63,7 +63,7 @@ func (m Manager) connectToMessageBus() {
_ = 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) {
@ -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)
}
table, fwmark, err := m.getRoutingTableAndFwMark(&iface, allowedIPs, link)
table, fwmark, err := m.getRoutingTableAndFwMark(&iface, link)
if err != nil {
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
}
func (m Manager) getRoutingTableAndFwMark(
iface *domain.Interface,
allowedIPs []domain.Cidr,
link netlink.Link,
) (table int, fwmark uint32, err error) {
func (m Manager) getRoutingTableAndFwMark(iface *domain.Interface, link netlink.Link) (
table int,
fwmark uint32,
err error,
) {
table = iface.GetRoutingTable()
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.
// 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 {
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
}
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)
if !currentUser.IsAdmin {
@ -847,7 +848,7 @@ func (m Manager) validateInterfaceModifications(ctx context.Context, old, new *d
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)
if new.Identifier == "" {
@ -868,7 +869,7 @@ func (m Manager) validateInterfaceCreation(ctx context.Context, old, new *domain
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)
if !currentUser.IsAdmin {

View File

@ -475,7 +475,7 @@ func (m Manager) getFreshPeerIpConfig(ctx context.Context, iface *domain.Interfa
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)
if !currentUser.IsAdmin && !m.cfg.Core.SelfProvisioningAllowed {
@ -485,7 +485,7 @@ func (m Manager) validatePeerModifications(ctx context.Context, old, new *domain
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)
if new.Identifier == "" {
@ -504,7 +504,7 @@ func (m Manager) validatePeerCreation(ctx context.Context, old, new *domain.Peer
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)
if !currentUser.IsAdmin && !m.cfg.Core.SelfProvisioningAllowed {

View File

@ -9,24 +9,37 @@ import (
)
type Auth struct {
// OpenIDConnect contains a list of OpenID Connect providers.
OpenIDConnect []OpenIDConnectProvider `yaml:"oidc"`
OAuth []OAuthProvider `yaml:"oauth"`
Ldap []LdapProvider `yaml:"ldap"`
// OAuth contains a list of plain OAuth providers.
OAuth []OAuthProvider `yaml:"oauth"`
// Ldap contains a list of LDAP providers.
Ldap []LdapProvider `yaml:"ldap"`
}
type BaseFields struct {
// UserIdentifier is the name of the field that contains the user identifier.
UserIdentifier string `yaml:"user_identifier"`
Email string `yaml:"email"`
Firstname string `yaml:"firstname"`
Lastname string `yaml:"lastname"`
Phone string `yaml:"phone"`
Department string `yaml:"department"`
// Email is the name of the field that contains the user's email address.
Email string `yaml:"email"`
// Firstname is the name of the field that contains the user's first name.
Firstname string `yaml:"firstname"`
// 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 {
BaseFields `yaml:",inline"`
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.
// IsAdmin is the name of the field that contains the admin flag.
// 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
@ -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.
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.
AdminGroupRegex string `yaml:"admin_group_regex"`
@ -50,6 +63,8 @@ type OauthAdminMapping struct {
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 {
if o.adminValueRegex != nil {
return o.adminValueRegex // return cached value
@ -69,6 +84,8 @@ func (o *OauthAdminMapping) GetAdminValueRegex() *regexp.Regexp {
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 {
if o.adminGroupRegex != nil {
return o.adminGroupRegex // return cached value
@ -89,7 +106,8 @@ func (o *OauthAdminMapping) GetAdminGroupRegex() *regexp.Regexp {
}
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"`
}
@ -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 string `yaml:"provider_name"`
URL string `yaml:"url"`
StartTLS bool `yaml:"start_tls"`
CertValidation bool `yaml:"cert_validation"`
// URL is the LDAP server URL, e.g. ldap://srv-ad01.company.local:389
URL string `yaml:"url"`
// 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"`
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"`
// BindPass is the bind password for LDAP
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"`
LoginFilter string `yaml:"login_filter"` // {{login_identifier}} gets replaced with the login email address / username
AdminGroupDN string `yaml:"admin_group"` // Members of this group receive admin rights in WG-Portal
// LoginFilter is used to select which users can log in.
// 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:"-"`
// If DisableMissing is true, missing users will be deactivated
DisableMissing bool `yaml:"disable_missing"`
// If AutoReEnable is true, users that where disabled because they were missing will be re-enabled once they are found again
AutoReEnable bool `yaml:"auto_re_enable"`
SyncFilter string `yaml:"sync_filter"`
AutoReEnable bool `yaml:"auto_re_enable"`
// 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"`
// 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 string `yaml:"display_name"`
// BaseUrl is the base URL of the OIDC provider.
BaseUrl string `yaml:"base_url"`
// ClientID is the application's ID.
@ -172,8 +207,11 @@ type OAuthProvider struct {
// ClientSecret is the application's secret.
ClientSecret string `yaml:"client_secret"`
AuthURL string `yaml:"auth_url"`
TokenURL string `yaml:"token_url"`
// AuthURL is the URL to request OAuth user authorization.
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"`
// Scope specifies optional requested permissions.

View File

@ -63,6 +63,7 @@ type Config struct {
Web WebConfig `yaml:"web"`
}
// LogStartupValues logs the startup values of the configuration in debug level
func (c *Config) LogStartupValues() {
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))
}
// defaultConfig returns the default configuration
func defaultConfig() *Config {
cfg := &Config{}
@ -155,6 +157,8 @@ func defaultConfig() *Config {
return cfg
}
// GetConfig returns the configuration from the config file.
// Environment variable substitution is supported.
func GetConfig() (*Config, error) {
cfg := defaultConfig()

View File

@ -12,8 +12,14 @@ const (
)
type DatabaseConfig struct {
Debug bool `yaml:"debug"`
SlowQueryThreshold time.Duration `yaml:"slow_query_threshold"` // 0 means no logging of slow queries
Type SupportedDatabase `yaml:"type"`
DSN string `yaml:"dsn"` // On SQLite: the database file-path, otherwise the dsn (see: https://gorm.io/docs/connecting_to_the_database.html)
// Debug enables logging of all database statements
Debug bool `yaml:"debug"`
// SlowQueryThreshold enables logging of slow queries which take longer than the specified duration
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 {
Host string `yaml:"host"`
Port int `yaml:"port"`
Encryption MailEncryption `yaml:"encryption"`
CertValidation bool `yaml:"cert_validation"`
Username string `yaml:"username"`
Password string `yaml:"password"`
AuthType MailAuthType `yaml:"auth_type"`
// Host is the hostname or IP of the SMTP server
Host string `yaml:"host"`
// Port is the port number for the SMTP server
Port int `yaml:"port"`
// Encryption is the SMTP encryption type
Encryption MailEncryption `yaml:"encryption"`
// 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"`
LinkOnly bool `yaml:"link_only"`
// From is the default "From" address when sending emails
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
type WebConfig struct {
RequestLogging bool `yaml:"request_logging"`
ExternalUrl string `yaml:"external_url"`
ListeningAddress string `yaml:"listening_address"`
// RequestLogging enables logging of all HTTP requests.
RequestLogging bool `yaml:"request_logging"`
// 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"`
SessionSecret string `yaml:"session_secret"`
CsrfSecret string `yaml:"csrf_secret"`
SiteTitle string `yaml:"site_title"`
SiteCompanyName string `yaml:"site_company_name"`
CertFile string `yaml:"cert_file"`
KeyFile string `yaml:"key_file"`
// SessionSecret is the session secret for the web frontend.
SessionSecret string `yaml:"session_secret"`
// CsrfSecret is the CSRF secret.
CsrfSecret string `yaml:"csrf_secret"`
// SiteTitle is the title that is shown in the web frontend.
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
const AuditSeverityLevelLow AuditSeverityLevel = "low"
const AuditSeverityLevelMedium AuditSeverityLevel = "medium"
const AuditSeverityLevelHigh AuditSeverityLevel = "high"
type AuditEntry struct {
UniqueId uint64 `gorm:"primaryKey;autoIncrement:true;column:id"`

View File

@ -42,7 +42,7 @@ func (ps *PrivateString) Scan(value any) error {
case string:
*ps = PrivateString(v)
case []byte:
*ps = PrivateString(string(v))
*ps = PrivateString(v)
default:
return errors.New("invalid type for PrivateString")
}
@ -57,7 +57,6 @@ const (
DisabledReasonAdmin = "disabled by admin"
DisabledReasonApi = "disabled through api"
DisabledReasonLdapMissing = "missing in ldap"
DisabledReasonUserMissing = "missing user"
DisabledReasonMigrationDummy = "migration dummy user"
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.
// By default, the following signals are handled: syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP
func SignalAwareContext(ctx context.Context, sig ...os.Signal) context.Context {