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:
@@ -94,7 +94,8 @@ func (r *PGRepository) BuildSyncBundle(ctx context.Context, gatewayID uuid.UUID)
|
||||
wp.public_key,
|
||||
set_masklen(wp.assigned_ip, 32)::text,
|
||||
coalesce(array_agg(distinct pd.destination::text) filter (where pd.destination is not null), '{}'),
|
||||
coalesce(g.dns_servers, '{}')::text[]
|
||||
coalesce(g.dns_servers, '{}')::text[],
|
||||
s.value->>'profile_id'
|
||||
from devices d
|
||||
join wireguard_peers wp on wp.device_id = d.id and wp.deleted_at is null
|
||||
join gateways g on g.id = d.gateway_id
|
||||
@@ -115,7 +116,7 @@ func (r *PGRepository) BuildSyncBundle(ctx context.Context, gatewayID uuid.UUID)
|
||||
or p.id::text = s.value->>'profile_id'
|
||||
)
|
||||
where d.gateway_id = $1 and d.deleted_at is null and d.status = 'active'
|
||||
group by d.id, wp.public_key, wp.assigned_ip, g.dns_servers
|
||||
group by d.id, wp.public_key, wp.assigned_ip, g.dns_servers, s.value
|
||||
`, gatewayID)
|
||||
if err != nil {
|
||||
return wireguard.GatewayBundle{}, err
|
||||
@@ -125,37 +126,70 @@ func (r *PGRepository) BuildSyncBundle(ctx context.Context, gatewayID uuid.UUID)
|
||||
for rows.Next() {
|
||||
var peer wireguard.Peer
|
||||
var deviceID uuid.UUID
|
||||
if err := rows.Scan(&deviceID, &peer.PublicKey, &peer.AssignedIP, &peer.AllowedDestinations, &peer.DNSServers); err != nil {
|
||||
var selectedProfileID *string
|
||||
if err := rows.Scan(&deviceID, &peer.PublicKey, &peer.AssignedIP, &peer.AllowedDestinations, &peer.DNSServers, &selectedProfileID); err != nil {
|
||||
return wireguard.GatewayBundle{}, err
|
||||
}
|
||||
peer.DeviceID = deviceID.String()
|
||||
peer.WebProxyTargets = alwaysAllowWebProxyTargets()
|
||||
services, err := r.listAllowedServices(ctx, deviceID, selectedProfileID)
|
||||
if err != nil {
|
||||
return wireguard.GatewayBundle{}, err
|
||||
}
|
||||
peer.AllowedServices = services
|
||||
bundle.Peers = append(bundle.Peers, peer)
|
||||
}
|
||||
|
||||
return bundle, rows.Err()
|
||||
}
|
||||
|
||||
func alwaysAllowWebProxyTargets() []string {
|
||||
raw := os.Getenv("NEXAVPN_ALWAYS_ALLOW_WEB_PROXY_IPS")
|
||||
if strings.TrimSpace(raw) == "" {
|
||||
return nil
|
||||
func (r *PGRepository) listAllowedServices(ctx context.Context, deviceID uuid.UUID, selectedProfileID *string) ([]wireguard.AllowedService, error) {
|
||||
rows, err := r.db.Query(ctx, `
|
||||
select distinct
|
||||
s.name,
|
||||
s.domain,
|
||||
host(s.upstream_ip),
|
||||
host(s.proxy_ip),
|
||||
s.ports
|
||||
from devices d
|
||||
left join group_memberships gm on gm.user_id = d.user_id
|
||||
join policy_targets pt on (
|
||||
(pt.target_type = 'device' and pt.target_id = d.id) or
|
||||
(pt.target_type = 'user' and pt.target_id = d.user_id) or
|
||||
(pt.target_type = 'group' and pt.target_id = gm.group_id)
|
||||
)
|
||||
join policies p on p.id = pt.policy_id
|
||||
and p.deleted_at is null
|
||||
and p.is_active = true
|
||||
and p.effect = 'allow'
|
||||
join policy_services ps on ps.policy_id = p.id
|
||||
join services s on s.id = ps.service_id and s.deleted_at is null and s.is_active = true
|
||||
where d.id = $1
|
||||
and ($2::text is null or p.id::text = $2::text)
|
||||
order by s.name asc
|
||||
`, deviceID, selectedProfileID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
seen := make(map[string]struct{})
|
||||
targets := make([]string, 0)
|
||||
for _, part := range strings.Split(raw, ",") {
|
||||
value := strings.TrimSpace(part)
|
||||
if value == "" {
|
||||
continue
|
||||
var items []wireguard.AllowedService
|
||||
for rows.Next() {
|
||||
var item wireguard.AllowedService
|
||||
if err := rows.Scan(&item.Name, &item.Domain, &item.UpstreamIP, &item.ProxyIP, &item.Ports); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, ok := seen[value]; ok {
|
||||
continue
|
||||
}
|
||||
seen[value] = struct{}{}
|
||||
targets = append(targets, value)
|
||||
item.AccessProxyIP = effectiveAccessProxyIP(item.ProxyIP)
|
||||
items = append(items, item)
|
||||
}
|
||||
return targets
|
||||
return items, rows.Err()
|
||||
}
|
||||
|
||||
func effectiveAccessProxyIP(proxyIP string) string {
|
||||
override := strings.TrimSpace(os.Getenv("NEXAVPN_ACCESS_PROXY_IP"))
|
||||
if override != "" {
|
||||
return override
|
||||
}
|
||||
return proxyIP
|
||||
}
|
||||
|
||||
func (r *PGRepository) Update(ctx context.Context, gatewayID uuid.UUID, input UpdateRequest) (Gateway, error) {
|
||||
|
||||
Reference in New Issue
Block a user