Commit b8e17bf9 authored by Eric Dieckman's avatar Eric Dieckman
Browse files

feat: add Infoblox group controller, mf_groups consolidation, and PA access...

feat: add Infoblox group controller, mf_groups consolidation, and PA access domain junction endpoints
parent 4856846a
Loading
Loading
Loading
Loading
Loading
+5 −1
Original line number Diff line number Diff line
@@ -60,7 +60,9 @@ All write endpoints (POST/PUT) require `[Authorize(Policy = Policies.CanEdit)]`.

## Database Migrations

Migrations are plain SQL files in `src/migrations/scripts/`. To add a migration, create a new timestamped `.sql` file (format: `YYYYMMDDHHMMSS_Description.sql`) and apply it manually to the target database.
Migrations are plain SQL files in `src/migrations/scripts/`. To add a migration, create a new timestamped `.sql` file (format: `YYYYMMDD_HHMM_Description.sql`) and apply it manually to the target database.

Each migration must be marked as an Embedded Resource in the Migrations.csproj

## NuGet Sources

@@ -72,5 +74,7 @@ Requires access to two internal feeds (configured in `nuget.config`):

Commits must follow Conventional Commits format (enforced by commitlint in CI). Semantic-release uses commits to determine version bumps.

Never commit automatically — always wait for the user to explicitly request a commit.

## Related Repos
- SPA: ../spa (Vue 3.5 frontend)
+87 −0
Original line number Diff line number Diff line
using Cals.Visor.Api.Authorization;
using Cals.Visor.Infrastructure.Contexts;
using Cals.Visor.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace Cals.Visor.Api.Controllers;

[Route("infoblox/groups")]
[ApiController]
public class InfobloxGroupsController : ControllerBase
{
    private readonly ApplicationDbContext _context;

    public InfobloxGroupsController(ApplicationDbContext context) => _context = context;

    // GET: /infoblox/groups
    [ProducesResponseType<IList<InfobloxGroup>>(StatusCodes.Status200OK)]
    [HttpGet(Name = "GetAllInfobloxGroups")]
    public async Task<IActionResult> GetAllAsync()
    {
        List<InfobloxGroup> result = await _context.InfobloxGroups
            .Where(e => e.IsActive == true)
            .ToListAsync();

        return Ok(result);
    }

    // GET: /infoblox/groups/1
    [ProducesResponseType<InfobloxGroup>(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    [HttpGet("{groupId:long}", Name = "GetInfobloxGroupById")]
    public async Task<IActionResult> GetByIdAsync(long groupId)
    {
        InfobloxGroup? result = await _context.InfobloxGroups
            .Where(e => e.InfobloxGroupId == groupId)
            .FirstOrDefaultAsync();

        if (result == null)
            return NotFound();

        return Ok(result);
    }

    // POST: /infoblox/groups
    [Authorize(Policy = Policies.CanEdit)]
    [ProducesResponseType<InfobloxGroup>(StatusCodes.Status201Created)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    [HttpPost(Name = "CreateNewInfobloxGroup")]
    public async Task<IActionResult> CreateNewAsync(InfobloxGroup data)
    {
        if (data.MfGroupId == null)
            return BadRequest("MfGroupId is required.");

        if (!await _context.ManifestGroups.AnyAsync(g => g.ManifestGroupId == data.MfGroupId))
            return BadRequest("Specified ManifestGroup does not exist.");

        _context.Add(data);
        await _context.SaveChangesAsync();

        return CreatedAtRoute("GetInfobloxGroupById", new { groupId = data.InfobloxGroupId }, data);
    }

    // PUT: /infoblox/groups/2
    [Authorize(Policy = Policies.CanEdit)]
    [ProducesResponseType<InfobloxGroup>(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    [HttpPut("{groupId:long}", Name = "UpdateInfobloxGroup")]
    public async Task<IActionResult> UpdateAsync(long groupId, InfobloxGroup data)
    {
        InfobloxGroup? entity = await _context.InfobloxGroups
            .Where(e => e.InfobloxGroupId == groupId)
            .FirstOrDefaultAsync();

        if (entity == null)
            return NotFound();

        entity.IsActive = data.IsActive;
        entity.MfGroupId = data.MfGroupId;

        await _context.SaveChangesAsync();

        return Ok(entity);
    }
}
+79 −0
Original line number Diff line number Diff line
using Cals.Visor.Api.Authorization;
using Cals.Visor.Infrastructure.Contexts;
using Cals.Visor.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace Cals.Visor.Api.Controllers;

[Route("manifest/groups")]
[ApiController]
public class ManifestGroupsController : ControllerBase
{
    private readonly ApplicationDbContext _context;

    public ManifestGroupsController(ApplicationDbContext context) => _context = context;

    // GET: /manifest/groups
    [ProducesResponseType<IList<ManifestGroup>>(StatusCodes.Status200OK)]
    [HttpGet(Name = "GetAllManifestGroups")]
    public async Task<IActionResult> GetAllAsync()
    {
        List<ManifestGroup> result = await _context.ManifestGroups
            .ToListAsync();

        return Ok(result);
    }

    // GET: /manifest/groups/1
    [ProducesResponseType<ManifestGroup>(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    [HttpGet("{manifestGroupId:long}", Name = "GetManifestGroupById")]
    public async Task<IActionResult> GetByIdAsync(long manifestGroupId)
    {
        ManifestGroup? result = await _context.ManifestGroups
            .Where(e => e.ManifestGroupId == manifestGroupId)
            .FirstOrDefaultAsync();

        if (result == null)
            return NotFound();

        return Ok(result);
    }

    // POST: /manifest/groups
    [Authorize(Policy = Policies.CanEdit)]
    [ProducesResponseType<ManifestGroup>(StatusCodes.Status201Created)]
    [HttpPost(Name = "CreateManifestGroup")]
    public async Task<IActionResult> CreateNewAsync(ManifestGroup data)
    {
        _context.Add(data);
        await _context.SaveChangesAsync();

        return CreatedAtRoute("GetManifestGroupById", new { manifestGroupId = data.ManifestGroupId }, data);
    }

    // PUT: /manifest/groups/2
    [Authorize(Policy = Policies.CanEdit)]
    [ProducesResponseType<ManifestGroup>(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    [HttpPut("{manifestGroupId:long}", Name = "UpdateManifestGroup")]
    public async Task<IActionResult> UpdateAsync(long manifestGroupId, ManifestGroup data)
    {
        ManifestGroup? entity = await _context.ManifestGroups
            .Where(e => e.ManifestGroupId == manifestGroupId)
            .FirstOrDefaultAsync();

        if (entity == null)
            return NotFound();

        entity.Uuid = data.Uuid;
        entity.Path = data.Path;

        await _context.SaveChangesAsync();

        return Ok(entity);
    }
}
+141 −9
Original line number Diff line number Diff line
@@ -2,10 +2,12 @@ using Cals.Visor.Api.Authorization;
using Cals.Visor.Infrastructure.Contexts;
using Cals.Visor.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace Cals.Visor.Api.Controllers;

[Route("palo_alto/access_domains")]
[ApiController]
public class PaloAltoAccessDomainsController : ControllerBase
@@ -14,7 +16,7 @@ public class PaloAltoAccessDomainsController : ControllerBase

    public PaloAltoAccessDomainsController(ApplicationDbContext context) => _context = context;

    // GET: /api/palo_alto/access_domains
    // GET: /palo_alto/access_domains
    [ProducesResponseType<IList<PaloAltoAccessDomain>>(StatusCodes.Status200OK)]
    [HttpGet(Name = "GetAccessDomains")]
    public async Task<IActionResult> GetAllAsync()
@@ -26,7 +28,7 @@ public class PaloAltoAccessDomainsController : ControllerBase
        return Ok(result);
    }

    // GET: /api/palo_alto/access_domains/1
    // GET: /palo_alto/access_domains/1
    [ProducesResponseType<PaloAltoAccessDomain>(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    [HttpGet("{accessDomainId:long}", Name = "GetAccessDomainById")]
@@ -42,24 +44,155 @@ public class PaloAltoAccessDomainsController : ControllerBase
        return Ok(result);
    }

    // POST: /api/palo_alto/access_domains
    // POST: /palo_alto/access_domains
    [Authorize(Policy = Policies.CanEdit)]
    [ProducesResponseType<PaloAltoAccessDomain>(StatusCodes.Status201Created)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    [HttpPost(Name = "CreateAccessDomain")]
    public async Task<IActionResult> CreateAsync([FromBody] PaloAltoAccessDomain data)
    {
        if (data.MfGroupId == null)
            return BadRequest("MfGroupId is required.");

        if (!await _context.ManifestGroups.AnyAsync(g => g.ManifestGroupId == data.MfGroupId))
            return BadRequest("Specified ManifestGroup does not exist.");

        _context.Add(data);
        await _context.SaveChangesAsync();

        return CreatedAtRoute("GetAccessDomainById", new { accessDomainId = data.AccessDomainId }, data);
    }

    // PUT: /api/palo_alto/access_domains/1
    // GET: /palo_alto/access_domains/1/firewalls
    [ProducesResponseType<IList<PaloAltoFirewall>>(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    [HttpGet("{accessDomainId:long}/firewalls", Name = "GetFirewallsByAccessDomain")]
    public async Task<IActionResult> GetFirewallsByAccessDomainAsync(long accessDomainId)
    {
        if (!await _context.PaloAltoAccessDomains.AnyAsync(e => e.AccessDomainId == accessDomainId))
            return NotFound();

        List<PaloAltoFirewall?> firewalls = await _context.PaloAltoAccessDomainFirewalls
            .Where(e => e.AccessDomainId == accessDomainId)
            .Select(e => e.Firewall)
            .ToListAsync();

        return Ok(firewalls);
    }

    // POST: /palo_alto/access_domains/1/firewalls/2
    [Authorize(Policy = Policies.CanEdit)]
    [ProducesResponseType<PaloAltoAccessDomainFirewall>(StatusCodes.Status201Created)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    [HttpPost("{accessDomainId:long}/firewalls/{firewallId:long}", Name = "AssignFirewallToAccessDomain")]
    public async Task<IActionResult> AssignFirewallToAccessDomainAsync(long accessDomainId, long firewallId)
    {
        if (!await _context.PaloAltoAccessDomains.AnyAsync(e => e.AccessDomainId == accessDomainId))
            return NotFound();

        if (!await _context.PaloAltoFirewalls.AnyAsync(e => e.FirewallId == firewallId))
            return NotFound();

        var mapping = new PaloAltoAccessDomainFirewall
        {
            AccessDomainId = accessDomainId,
            FirewallId = firewallId,
        };

        _context.PaloAltoAccessDomainFirewalls.Add(mapping);
        await _context.SaveChangesAsync();

        return CreatedAtRoute("GetAccessDomainById", new { accessDomainId }, mapping);
    }

    // DELETE: /palo_alto/access_domains/1/firewalls/2
    [Authorize(Policy = Policies.CanEdit)]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    [HttpDelete("{accessDomainId:long}/firewalls/{firewallId:long}", Name = "RemoveFirewallFromAccessDomain")]
    public async Task<IActionResult> RemoveFirewallFromAccessDomainAsync(long accessDomainId, long firewallId)
    {
        PaloAltoAccessDomainFirewall? mapping = await _context.PaloAltoAccessDomainFirewalls
            .Where(e => e.AccessDomainId == accessDomainId && e.FirewallId == firewallId)
            .FirstOrDefaultAsync();

        if (mapping == null)
            return NotFound();

        _context.PaloAltoAccessDomainFirewalls.Remove(mapping);
        await _context.SaveChangesAsync();

        return NoContent();
    }

    // GET: /palo_alto/access_domains/1/vsyses
    [ProducesResponseType<IList<PaloAltoVsys>>(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    [HttpGet("{accessDomainId:long}/vsyses", Name = "GetVsysesByAccessDomain")]
    public async Task<IActionResult> GetVsysesByAccessDomainAsync(long accessDomainId)
    {
        if (!await _context.PaloAltoAccessDomains.AnyAsync(e => e.AccessDomainId == accessDomainId))
            return NotFound();

        List<PaloAltoVsys?> vsyses = await _context.PaloAltoAccessDomainVsyses
            .Where(e => e.AccessDomainId == accessDomainId)
            .Select(e => e.Vsys)
            .ToListAsync();

        return Ok(vsyses);
    }

    // POST: /palo_alto/access_domains/1/vsyses/2
    [Authorize(Policy = Policies.CanEdit)]
    [ProducesResponseType<PaloAltoAccessDomainVsys>(StatusCodes.Status201Created)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    [HttpPost("{accessDomainId:long}/vsyses/{vsysId:long}", Name = "AssignVsysToAccessDomain")]
    public async Task<IActionResult> AssignVsysToAccessDomainAsync(long accessDomainId, long vsysId)
    {
        if (!await _context.PaloAltoAccessDomains.AnyAsync(e => e.AccessDomainId == accessDomainId))
            return NotFound();

        if (!await _context.PaloAltoVsyses.AnyAsync(e => e.VsysId == vsysId))
            return NotFound();

        var mapping = new PaloAltoAccessDomainVsys
        {
            AccessDomainId = accessDomainId,
            VsysId = vsysId,
        };

        _context.PaloAltoAccessDomainVsyses.Add(mapping);
        await _context.SaveChangesAsync();

        return CreatedAtRoute("GetAccessDomainById", new { accessDomainId }, mapping);
    }

    // DELETE: /palo_alto/access_domains/1/vsyses/2
    [Authorize(Policy = Policies.CanEdit)]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    [HttpDelete("{accessDomainId:long}/vsyses/{vsysId:long}", Name = "RemoveVsysFromAccessDomain")]
    public async Task<IActionResult> RemoveVsysFromAccessDomainAsync(long accessDomainId, long vsysId)
    {
        PaloAltoAccessDomainVsys? mapping = await _context.PaloAltoAccessDomainVsyses
            .Where(e => e.AccessDomainId == accessDomainId && e.VsysId == vsysId)
            .FirstOrDefaultAsync();

        if (mapping == null)
            return NotFound();

        _context.PaloAltoAccessDomainVsyses.Remove(mapping);
        await _context.SaveChangesAsync();

        return NoContent();
    }

    // PUT: /palo_alto/access_domains/1
    [Authorize(Policy = Policies.CanEdit)]
    [ProducesResponseType<PaloAltoAccessDomain>(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    [HttpPut("{accessDomainId:long}", Name = "UpdateAccessDomain")]
    public async Task<IActionResult> GetByIdAsync(long accessDomainId, [FromBody] PaloAltoAccessDomain data)
    public async Task<IActionResult> UpdateAsync(long accessDomainId, [FromBody] PaloAltoAccessDomain data)
    {
        PaloAltoAccessDomain? entity = await _context.PaloAltoAccessDomains
            .Where(e => e.AccessDomainId == accessDomainId)
@@ -69,9 +202,8 @@ public class PaloAltoAccessDomainsController : ControllerBase
            return NotFound();

        entity.IsActive = data.IsActive;
        entity.ManifestPath = data.ManifestPath;
        entity.ManifestUuid = data.ManifestUuid;
        entity.Name = data.Name;
        entity.MfGroupId = data.MfGroupId;

        await _context.SaveChangesAsync();

+3 −0
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@ public class ApplicationDbContext : DbContext
    public DbSet<AppUser> Users => Set<AppUser>();
    public DbSet<Area> Areas => Set<Area>();
    public DbSet<AuditLog> AuditLogs => Set<AuditLog>();
    public DbSet<ManifestGroup> ManifestGroups => Set<ManifestGroup>();
    public DbSet<InfobloxDomain> InfobloxDomains => Set<InfobloxDomain>();
    public DbSet<InfobloxGroup> InfobloxGroups => Set<InfobloxGroup>();
    public DbSet<InfobloxNetwork> InfobloxNetworks => Set<InfobloxNetwork>();
@@ -30,6 +31,8 @@ public class ApplicationDbContext : DbContext
    public DbSet<Location> Locations => Set<Location>();
    public DbSet<LocationVlan> LocationVlans => Set<LocationVlan>();
    public DbSet<PaloAltoAccessDomain> PaloAltoAccessDomains => Set<PaloAltoAccessDomain>();
    public DbSet<PaloAltoAccessDomainFirewall> PaloAltoAccessDomainFirewalls => Set<PaloAltoAccessDomainFirewall>();
    public DbSet<PaloAltoAccessDomainVsys> PaloAltoAccessDomainVsyses => Set<PaloAltoAccessDomainVsys>();
    public DbSet<PaloAltoFirewall> PaloAltoFirewalls => Set<PaloAltoFirewall>();
    public DbSet<PaloAltoFirewallVlan> PaloAltoFirewallVlans => Set<PaloAltoFirewallVlan>();
    public DbSet<PaloAltoVsys> PaloAltoVsyses => Set<PaloAltoVsys>();
Loading