feat: add access profile selection support with device-specific profile persistence

Add SelectOwnProfile handler to allow users to choose from available access profiles. Store selected profile ID per device in settings table with device_access_profile category. Implement GetSelectedProfileID and SetSelectedProfileID repository methods using JSONB storage.

Add ListSelectableProfiles to policy repository and service to query user/group/device-specific profiles ordered by priority. Filter gateway
This commit is contained in:
2026-03-18 12:21:48 +01:00
parent 1ddcbf0b14
commit aaa601a8ba
14 changed files with 549 additions and 43 deletions

View File

@@ -2,6 +2,7 @@ package device
import (
"context"
"fmt"
"os"
"strings"
@@ -63,7 +64,14 @@ func (s *Service) Enroll(ctx context.Context, userID uuid.UUID, input EnrollRequ
if len(destinations) == 0 {
destinations = []string{"172.16.10.0/24"}
}
profileAllowedIPs := mergeProfileAllowedIPs(destinations, selectedGateway.DNSServers, alwaysAllowWebProxyTargets())
availableProfiles, selectedProfileID, selectedDestinations, err := s.resolveAccessProfiles(ctx, userID, enrollment.Device.ID)
if err != nil {
return EnrollmentResponse{}, err
}
if len(selectedDestinations) == 0 {
selectedDestinations = destinations
}
profileAllowedIPs := mergeProfileAllowedIPs(selectedDestinations, selectedGateway.DNSServers, alwaysAllowWebProxyTargets())
enrollment.Peer = PeerView{
AssignedIP: assignedIP,
@@ -77,13 +85,9 @@ func (s *Service) Enroll(ctx context.Context, userID uuid.UUID, input EnrollRequ
},
ProfileRevision: 1,
}
for _, destination := range destinations {
enrollment.Resources = append(enrollment.Resources, Resource{
Type: "cidr",
Value: destination,
Label: destination,
})
}
enrollment.Resources = resourcesFromDestinations(selectedDestinations)
enrollment.AvailableProfiles = availableProfiles
enrollment.SelectedProfileID = selectedProfileID
enrollment.Profile = ProfileView{
Format: "wireguard",
@@ -125,6 +129,35 @@ func (s *Service) GetEnrollmentByDeviceID(ctx context.Context, deviceID uuid.UUI
return s.applyCurrentPolicy(ctx, enrollment)
}
func (s *Service) SelectProfile(ctx context.Context, userID uuid.UUID, profileID uuid.UUID) (EnrollmentResponse, error) {
enrollment, err := s.repo.GetLatestEnrollmentByUser(ctx, userID)
if err != nil {
return EnrollmentResponse{}, err
}
profiles, err := s.policyService.ListSelectableProfiles(ctx, userID, &enrollment.Device.ID)
if err != nil {
return EnrollmentResponse{}, err
}
var exists bool
for _, profile := range profiles {
if profile.ID == profileID {
exists = true
break
}
}
if !exists {
return EnrollmentResponse{}, fmt.Errorf("selected access profile is not available for this device")
}
if err := s.repo.SetSelectedProfileID(ctx, enrollment.Device.ID, profileID); err != nil {
return EnrollmentResponse{}, err
}
return s.applyCurrentPolicy(ctx, enrollment)
}
func (s *Service) GetConnectionStatus(ctx context.Context, userID uuid.UUID) (ConnectionStatus, error) {
enrollment, err := s.repo.GetLatestEnrollmentByUser(ctx, userID)
if err != nil {
@@ -134,6 +167,11 @@ func (s *Service) GetConnectionStatus(ctx context.Context, userID uuid.UUID) (Co
}, nil
}
enrollment, err = s.applyCurrentPolicy(ctx, enrollment)
if err != nil {
return ConnectionStatus{}, err
}
lastSync := "just now"
return ConnectionStatus{
Status: "provisioned",
@@ -169,24 +207,70 @@ func withDebugProfile(enrollment EnrollmentResponse) EnrollmentResponse {
}
func (s *Service) applyCurrentPolicy(ctx context.Context, enrollment EnrollmentResponse) (EnrollmentResponse, error) {
destinations, err := s.policyService.ResolveDestinations(ctx, enrollment.Device.UserID, &enrollment.Device.ID)
availableProfiles, selectedProfileID, selectedDestinations, err := s.resolveAccessProfiles(ctx, enrollment.Device.UserID, enrollment.Device.ID)
if err != nil {
return EnrollmentResponse{}, err
}
if len(destinations) == 0 {
destinations = []string{"172.16.10.0/24"}
if len(selectedDestinations) == 0 {
selectedDestinations = []string{"172.16.10.0/24"}
}
enrollment.Resources = nil
enrollment.Resources = resourcesFromDestinations(selectedDestinations)
enrollment.AvailableProfiles = availableProfiles
enrollment.SelectedProfileID = selectedProfileID
enrollment.Peer.AllowedIPs = mergeProfileAllowedIPs(selectedDestinations, enrollment.Peer.DNSServers, alwaysAllowWebProxyTargets())
return withDebugProfile(enrollment), nil
}
func (s *Service) resolveAccessProfiles(ctx context.Context, userID uuid.UUID, deviceID uuid.UUID) ([]AccessProfile, *uuid.UUID, []string, error) {
profiles, err := s.policyService.ListSelectableProfiles(ctx, userID, &deviceID)
if err != nil {
return nil, nil, nil, err
}
availableProfiles := make([]AccessProfile, 0, len(profiles))
for _, profile := range profiles {
availableProfiles = append(availableProfiles, AccessProfile{
ID: profile.ID,
Name: profile.Name,
Description: profile.Description,
FullTunnel: profile.FullTunnel,
Destinations: profile.Destinations,
})
}
if len(availableProfiles) == 0 {
return nil, nil, nil, nil
}
selectedProfileID, err := s.repo.GetSelectedProfileID(ctx, deviceID)
if err != nil {
return nil, nil, nil, err
}
for _, profile := range availableProfiles {
if selectedProfileID != nil && profile.ID == *selectedProfileID {
return availableProfiles, selectedProfileID, profile.Destinations, nil
}
}
fallback := availableProfiles[0]
if err := s.repo.SetSelectedProfileID(ctx, deviceID, fallback.ID); err != nil {
return nil, nil, nil, err
}
return availableProfiles, &fallback.ID, fallback.Destinations, nil
}
func resourcesFromDestinations(destinations []string) []Resource {
resources := make([]Resource, 0, len(destinations))
for _, destination := range destinations {
enrollment.Resources = append(enrollment.Resources, Resource{
resources = append(resources, Resource{
Type: "cidr",
Value: destination,
Label: destination,
})
}
enrollment.Peer.AllowedIPs = mergeProfileAllowedIPs(destinations, enrollment.Peer.DNSServers, alwaysAllowWebProxyTargets())
return withDebugProfile(enrollment), nil
return resources
}
func mergeProfileAllowedIPs(destinations []string, dnsServers []string, webProxyTargets []string) []string {