feat: add service catalog management with policy integration for domain-based resource access control

Add ServiceCatalogItem type and services CRUD API endpoints (list, create, update, delete). Extend Policy type to include services array with domain, upstream_ip, proxy_ip, and ports metadata.

Add ServicesPage component with table view and create/edit modals for managing service definitions. Include service name, domain, proxy, and upstream columns with port parsing logic.

Integrate service selection
This commit is contained in:
2026-03-18 13:09:54 +01:00
parent 0ac93dfeb6
commit 6cf49ff3e0
25 changed files with 1375 additions and 99 deletions

View File

@@ -71,7 +71,11 @@ func (s *Service) Enroll(ctx context.Context, userID uuid.UUID, input EnrollRequ
if len(selectedDestinations) == 0 {
selectedDestinations = destinations
}
profileAllowedIPs := mergeProfileAllowedIPs(selectedDestinations, selectedGateway.DNSServers, alwaysAllowWebProxyTargets())
selectedServices := servicesForSelectedProfile(availableProfiles, selectedProfileID)
profileAllowedIPs := mergeProfileAllowedIPs(
append(selectedDestinations, proxyRoutesForServices(selectedServices)...),
selectedGateway.DNSServers,
)
enrollment.Peer = PeerView{
AssignedIP: assignedIP,
@@ -85,7 +89,7 @@ func (s *Service) Enroll(ctx context.Context, userID uuid.UUID, input EnrollRequ
},
ProfileRevision: 1,
}
enrollment.Resources = resourcesFromDestinations(selectedDestinations)
enrollment.Resources = resourcesFromProfile(selectedDestinations, selectedServices)
enrollment.AvailableProfiles = availableProfiles
enrollment.SelectedProfileID = selectedProfileID
@@ -215,10 +219,14 @@ func (s *Service) applyCurrentPolicy(ctx context.Context, enrollment EnrollmentR
selectedDestinations = []string{"172.16.10.0/24"}
}
enrollment.Resources = resourcesFromDestinations(selectedDestinations)
selectedServices := servicesForSelectedProfile(availableProfiles, selectedProfileID)
enrollment.Resources = resourcesFromProfile(selectedDestinations, selectedServices)
enrollment.AvailableProfiles = availableProfiles
enrollment.SelectedProfileID = selectedProfileID
enrollment.Peer.AllowedIPs = mergeProfileAllowedIPs(selectedDestinations, enrollment.Peer.DNSServers, alwaysAllowWebProxyTargets())
enrollment.Peer.AllowedIPs = mergeProfileAllowedIPs(
append(selectedDestinations, proxyRoutesForServices(selectedServices)...),
enrollment.Peer.DNSServers,
)
return withDebugProfile(enrollment), nil
}
@@ -230,12 +238,25 @@ func (s *Service) resolveAccessProfiles(ctx context.Context, userID uuid.UUID, d
availableProfiles := make([]AccessProfile, 0, len(profiles))
for _, profile := range profiles {
services := make([]AccessService, 0, len(profile.Services))
for _, service := range profile.Services {
services = append(services, AccessService{
ID: service.ID,
Name: service.Name,
Description: service.Description,
Domain: service.Domain,
UpstreamIP: service.UpstreamIP,
ProxyIP: service.ProxyIP,
Ports: service.Ports,
})
}
availableProfiles = append(availableProfiles, AccessProfile{
ID: profile.ID,
Name: profile.Name,
Description: profile.Description,
FullTunnel: profile.FullTunnel,
Destinations: profile.Destinations,
Services: services,
})
}
@@ -273,9 +294,64 @@ func resourcesFromDestinations(destinations []string) []Resource {
return resources
}
func mergeProfileAllowedIPs(destinations []string, dnsServers []string, webProxyTargets []string) []string {
seen := make(map[string]struct{}, len(destinations)+len(dnsServers)+len(webProxyTargets))
merged := make([]string, 0, len(destinations)+len(dnsServers)+len(webProxyTargets))
func resourcesFromProfile(destinations []string, services []AccessService) []Resource {
resources := resourcesFromDestinations(destinations)
for _, service := range services {
resources = append(resources, Resource{
Type: "service",
Value: service.Domain,
Label: service.Name,
Domain: service.Domain,
})
}
return resources
}
func servicesForSelectedProfile(profiles []AccessProfile, selectedProfileID *uuid.UUID) []AccessService {
if selectedProfileID == nil {
if len(profiles) == 0 {
return nil
}
return profiles[0].Services
}
for _, profile := range profiles {
if profile.ID == *selectedProfileID {
return profile.Services
}
}
return nil
}
func proxyRoutesForServices(services []AccessService) []string {
seen := make(map[string]struct{}, len(services))
routes := make([]string, 0, len(services))
for _, service := range services {
route := dnsServerRoute(effectiveServiceProxyIP(service.ProxyIP))
if route == "" {
continue
}
if _, ok := seen[route]; ok {
continue
}
seen[route] = struct{}{}
routes = append(routes, route)
}
return routes
}
func effectiveServiceProxyIP(proxyIP string) string {
override := strings.TrimSpace(os.Getenv("NEXAVPN_ACCESS_PROXY_IP"))
if override != "" {
return override
}
return proxyIP
}
func mergeProfileAllowedIPs(destinations []string, dnsServers []string) []string {
seen := make(map[string]struct{}, len(destinations)+len(dnsServers))
merged := make([]string, 0, len(destinations)+len(dnsServers))
for _, destination := range destinations {
destination = strings.TrimSpace(destination)
@@ -301,18 +377,6 @@ func mergeProfileAllowedIPs(destinations []string, dnsServers []string, webProxy
merged = append(merged, route)
}
for _, target := range webProxyTargets {
route := dnsServerRoute(target)
if route == "" {
continue
}
if _, exists := seen[route]; exists {
continue
}
seen[route] = struct{}{}
merged = append(merged, route)
}
return merged
}
@@ -326,25 +390,3 @@ func dnsServerRoute(value string) string {
}
return value + "/32"
}
func alwaysAllowWebProxyTargets() []string {
raw := os.Getenv("NEXAVPN_ALWAYS_ALLOW_WEB_PROXY_IPS")
if strings.TrimSpace(raw) == "" {
return nil
}
seen := make(map[string]struct{})
targets := make([]string, 0)
for _, part := range strings.Split(raw, ",") {
value := strings.TrimSpace(part)
if value == "" {
continue
}
if _, ok := seen[value]; ok {
continue
}
seen[value] = struct{}{}
targets = append(targets, value)
}
return targets
}

View File

@@ -34,6 +34,7 @@ type Resource struct {
Type string `json:"type"`
Value string `json:"value"`
Label string `json:"label"`
Domain string `json:"domain,omitempty"`
}
type EnrollmentResponse struct {
@@ -51,6 +52,17 @@ type AccessProfile struct {
Description string `json:"description"`
FullTunnel bool `json:"full_tunnel"`
Destinations []string `json:"destinations"`
Services []AccessService `json:"services"`
}
type AccessService struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Domain string `json:"domain"`
UpstreamIP string `json:"upstream_ip"`
ProxyIP string `json:"proxy_ip"`
Ports []int `json:"ports"`
}
type PeerView struct {