-
+
@@ -56,9 +64,17 @@ onMounted(async () => {
diff --git a/internal/app/api/v0/handlers/endpoint_peers.go b/internal/app/api/v0/handlers/endpoint_peers.go
index 60645ab..3eb65ba 100644
--- a/internal/app/api/v0/handlers/endpoint_peers.go
+++ b/internal/app/api/v0/handlers/endpoint_peers.go
@@ -24,8 +24,8 @@ func (e peerEndpoint) RegisterRoutes(g *gin.RouterGroup, authenticator *authenti
apiGroup.GET("/iface/:iface/all", e.authenticator.LoggedIn(ScopeAdmin), e.handleAllGet())
apiGroup.GET("/iface/:iface/stats", e.authenticator.LoggedIn(ScopeAdmin), e.handleStatsGet())
- apiGroup.GET("/iface/:iface/prepare", e.authenticator.LoggedIn(ScopeAdmin), e.handlePrepareGet())
- apiGroup.POST("/iface/:iface/new", e.authenticator.LoggedIn(ScopeAdmin), e.handleCreatePost())
+ apiGroup.GET("/iface/:iface/prepare", e.authenticator.LoggedIn(), e.handlePrepareGet())
+ apiGroup.POST("/iface/:iface/new", e.authenticator.LoggedIn(), e.handleCreatePost())
apiGroup.POST("/iface/:iface/multiplenew", e.authenticator.LoggedIn(ScopeAdmin), e.handleCreateMultiplePost())
apiGroup.GET("/config-qr/:id", e.handleQrCodeGet())
apiGroup.POST("/config-mail", e.handleEmailPost())
diff --git a/internal/app/api/v0/handlers/endpoint_users.go b/internal/app/api/v0/handlers/endpoint_users.go
index 74c5929..5302b9b 100644
--- a/internal/app/api/v0/handlers/endpoint_users.go
+++ b/internal/app/api/v0/handlers/endpoint_users.go
@@ -28,6 +28,7 @@ func (e userEndpoint) RegisterRoutes(g *gin.RouterGroup, authenticator *authenti
apiGroup.POST("/new", e.authenticator.LoggedIn(ScopeAdmin), e.handleCreatePost())
apiGroup.GET("/:id/peers", e.authenticator.UserIdMatch("id"), e.handlePeersGet())
apiGroup.GET("/:id/stats", e.authenticator.UserIdMatch("id"), e.handleStatsGet())
+ apiGroup.GET("/:id/interfaces", e.authenticator.UserIdMatch("id"), e.handleInterfacesGet())
apiGroup.POST("/:id/api/enable", e.authenticator.UserIdMatch("id"), e.handleApiEnablePost())
apiGroup.POST("/:id/api/disable", e.authenticator.UserIdMatch("id"), e.handleApiDisablePost())
}
@@ -170,6 +171,7 @@ func (e userEndpoint) handleCreatePost() gin.HandlerFunc {
// @ID users_handlePeersGet
// @Tags Users
// @Summary Get peers for the given user.
+// @Param id path string true "The user identifier"
// @Produce json
// @Success 200 {object} []model.Peer
// @Failure 400 {object} model.Error
@@ -179,14 +181,14 @@ func (e userEndpoint) handlePeersGet() gin.HandlerFunc {
return func(c *gin.Context) {
ctx := domain.SetUserInfoFromGin(c)
- interfaceId := Base64UrlDecode(c.Param("id"))
- if interfaceId == "" {
+ userId := Base64UrlDecode(c.Param("id"))
+ if userId == "" {
c.JSON(http.StatusBadRequest,
model.Error{Code: http.StatusInternalServerError, Message: "missing id parameter"})
return
}
- peers, err := e.app.GetUserPeers(ctx, domain.UserIdentifier(interfaceId))
+ peers, err := e.app.GetUserPeers(ctx, domain.UserIdentifier(userId))
if err != nil {
c.JSON(http.StatusInternalServerError,
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
@@ -202,6 +204,7 @@ func (e userEndpoint) handlePeersGet() gin.HandlerFunc {
// @ID users_handleStatsGet
// @Tags Users
// @Summary Get peer stats for the given user.
+// @Param id path string true "The user identifier"
// @Produce json
// @Success 200 {object} model.PeerStats
// @Failure 400 {object} model.Error
@@ -229,6 +232,39 @@ func (e userEndpoint) handleStatsGet() gin.HandlerFunc {
}
}
+// handleInterfacesGet returns a gorm handler function.
+//
+// @ID users_handleInterfacesGet
+// @Tags Users
+// @Summary Get interfaces for the given user. Returns an empty list if self provisioning is disabled.
+// @Param id path string true "The user identifier"
+// @Produce json
+// @Success 200 {object} []model.Interface
+// @Failure 400 {object} model.Error
+// @Failure 500 {object} model.Error
+// @Router /user/{id}/interfaces [get]
+func (e userEndpoint) handleInterfacesGet() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ ctx := domain.SetUserInfoFromGin(c)
+
+ userId := Base64UrlDecode(c.Param("id"))
+ if userId == "" {
+ c.JSON(http.StatusBadRequest,
+ model.Error{Code: http.StatusInternalServerError, Message: "missing id parameter"})
+ return
+ }
+
+ peers, err := e.app.GetUserInterfaces(ctx, domain.UserIdentifier(userId))
+ if err != nil {
+ c.JSON(http.StatusInternalServerError,
+ model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, model.NewInterfaces(peers, nil))
+ }
+}
+
// handleDelete returns a gorm handler function.
//
// @ID users_handleDelete
diff --git a/internal/app/api/v0/model/models_interface.go b/internal/app/api/v0/model/models_interface.go
index aef336d..2d7b7b9 100644
--- a/internal/app/api/v0/model/models_interface.go
+++ b/internal/app/api/v0/model/models_interface.go
@@ -109,7 +109,11 @@ func NewInterface(src *domain.Interface, peers []domain.Peer) *Interface {
func NewInterfaces(src []domain.Interface, srcPeers [][]domain.Peer) []Interface {
results := make([]Interface, len(src))
for i := range src {
- results[i] = *NewInterface(&src[i], srcPeers[i])
+ if srcPeers == nil {
+ results[i] = *NewInterface(&src[i], nil)
+ } else {
+ results[i] = *NewInterface(&src[i], srcPeers[i])
+ }
}
return results
diff --git a/internal/app/repos.go b/internal/app/repos.go
index 023189a..85fae2a 100644
--- a/internal/app/repos.go
+++ b/internal/app/repos.go
@@ -39,6 +39,7 @@ type WireGuardManager interface {
GetUserPeerStats(ctx context.Context, id domain.UserIdentifier) ([]domain.PeerStatus, error)
GetAllInterfacesAndPeers(ctx context.Context) ([]domain.Interface, [][]domain.Peer, error)
GetUserPeers(ctx context.Context, id domain.UserIdentifier) ([]domain.Peer, error)
+ GetUserInterfaces(ctx context.Context, id domain.UserIdentifier) ([]domain.Interface, error)
PrepareInterface(ctx context.Context) (*domain.Interface, error)
CreateInterface(ctx context.Context, in *domain.Interface) (*domain.Interface, error)
UpdateInterface(ctx context.Context, in *domain.Interface) (*domain.Interface, []domain.Peer, error)
diff --git a/internal/app/wireguard/wireguard_interfaces.go b/internal/app/wireguard/wireguard_interfaces.go
index e0bbcff..02f9e3d 100644
--- a/internal/app/wireguard/wireguard_interfaces.go
+++ b/internal/app/wireguard/wireguard_interfaces.go
@@ -68,6 +68,34 @@ func (m Manager) GetAllInterfacesAndPeers(ctx context.Context) ([]domain.Interfa
return interfaces, allPeers, nil
}
+// 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) {
+ if !m.cfg.Core.SelfProvisioningAllowed {
+ return nil, nil // self-provisioning is disabled - no interfaces for users
+ }
+
+ interfaces, err := m.db.GetAllInterfaces(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("unable to load all interfaces: %w", err)
+ }
+
+ // strip sensitive data, users only need very limited information
+ userInterfaces := make([]domain.Interface, 0, len(interfaces))
+ for _, iface := range interfaces {
+ if iface.IsDisabled() {
+ continue // skip disabled interfaces
+ }
+ if iface.Type != domain.InterfaceTypeServer {
+ continue // skip client interfaces
+ }
+
+ userInterfaces = append(userInterfaces, iface.PublicInfo())
+ }
+
+ return userInterfaces, nil
+}
+
func (m Manager) ImportNewInterfaces(ctx context.Context, filter ...domain.InterfaceIdentifier) (int, error) {
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
return 0, err
diff --git a/internal/app/wireguard/wireguard_peers.go b/internal/app/wireguard/wireguard_peers.go
index 0f1039e..4bea2d4 100644
--- a/internal/app/wireguard/wireguard_peers.go
+++ b/internal/app/wireguard/wireguard_peers.go
@@ -62,8 +62,10 @@ func (m Manager) GetUserPeers(ctx context.Context, id domain.UserIdentifier) ([]
}
func (m Manager) PreparePeer(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Peer, error) {
- if err := domain.ValidateAdminAccessRights(ctx); err != nil {
- return nil, err // TODO: self provisioning?
+ if !m.cfg.Core.SelfProvisioningAllowed {
+ if err := domain.ValidateAdminAccessRights(ctx); err != nil {
+ return nil, err
+ }
}
currentUser := domain.GetUserInfo(ctx)
@@ -73,6 +75,10 @@ func (m Manager) PreparePeer(ctx context.Context, id domain.InterfaceIdentifier)
return nil, fmt.Errorf("unable to find interface %s: %w", id, err)
}
+ if m.cfg.Core.SelfProvisioningAllowed && iface.Type != domain.InterfaceTypeServer {
+ return nil, fmt.Errorf("self provisioning is only allowed for server interfaces: %w", domain.ErrNoPermission)
+ }
+
ips, err := m.getFreshPeerIpConfig(ctx, iface)
if err != nil {
return nil, fmt.Errorf("unable to get fresh ip addresses: %w", err)
@@ -149,10 +155,18 @@ func (m Manager) GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain
}
func (m Manager) CreatePeer(ctx context.Context, peer *domain.Peer) (*domain.Peer, error) {
- if err := domain.ValidateUserAccessRights(ctx, peer.UserIdentifier); err != nil {
- return nil, err
+ if !m.cfg.Core.SelfProvisioningAllowed {
+ if err := domain.ValidateAdminAccessRights(ctx); err != nil {
+ return nil, err
+ }
+ } else {
+ if err := domain.ValidateUserAccessRights(ctx, peer.UserIdentifier); err != nil {
+ return nil, err
+ }
}
+ sessionUser := domain.GetUserInfo(ctx)
+
existingPeer, err := m.db.GetPeer(ctx, peer.Identifier)
if err != nil && !errors.Is(err, domain.ErrNotFound) {
return nil, fmt.Errorf("unable to load existing peer %s: %w", peer.Identifier, err)
@@ -161,6 +175,18 @@ func (m Manager) CreatePeer(ctx context.Context, peer *domain.Peer) (*domain.Pee
return nil, fmt.Errorf("peer %s already exists: %w", peer.Identifier, domain.ErrDuplicateEntry)
}
+ // if a peer is self provisioned, ensure that only allowed fields are set from the request
+ if !sessionUser.IsAdmin {
+ preparedPeer, err := m.PreparePeer(ctx, peer.InterfaceIdentifier)
+ if err != nil {
+ return nil, fmt.Errorf("failed to prepare peer for interface %s: %w", peer.InterfaceIdentifier, err)
+ }
+
+ preparedPeer.OverwriteUserEditableFields(peer)
+
+ peer = preparedPeer
+ }
+
if err := m.validatePeerCreation(ctx, existingPeer, peer); err != nil {
return nil, fmt.Errorf("creation not allowed: %w", err)
}
@@ -229,6 +255,19 @@ func (m Manager) UpdatePeer(ctx context.Context, peer *domain.Peer) (*domain.Pee
return nil, fmt.Errorf("update not allowed: %w", err)
}
+ sessionUser := domain.GetUserInfo(ctx)
+
+ // if a peer is self provisioned, ensure that only allowed fields are set from the request
+ if !sessionUser.IsAdmin {
+ originalPeer, err := m.db.GetPeer(ctx, peer.Identifier)
+ if err != nil {
+ return nil, fmt.Errorf("unable to load existing peer %s: %w", peer.Identifier, err)
+ }
+ originalPeer.OverwriteUserEditableFields(peer)
+
+ peer = originalPeer
+ }
+
// handle peer identifier change (new public key)
if existingPeer.Identifier != domain.PeerIdentifier(peer.Interface.PublicKey) {
peer.Identifier = domain.PeerIdentifier(peer.Interface.PublicKey) // set new identifier
@@ -438,7 +477,7 @@ func (m Manager) getFreshPeerIpConfig(ctx context.Context, iface *domain.Interfa
func (m Manager) validatePeerModifications(ctx context.Context, old, new *domain.Peer) error {
currentUser := domain.GetUserInfo(ctx)
- if !currentUser.IsAdmin {
+ if !currentUser.IsAdmin && !m.cfg.Core.SelfProvisioningAllowed {
return domain.ErrNoPermission
}
@@ -452,7 +491,7 @@ func (m Manager) validatePeerCreation(ctx context.Context, old, new *domain.Peer
return fmt.Errorf("invalid peer identifier: %w", domain.ErrInvalidData)
}
- if !currentUser.IsAdmin {
+ if !currentUser.IsAdmin && !m.cfg.Core.SelfProvisioningAllowed {
return domain.ErrNoPermission
}
@@ -467,7 +506,7 @@ func (m Manager) validatePeerCreation(ctx context.Context, old, new *domain.Peer
func (m Manager) validatePeerDeletion(ctx context.Context, del *domain.Peer) error {
currentUser := domain.GetUserInfo(ctx)
- if !currentUser.IsAdmin {
+ if !currentUser.IsAdmin && !m.cfg.Core.SelfProvisioningAllowed {
return domain.ErrNoPermission
}
diff --git a/internal/domain/interface.go b/internal/domain/interface.go
index 8d618ac..892aaef 100644
--- a/internal/domain/interface.go
+++ b/internal/domain/interface.go
@@ -72,6 +72,17 @@ type Interface struct {
PeerDefPostDown string // default action that is executed after the device is down
}
+// PublicInfo returns a copy of the interface with only the public information.
+// Sensible information like keys are not included.
+func (i *Interface) PublicInfo() Interface {
+ return Interface{
+ Identifier: i.Identifier,
+ DisplayName: i.DisplayName,
+ Type: i.Type,
+ Disabled: i.Disabled,
+ }
+}
+
// Validate performs checks to ensure that the interface is valid.
func (i *Interface) Validate() error {
// validate peer default endpoint, add port if needed
diff --git a/internal/domain/peer.go b/internal/domain/peer.go
index f496114..34dfab6 100644
--- a/internal/domain/peer.go
+++ b/internal/domain/peer.go
@@ -127,6 +127,19 @@ func (p *Peer) GenerateDisplayName(prefix string) {
p.DisplayName = fmt.Sprintf("%sPeer %s", prefix, internal.TruncateString(string(p.Identifier), 8))
}
+// OverwriteUserEditableFields overwrites the user editable fields of the peer with the values from the userPeer
+func (p *Peer) OverwriteUserEditableFields(userPeer *Peer) {
+ p.DisplayName = userPeer.DisplayName
+ p.Interface.PublicKey = userPeer.Interface.PublicKey
+ p.Interface.PrivateKey = userPeer.Interface.PrivateKey
+ p.Interface.Mtu = userPeer.Interface.Mtu
+ p.PersistentKeepalive = userPeer.PersistentKeepalive
+ p.ExpiresAt = userPeer.ExpiresAt
+ p.Disabled = userPeer.Disabled
+ p.DisabledReason = userPeer.DisabledReason
+ p.PresharedKey = userPeer.PresharedKey
+}
+
type PeerInterfaceConfig struct {
KeyPair // private/public Key of the peer