mirror of
				https://github.com/h44z/wg-portal.git
				synced 2025-10-31 19:12:44 +08:00 
			
		
		
		
	chore: get rid of static code warnings
This commit is contained in:
		
							
								
								
									
										2
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
								
							| @@ -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 | ||||||
|   | |||||||
| @@ -5,7 +5,7 @@ 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: | | ||||||
|  |  | ||||||
| @@ -13,8 +13,8 @@ If you believe you've found a security issue in one of the supported versions of | |||||||
|  |  | ||||||
| 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. | ||||||
|   | |||||||
| @@ -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) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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 it’s guaranteed to be unique for the user within the IdP. Some providers also support `preferred_username` if it’s unique. | |     | `user_identifier` | `sub` or `preferred_username`     | A unique identifier for the user. Often the OIDC `sub` claim is used because it’s guaranteed to be unique for the user within the IdP. Some providers also support `preferred_username` if it’s unique. | | ||||||
|     | `email`           | `email`                           | The user’s email address as provided by the IdP. Not always verified, depending on IdP settings.                                                                                                        | |     | `email`           | `email`                           | The user’s email address as provided by the IdP. Not always verified, depending on IdP settings.                                                                                                        | | ||||||
|     | `firstname`       | `given_name`                      | The user’s first name, typically provided by the IdP in the `given_name` claim.                                                                                                                         | |     | `firstname`       | `given_name`                      | The user’s 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 it’s guaranteed to be unique for the user within the IdP. Some providers also support `preferred_username` if it’s unique. | |     | `user_identifier` | `sub` or `preferred_username`     | A unique identifier for the user. Often the OIDC `sub` claim is used because it’s guaranteed to be unique for the user within the IdP. Some providers also support `preferred_username` if it’s unique. | | ||||||
|     | `email`           | `email`                           | The user’s email address as provided by the IdP. Not always verified, depending on IdP settings.                                                                                                        | |     | `email`           | `email`                           | The user’s email address as provided by the IdP. Not always verified, depending on IdP settings.                                                                                                        | | ||||||
|     | `firstname`       | `given_name`                      | The user’s first name, typically provided by the IdP in the `given_name` claim.                                                                                                                         | |     | `firstname`       | `given_name`                      | The user’s 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.                      | | ||||||
|   | |||||||
| @@ -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 | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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() | ||||||
|   | |||||||
| @@ -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 { | ||||||
|   | |||||||
| @@ -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 | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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{} | ||||||
|   | |||||||
| @@ -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 { | ||||||
|   | |||||||
| @@ -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" | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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") | ||||||
|   | |||||||
| @@ -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) | ||||||
|   | |||||||
| @@ -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()) | ||||||
|   | |||||||
| @@ -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()) | ||||||
|   | |||||||
| @@ -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()) | ||||||
|   | |||||||
| @@ -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()) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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()) | ||||||
|   | |||||||
| @@ -14,8 +14,6 @@ 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 { | ||||||
|   | |||||||
| @@ -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, | ||||||
|   | |||||||
| @@ -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, | ||||||
|   | |||||||
| @@ -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 | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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: | ||||||
|   | |||||||
| @@ -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 | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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 { | ||||||
|   | |||||||
| @@ -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 { | ||||||
|   | |||||||
| @@ -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 contains a list of plain OAuth providers. | ||||||
| 	OAuth []OAuthProvider `yaml:"oauth"` | 	OAuth []OAuthProvider `yaml:"oauth"` | ||||||
|  | 	// Ldap contains a list of LDAP providers. | ||||||
| 	Ldap []LdapProvider `yaml:"ldap"` | 	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 is the name of the field that contains the user's email address. | ||||||
| 	Email string `yaml:"email"` | 	Email string `yaml:"email"` | ||||||
|  | 	// Firstname is the name of the field that contains the user's first name. | ||||||
| 	Firstname string `yaml:"firstname"` | 	Firstname string `yaml:"firstname"` | ||||||
|  | 	// Lastname is the name of the field that contains the user's last name. | ||||||
| 	Lastname string `yaml:"lastname"` | 	Lastname string `yaml:"lastname"` | ||||||
|  | 	// Phone is the name of the field that contains the user's phone number. | ||||||
| 	Phone string `yaml:"phone"` | 	Phone string `yaml:"phone"` | ||||||
|  | 	// Department is the name of the field that contains the user's department. | ||||||
| 	Department string `yaml:"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 | ||||||
| @@ -90,6 +107,7 @@ 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 is the LDAP server URL, e.g. ldap://srv-ad01.company.local:389 | ||||||
| 	URL string `yaml:"url"` | 	URL string `yaml:"url"` | ||||||
|  | 	// StartTLS specifies whether STARTTLS should be used to secure the LDAP connection | ||||||
| 	StartTLS bool `yaml:"start_tls"` | 	StartTLS bool `yaml:"start_tls"` | ||||||
|  | 	// CertValidation specifies whether the LDAP server's TLS certificate should be validated | ||||||
| 	CertValidation bool `yaml:"cert_validation"` | 	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 is the path to the corresponding TLS certificate key | ||||||
| 	TlsKeyPath string `yaml:"tls_key_path"` | 	TlsKeyPath string `yaml:"tls_key_path"` | ||||||
|  |  | ||||||
|  | 	// BaseDN is the base DN for user searches | ||||||
| 	BaseDN string `yaml:"base_dn"` | 	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 is used to select which users get synchronized into wg-portal | ||||||
| 	SyncFilter string `yaml:"sync_filter"` | 	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 is the URL to request OAuth user authorization. | ||||||
| 	AuthURL string `yaml:"auth_url"` | 	AuthURL string `yaml:"auth_url"` | ||||||
|  | 	// TokenURL is the URL to request a token. | ||||||
| 	TokenURL string `yaml:"token_url"` | 	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. | ||||||
|   | |||||||
| @@ -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() | ||||||
|  |  | ||||||
|   | |||||||
| @@ -12,8 +12,14 @@ const ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| type DatabaseConfig struct { | type DatabaseConfig struct { | ||||||
|  | 	// Debug enables logging of all database statements | ||||||
| 	Debug bool `yaml:"debug"` | 	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 | 	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"` | 	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) | 	// 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"` | ||||||
| } | } | ||||||
|   | |||||||
| @@ -17,14 +17,23 @@ const ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| type MailConfig struct { | type MailConfig struct { | ||||||
|  | 	// Host is the hostname or IP of the SMTP server | ||||||
| 	Host string `yaml:"host"` | 	Host string `yaml:"host"` | ||||||
|  | 	// Port is the port number for the SMTP server | ||||||
| 	Port int `yaml:"port"` | 	Port int `yaml:"port"` | ||||||
|  | 	// Encryption is the SMTP encryption type | ||||||
| 	Encryption MailEncryption `yaml:"encryption"` | 	Encryption MailEncryption `yaml:"encryption"` | ||||||
|  | 	// CertValidation specifies whether the SMTP server certificate should be validated | ||||||
| 	CertValidation bool `yaml:"cert_validation"` | 	CertValidation bool `yaml:"cert_validation"` | ||||||
|  | 	// Username is the optional SMTP username for authentication | ||||||
| 	Username string `yaml:"username"` | 	Username string `yaml:"username"` | ||||||
|  | 	// Password is the optional SMTP password for authentication | ||||||
| 	Password string `yaml:"password"` | 	Password string `yaml:"password"` | ||||||
|  | 	// AuthType is the SMTP authentication type | ||||||
| 	AuthType MailAuthType `yaml:"auth_type"` | 	AuthType MailAuthType `yaml:"auth_type"` | ||||||
|  |  | ||||||
|  | 	// From is the default "From" address when sending emails | ||||||
| 	From string `yaml:"from"` | 	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"` | 	LinkOnly bool `yaml:"link_only"` | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,14 +1,25 @@ | |||||||
| package config | package config | ||||||
|  |  | ||||||
| type WebConfig struct { | type WebConfig struct { | ||||||
|  | 	// RequestLogging enables logging of all HTTP requests. | ||||||
| 	RequestLogging bool `yaml:"request_logging"` | 	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"` | 	ExternalUrl string `yaml:"external_url"` | ||||||
|  | 	// ListeningAddress is the address and port for the web server. | ||||||
| 	ListeningAddress string `yaml:"listening_address"` | 	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 is the session secret for the web frontend. | ||||||
| 	SessionSecret string `yaml:"session_secret"` | 	SessionSecret string `yaml:"session_secret"` | ||||||
|  | 	// CsrfSecret is the CSRF secret. | ||||||
| 	CsrfSecret string `yaml:"csrf_secret"` | 	CsrfSecret string `yaml:"csrf_secret"` | ||||||
|  | 	// SiteTitle is the title that is shown in the web frontend. | ||||||
| 	SiteTitle string `yaml:"site_title"` | 	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"` | 	SiteCompanyName string `yaml:"site_company_name"` | ||||||
|  | 	// CertFile is the path to the TLS certificate file. | ||||||
| 	CertFile string `yaml:"cert_file"` | 	CertFile string `yaml:"cert_file"` | ||||||
|  | 	// KeyFile is the path to the TLS certificate key file. | ||||||
| 	KeyFile string `yaml:"key_file"` | 	KeyFile string `yaml:"key_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"` | ||||||
|   | |||||||
| @@ -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" | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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 { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Christoph Haas
					Christoph Haas