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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user